kibana/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
Marco Liberati 0b99841310
[Lens] Performance refactoring for indexpattern fast lookup and Operation support matrix computation (#82829)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
2020-11-10 14:31:04 +01:00

576 lines
19 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import './datapanel.scss';
import { uniq, groupBy } from 'lodash';
import React, { useState, memo, useCallback, useMemo } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiPopover,
EuiCallOut,
EuiFormControlLayout,
EuiSpacer,
EuiFilterGroup,
EuiFilterButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { DataPublicPluginStart, EsQueryConfig, Query, Filter } from 'src/plugins/data/public';
import { DatasourceDataPanelProps, DataType, StateSetter } from '../types';
import { ChildDragDropProvider, DragContextState } from '../drag_drop';
import {
IndexPattern,
IndexPatternPrivateState,
IndexPatternField,
IndexPatternRef,
} from './types';
import { trackUiEvent } from '../lens_ui_telemetry';
import { syncExistingFields } from './loader';
import { fieldExists } from './pure_helpers';
import { Loader } from '../loader';
import { esQuery, IIndexPattern } from '../../../../../src/plugins/data/public';
export type Props = DatasourceDataPanelProps<IndexPatternPrivateState> & {
data: DataPublicPluginStart;
changeIndexPattern: (
id: string,
state: IndexPatternPrivateState,
setState: StateSetter<IndexPatternPrivateState>
) => void;
charts: ChartsPluginSetup;
};
import { LensFieldIcon } from './lens_field_icon';
import { ChangeIndexPattern } from './change_indexpattern';
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import { FieldGroups, FieldList } from './field_list';
function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) {
return fieldA.displayName.localeCompare(fieldB.displayName, undefined, { sensitivity: 'base' });
}
const supportedFieldTypes = new Set(['string', 'number', 'boolean', 'date', 'ip', 'document']);
const fieldTypeNames: Record<DataType, string> = {
document: i18n.translate('xpack.lens.datatypes.record', { defaultMessage: 'record' }),
string: i18n.translate('xpack.lens.datatypes.string', { defaultMessage: 'string' }),
number: i18n.translate('xpack.lens.datatypes.number', { defaultMessage: 'number' }),
boolean: i18n.translate('xpack.lens.datatypes.boolean', { defaultMessage: 'boolean' }),
date: i18n.translate('xpack.lens.datatypes.date', { defaultMessage: 'date' }),
ip: i18n.translate('xpack.lens.datatypes.ipAddress', { defaultMessage: 'IP' }),
};
// Wrapper around esQuery.buildEsQuery, handling errors (e.g. because a query can't be parsed) by
// returning a query dsl object not matching anything
function buildSafeEsQuery(
indexPattern: IIndexPattern,
query: Query,
filters: Filter[],
queryConfig: EsQueryConfig
) {
try {
return esQuery.buildEsQuery(indexPattern, query, filters, queryConfig);
} catch (e) {
return {
bool: {
must_not: {
match_all: {},
},
},
};
}
}
export function IndexPatternDataPanel({
setState,
state,
dragDropContext,
core,
data,
query,
filters,
dateRange,
changeIndexPattern,
charts,
showNoDataPopover,
}: Props) {
const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state;
const onChangeIndexPattern = useCallback(
(id: string) => changeIndexPattern(id, state, setState),
[state, setState, changeIndexPattern]
);
const indexPatternList = uniq(
Object.values(state.layers)
.map((l) => l.indexPatternId)
.concat(currentIndexPatternId)
)
.sort((a, b) => a.localeCompare(b))
.filter((id) => !!indexPatterns[id])
.map((id) => ({
id,
title: indexPatterns[id].title,
timeFieldName: indexPatterns[id].timeFieldName,
fields: indexPatterns[id].fields,
hasRestrictions: indexPatterns[id].hasRestrictions,
}));
const dslQuery = buildSafeEsQuery(
indexPatterns[currentIndexPatternId] as IIndexPattern,
query,
filters,
esQuery.getEsQueryConfig(core.uiSettings)
);
return (
<>
<Loader
load={() =>
syncExistingFields({
dateRange,
setState,
isFirstExistenceFetch: state.isFirstExistenceFetch,
currentIndexPatternTitle: indexPatterns[currentIndexPatternId].title,
showNoDataPopover,
indexPatterns: indexPatternList,
fetchJson: core.http.post,
dslQuery,
})
}
loadDeps={[
query,
filters,
dateRange.fromDate,
dateRange.toDate,
indexPatternList.map((x) => `${x.title}:${x.timeFieldName}`).join(','),
]}
/>
{Object.keys(indexPatterns).length === 0 ? (
<EuiFlexGroup
gutterSize="m"
className="lnsInnerIndexPatternDataPanel"
direction="column"
responsive={false}
>
<EuiFlexItem grow={null}>
<EuiCallOut
data-test-subj="indexPattern-no-indexpatterns"
title={i18n.translate('xpack.lens.indexPattern.noPatternsLabel', {
defaultMessage: 'No index patterns',
})}
color="warning"
iconType="alert"
>
<p>
<FormattedMessage
id="xpack.lens.indexPattern.noPatternsDescription"
defaultMessage="Please create an index pattern or switch to another data source"
/>
</p>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<MemoizedDataPanel
currentIndexPatternId={currentIndexPatternId}
indexPatternRefs={indexPatternRefs}
indexPatterns={indexPatterns}
query={query}
dateRange={dateRange}
filters={filters}
dragDropContext={dragDropContext}
core={core}
data={data}
charts={charts}
onChangeIndexPattern={onChangeIndexPattern}
existingFields={state.existingFields}
existenceFetchFailed={state.existenceFetchFailed}
/>
)}
</>
);
}
interface DataPanelState {
nameFilter: string;
typeFilter: DataType[];
isTypeFilterOpen: boolean;
isAvailableAccordionOpen: boolean;
isEmptyAccordionOpen: boolean;
isMetaAccordionOpen: boolean;
}
const defaultFieldGroups: {
specialFields: IndexPatternField[];
availableFields: IndexPatternField[];
emptyFields: IndexPatternField[];
metaFields: IndexPatternField[];
} = {
specialFields: [],
availableFields: [],
emptyFields: [],
metaFields: [],
};
const fieldFiltersLabel = i18n.translate('xpack.lens.indexPatterns.fieldFiltersLabel', {
defaultMessage: 'Field filters',
});
export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
currentIndexPatternId,
indexPatternRefs,
indexPatterns,
existenceFetchFailed,
query,
dateRange,
filters,
dragDropContext,
onChangeIndexPattern,
core,
data,
existingFields,
charts,
}: Omit<DatasourceDataPanelProps, 'state' | 'setState' | 'showNoDataPopover'> & {
data: DataPublicPluginStart;
currentIndexPatternId: string;
indexPatternRefs: IndexPatternRef[];
indexPatterns: Record<string, IndexPattern>;
dragDropContext: DragContextState;
onChangeIndexPattern: (newId: string) => void;
existingFields: IndexPatternPrivateState['existingFields'];
charts: ChartsPluginSetup;
existenceFetchFailed?: boolean;
}) {
const [localState, setLocalState] = useState<DataPanelState>({
nameFilter: '',
typeFilter: [],
isTypeFilterOpen: false,
isAvailableAccordionOpen: true,
isEmptyAccordionOpen: false,
isMetaAccordionOpen: false,
});
const currentIndexPattern = indexPatterns[currentIndexPatternId];
const allFields = currentIndexPattern.fields;
const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] }));
const hasSyncedExistingFields = existingFields[currentIndexPattern.title];
const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter(
(type) => type in fieldTypeNames
);
const fieldInfoUnavailable = existenceFetchFailed || currentIndexPattern.hasRestrictions;
const unfilteredFieldGroups: FieldGroups = useMemo(() => {
const containsData = (field: IndexPatternField) => {
const overallField = currentIndexPattern.getFieldByName(field.name);
return (
overallField && fieldExists(existingFields, currentIndexPattern.title, overallField.name)
);
};
const allSupportedTypesFields = allFields.filter((field) =>
supportedFieldTypes.has(field.type)
);
const sorted = allSupportedTypesFields.sort(sortFields);
let groupedFields;
// optimization before existingFields are synced
if (!hasSyncedExistingFields) {
groupedFields = {
...defaultFieldGroups,
...groupBy(sorted, (field) => {
if (field.type === 'document') {
return 'specialFields';
} else if (field.meta) {
return 'metaFields';
} else {
return 'emptyFields';
}
}),
};
}
groupedFields = {
...defaultFieldGroups,
...groupBy(sorted, (field) => {
if (field.type === 'document') {
return 'specialFields';
} else if (field.meta) {
return 'metaFields';
} else if (containsData(field)) {
return 'availableFields';
} else return 'emptyFields';
}),
};
const fieldGroupDefinitions: FieldGroups = {
SpecialFields: {
fields: groupedFields.specialFields,
fieldCount: 1,
isAffectedByGlobalFilter: false,
isAffectedByTimeFilter: false,
isInitiallyOpen: false,
showInAccordion: false,
title: '',
hideDetails: true,
},
AvailableFields: {
fields: groupedFields.availableFields,
fieldCount: groupedFields.availableFields.length,
isInitiallyOpen: true,
showInAccordion: true,
title: fieldInfoUnavailable
? i18n.translate('xpack.lens.indexPattern.allFieldsLabel', {
defaultMessage: 'All fields',
})
: i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', {
defaultMessage: 'Available fields',
}),
isAffectedByGlobalFilter: !!filters.length,
isAffectedByTimeFilter: true,
hideDetails: fieldInfoUnavailable,
defaultNoFieldsMessage: i18n.translate('xpack.lens.indexPatterns.noAvailableDataLabel', {
defaultMessage: `There are no available fields that contain data.`,
}),
},
EmptyFields: {
fields: groupedFields.emptyFields,
fieldCount: groupedFields.emptyFields.length,
isAffectedByGlobalFilter: false,
isAffectedByTimeFilter: false,
isInitiallyOpen: false,
showInAccordion: true,
hideDetails: false,
title: i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', {
defaultMessage: 'Empty fields',
}),
defaultNoFieldsMessage: i18n.translate('xpack.lens.indexPatterns.noEmptyDataLabel', {
defaultMessage: `There are no empty fields.`,
}),
},
MetaFields: {
fields: groupedFields.metaFields,
fieldCount: groupedFields.metaFields.length,
isAffectedByGlobalFilter: false,
isAffectedByTimeFilter: false,
isInitiallyOpen: false,
showInAccordion: true,
hideDetails: false,
title: i18n.translate('xpack.lens.indexPattern.metaFieldsLabel', {
defaultMessage: 'Meta fields',
}),
defaultNoFieldsMessage: i18n.translate('xpack.lens.indexPatterns.noMetaDataLabel', {
defaultMessage: `There are no meta fields.`,
}),
},
};
// do not show empty field accordion if there is no existence information
if (fieldInfoUnavailable) {
delete fieldGroupDefinitions.EmptyFields;
}
return fieldGroupDefinitions;
}, [
allFields,
existingFields,
currentIndexPattern,
hasSyncedExistingFields,
fieldInfoUnavailable,
filters.length,
]);
const fieldGroups: FieldGroups = useMemo(() => {
const filterFieldGroup = (fieldGroup: IndexPatternField[]) =>
fieldGroup.filter((field) => {
if (
localState.nameFilter.length &&
!field.name.toLowerCase().includes(localState.nameFilter.toLowerCase()) &&
!field.displayName.toLowerCase().includes(localState.nameFilter.toLowerCase())
) {
return false;
}
if (localState.typeFilter.length > 0) {
return localState.typeFilter.includes(field.type as DataType);
}
return true;
});
return Object.fromEntries(
Object.entries(unfilteredFieldGroups).map(([name, group]) => [
name,
{ ...group, fields: filterFieldGroup(group.fields) },
])
);
}, [unfilteredFieldGroups, localState.nameFilter, localState.typeFilter]);
const fieldProps = useMemo(
() => ({
core,
data,
indexPattern: currentIndexPattern,
highlight: localState.nameFilter.toLowerCase(),
dateRange,
query,
filters,
chartsThemeService: charts.theme,
}),
[
core,
data,
currentIndexPattern,
dateRange,
query,
filters,
localState.nameFilter,
charts.theme,
]
);
return (
<ChildDragDropProvider {...dragDropContext}>
<EuiFlexGroup
gutterSize="none"
className="lnsInnerIndexPatternDataPanel"
direction="column"
responsive={false}
>
<EuiFlexItem grow={false}>
<div className="lnsInnerIndexPatternDataPanel__header">
<ChangeIndexPattern
data-test-subj="indexPattern-switcher"
trigger={{
label: currentIndexPattern.title,
title: currentIndexPattern.title,
'data-test-subj': 'indexPattern-switch-link',
fontWeight: 'bold',
}}
indexPatternId={currentIndexPatternId}
indexPatternRefs={indexPatternRefs}
onChangeIndexPattern={(newId: string) => {
onChangeIndexPattern(newId);
clearLocalState();
}}
/>
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormControlLayout
icon="search"
fullWidth
clear={{
title: i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', {
defaultMessage: 'Clear name and type filters',
}),
'aria-label': i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', {
defaultMessage: 'Clear name and type filters',
}),
onClick: () => {
trackUiEvent('indexpattern_filters_cleared');
clearLocalState();
},
}}
>
<input
className="euiFieldText euiFieldText--fullWidth lnsInnerIndexPatternDataPanel__textField"
data-test-subj="lnsIndexPatternFieldSearch"
placeholder={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', {
defaultMessage: 'Search field names',
description: 'Search the list of fields in the index pattern for the provided text',
})}
value={localState.nameFilter}
onChange={(e) => {
setLocalState({ ...localState, nameFilter: e.target.value });
}}
aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', {
defaultMessage: 'Search fields',
})}
/>
</EuiFormControlLayout>
<EuiSpacer size="xs" />
<EuiFilterGroup>
<EuiPopover
id="dataPanelTypeFilter"
panelClassName="euiFilterGroup__popoverPanel"
panelPaddingSize="none"
anchorPosition="rightUp"
display="block"
isOpen={localState.isTypeFilterOpen}
closePopover={() => setLocalState(() => ({ ...localState, isTypeFilterOpen: false }))}
button={
<EuiFilterButton
iconType="arrowDown"
isSelected={localState.isTypeFilterOpen}
numFilters={localState.typeFilter.length}
hasActiveFilters={!!localState.typeFilter.length}
numActiveFilters={localState.typeFilter.length}
data-test-subj="lnsIndexPatternFiltersToggle"
onClick={() => {
setLocalState((s) => ({
...s,
isTypeFilterOpen: !localState.isTypeFilterOpen,
}));
}}
>
{fieldFiltersLabel}
</EuiFilterButton>
}
>
<EuiContextMenuPanel
watchedItemProps={['icon', 'disabled']}
data-test-subj="lnsIndexPatternTypeFilterOptions"
items={(availableFieldTypes as DataType[]).map((type) => (
<EuiContextMenuItem
className="lnsInnerIndexPatternDataPanel__filterType"
key={type}
icon={localState.typeFilter.includes(type) ? 'check' : 'empty'}
data-test-subj={`typeFilter-${type}`}
onClick={() => {
trackUiEvent('indexpattern_type_filter_toggled');
setLocalState((s) => ({
...s,
typeFilter: localState.typeFilter.includes(type)
? localState.typeFilter.filter((t) => t !== type)
: [...localState.typeFilter, type],
}));
}}
>
<span className="lnsInnerIndexPatternDataPanel__filterTypeInner">
<LensFieldIcon type={type} /> {fieldTypeNames[type]}
</span>
</EuiContextMenuItem>
))}
/>
</EuiPopover>
</EuiFilterGroup>
</EuiFlexItem>
<EuiFlexItem>
<FieldList
exists={(field) =>
field.type === 'document' ||
fieldExists(existingFields, currentIndexPattern.title, field.name)
}
fieldProps={fieldProps}
fieldGroups={fieldGroups}
hasSyncedExistingFields={!!hasSyncedExistingFields}
filter={{
nameFilter: localState.nameFilter,
typeFilter: localState.typeFilter,
}}
currentIndexPatternId={currentIndexPatternId}
existenceFetchFailed={existenceFetchFailed}
existFieldsInIndex={!!allFields.length}
/>
</EuiFlexItem>
</EuiFlexGroup>
</ChildDragDropProvider>
);
};
export const MemoizedDataPanel = memo(InnerIndexPatternDataPanel);