mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
This commit is contained in:
parent
d8a3c53839
commit
14e2de3141
24 changed files with 313 additions and 240 deletions
|
@ -14,6 +14,7 @@ import { IndexPatternPrivateState } from './types';
|
|||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { ChangeIndexPattern } from './change_indexpattern';
|
||||
import { EuiProgress } from '@elastic/eui';
|
||||
import { documentField } from './document_field';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('../../../../../../src/legacy/ui/public/registry/field_formats');
|
||||
|
@ -121,6 +122,7 @@ const initialState: IndexPatternPrivateState = {
|
|||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
documentField,
|
||||
],
|
||||
},
|
||||
'2': {
|
||||
|
@ -174,6 +176,7 @@ const initialState: IndexPatternPrivateState = {
|
|||
},
|
||||
},
|
||||
},
|
||||
documentField,
|
||||
],
|
||||
},
|
||||
'3': {
|
||||
|
@ -199,6 +202,7 @@ const initialState: IndexPatternPrivateState = {
|
|||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
documentField,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -565,6 +569,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
);
|
||||
|
||||
expect(wrapper.find(FieldItem).map(fieldItem => fieldItem.prop('field').name)).toEqual([
|
||||
'Records',
|
||||
'bytes',
|
||||
'client',
|
||||
'memory',
|
||||
|
@ -630,6 +635,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
.simulate('click');
|
||||
|
||||
expect(wrapper.find(FieldItem).map(fieldItem => fieldItem.prop('field').name)).toEqual([
|
||||
'Records',
|
||||
'bytes',
|
||||
'client',
|
||||
'memory',
|
||||
|
@ -698,6 +704,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
const wrapper = shallowWithIntl(<InnerIndexPatternDataPanel {...props} />);
|
||||
|
||||
expect(wrapper.find(FieldItem).map(fieldItem => fieldItem.prop('field').name)).toEqual([
|
||||
'Records',
|
||||
'bytes',
|
||||
'memory',
|
||||
]);
|
||||
|
|
|
@ -18,12 +18,13 @@ import {
|
|||
EuiPopoverTitle,
|
||||
EuiPopoverFooter,
|
||||
EuiCallOut,
|
||||
EuiText,
|
||||
EuiFormControlLayout,
|
||||
EuiSwitch,
|
||||
EuiFacetButton,
|
||||
EuiIcon,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
EuiFormLabel,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -60,10 +61,11 @@ function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) {
|
|||
return fieldA.name.localeCompare(fieldB.name, undefined, { sensitivity: 'base' });
|
||||
}
|
||||
|
||||
const supportedFieldTypes = new Set(['string', 'number', 'boolean', 'date', 'ip']);
|
||||
const supportedFieldTypes = new Set(['string', 'number', 'boolean', 'date', 'ip', 'document']);
|
||||
const PAGINATION_SIZE = 50;
|
||||
|
||||
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' }),
|
||||
|
@ -255,7 +257,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
if (!showEmptyFields) {
|
||||
const indexField = currentIndexPattern && fieldByName[field.name];
|
||||
const exists =
|
||||
indexField && fieldExists(existingFields, currentIndexPattern.title, indexField.name);
|
||||
field.type === 'document' ||
|
||||
(indexField && fieldExists(existingFields, currentIndexPattern.title, indexField.name));
|
||||
if (localState.typeFilter.length > 0) {
|
||||
return exists && localState.typeFilter.includes(field.type as DataType);
|
||||
}
|
||||
|
@ -270,7 +273,12 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
return true;
|
||||
});
|
||||
|
||||
const paginatedFields = displayedFields.sort(sortFields).slice(0, pageSize);
|
||||
const specialFields = displayedFields.filter(f => f.type === 'document');
|
||||
const paginatedFields = displayedFields
|
||||
.filter(f => f.type !== 'document')
|
||||
.sort(sortFields)
|
||||
.slice(0, pageSize);
|
||||
const hilight = localState.nameFilter.toLowerCase();
|
||||
|
||||
return (
|
||||
<ChildDragDropProvider {...dragDropContext}>
|
||||
|
@ -418,6 +426,31 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
onScroll={lazyScroll}
|
||||
>
|
||||
<div className="lnsInnerIndexPatternDataPanel__list">
|
||||
{specialFields.map(field => (
|
||||
<FieldItem
|
||||
core={core}
|
||||
key={field.name}
|
||||
indexPattern={currentIndexPattern}
|
||||
field={field}
|
||||
highlight={hilight}
|
||||
exists={paginatedFields.length > 0}
|
||||
dateRange={dateRange}
|
||||
query={query}
|
||||
filters={filters}
|
||||
hideDetails={true}
|
||||
/>
|
||||
))}
|
||||
{specialFields.length > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.lens.indexPattern.individualFieldsLabel', {
|
||||
defaultMessage: 'Individual fields',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
{paginatedFields.map(field => {
|
||||
const overallField = fieldByName[field.name];
|
||||
return (
|
||||
|
@ -426,7 +459,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
indexPattern={currentIndexPattern}
|
||||
key={field.name}
|
||||
field={field}
|
||||
highlight={localState.nameFilter.toLowerCase()}
|
||||
highlight={hilight}
|
||||
exists={
|
||||
overallField &&
|
||||
fieldExists(existingFields, currentIndexPattern.title, overallField.name)
|
||||
|
@ -439,9 +472,11 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
})}
|
||||
|
||||
{paginatedFields.length === 0 && (
|
||||
<EuiText size="s" color="subdued">
|
||||
<p>
|
||||
{showEmptyFields
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
color="warning"
|
||||
title={
|
||||
showEmptyFields
|
||||
? localState.typeFilter.length || localState.nameFilter.length
|
||||
? i18n.translate('xpack.lens.indexPatterns.noFilteredFieldsLabel', {
|
||||
defaultMessage:
|
||||
|
@ -453,15 +488,17 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
: i18n.translate('xpack.lens.indexPatterns.emptyFieldsWithDataLabel', {
|
||||
defaultMessage:
|
||||
'No fields have data with the current filters and time range. Try changing your filters or time range.',
|
||||
})}
|
||||
</p>
|
||||
|
||||
})
|
||||
}
|
||||
>
|
||||
{(!showEmptyFields ||
|
||||
localState.typeFilter.length ||
|
||||
localState.nameFilter.length) && (
|
||||
<EuiButton
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="primary"
|
||||
flush="left"
|
||||
data-test-subj="lnsDataPanelShowAllFields"
|
||||
size="s"
|
||||
onClick={() => {
|
||||
trackUiEvent('indexpattern_show_all_fields_clicked');
|
||||
clearLocalState();
|
||||
|
@ -471,9 +508,9 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
{i18n.translate('xpack.lens.indexPatterns.showAllFields.buttonText', {
|
||||
defaultMessage: 'Show all fields',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,11 +20,11 @@ import {
|
|||
} from 'src/core/public';
|
||||
import { Storage } from 'ui/storage';
|
||||
import { IndexPatternPrivateState } from '../types';
|
||||
import { documentField } from '../document_field';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('../loader');
|
||||
jest.mock('../state_helpers');
|
||||
jest.mock('../operations');
|
||||
|
||||
// Used by indexpattern plugin, which is a dependency of a dependency
|
||||
jest.mock('ui/chrome');
|
||||
|
@ -67,6 +67,7 @@ const expectedIndexPatterns = {
|
|||
searchable: true,
|
||||
exists: true,
|
||||
},
|
||||
documentField,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -200,7 +201,7 @@ describe('IndexPatternDimensionPanel', () => {
|
|||
|
||||
expect(options).toHaveLength(2);
|
||||
|
||||
expect(options![0].label).toEqual('Document');
|
||||
expect(options![0].label).toEqual('Records');
|
||||
|
||||
expect(options![1].options!.map(({ label }) => label)).toEqual([
|
||||
'timestamp',
|
||||
|
@ -232,7 +233,7 @@ describe('IndexPatternDimensionPanel', () => {
|
|||
expect(options![1].options!.map(({ label }) => label)).toEqual(['timestamp', 'source']);
|
||||
});
|
||||
|
||||
it('should indicate fields which are imcompatible for the operation of the current column', () => {
|
||||
it('should indicate fields which are incompatible for the operation of the current column', () => {
|
||||
wrapper = mount(
|
||||
<IndexPatternDimensionPanel
|
||||
{...defaultProps}
|
||||
|
@ -263,7 +264,7 @@ describe('IndexPatternDimensionPanel', () => {
|
|||
|
||||
const options = wrapper.find(EuiComboBox).prop('options');
|
||||
|
||||
expect(options![0]['data-test-subj']).toEqual('lns-documentOptionIncompatible');
|
||||
expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records');
|
||||
|
||||
expect(
|
||||
options![1].options!.filter(({ label }) => label === 'timestamp')[0]['data-test-subj']
|
||||
|
@ -659,6 +660,7 @@ describe('IndexPatternDimensionPanel', () => {
|
|||
isBucketed: false,
|
||||
label: '',
|
||||
operationType: 'count',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -853,6 +855,7 @@ describe('IndexPatternDimensionPanel', () => {
|
|||
isBucketed: false,
|
||||
label: '',
|
||||
operationType: 'count',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -41,7 +41,6 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & {
|
|||
export interface OperationFieldSupportMatrix {
|
||||
operationByField: Partial<Record<string, OperationType[]>>;
|
||||
fieldByOperation: Partial<Record<OperationType, string[]>>;
|
||||
operationByDocument: OperationType[];
|
||||
}
|
||||
|
||||
export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPanel(
|
||||
|
@ -57,30 +56,25 @@ export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPan
|
|||
|
||||
const supportedOperationsByField: Partial<Record<string, OperationType[]>> = {};
|
||||
const supportedFieldsByOperation: Partial<Record<OperationType, string[]>> = {};
|
||||
const supportedOperationsByDocument: OperationType[] = [];
|
||||
|
||||
filteredOperationsByMetadata.forEach(({ operations }) => {
|
||||
operations.forEach(operation => {
|
||||
if (operation.type === 'field') {
|
||||
if (supportedOperationsByField[operation.field]) {
|
||||
supportedOperationsByField[operation.field]!.push(operation.operationType);
|
||||
} else {
|
||||
supportedOperationsByField[operation.field] = [operation.operationType];
|
||||
}
|
||||
|
||||
if (supportedFieldsByOperation[operation.operationType]) {
|
||||
supportedFieldsByOperation[operation.operationType]!.push(operation.field);
|
||||
} else {
|
||||
supportedFieldsByOperation[operation.operationType] = [operation.field];
|
||||
}
|
||||
if (supportedOperationsByField[operation.field]) {
|
||||
supportedOperationsByField[operation.field]!.push(operation.operationType);
|
||||
} else {
|
||||
supportedOperationsByDocument.push(operation.operationType);
|
||||
supportedOperationsByField[operation.field] = [operation.operationType];
|
||||
}
|
||||
|
||||
if (supportedFieldsByOperation[operation.operationType]) {
|
||||
supportedFieldsByOperation[operation.operationType]!.push(operation.field);
|
||||
} else {
|
||||
supportedFieldsByOperation[operation.operationType] = [operation.field];
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
operationByField: _.mapValues(supportedOperationsByField, _.uniq),
|
||||
fieldByOperation: _.mapValues(supportedFieldsByOperation, _.uniq),
|
||||
operationByDocument: _.uniq(supportedOperationsByDocument),
|
||||
};
|
||||
}, [currentIndexPattern, props.filterOperations]);
|
||||
|
||||
|
|
|
@ -21,9 +21,11 @@ import { IndexPattern, IndexPatternField, IndexPatternPrivateState } from '../ty
|
|||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { fieldExists } from '../pure_helpers';
|
||||
|
||||
export type FieldChoice =
|
||||
| { type: 'field'; field: string; operationType?: OperationType }
|
||||
| { type: 'document' };
|
||||
export interface FieldChoice {
|
||||
type: 'field';
|
||||
field: string;
|
||||
operationType?: OperationType;
|
||||
}
|
||||
|
||||
export interface FieldSelectProps {
|
||||
currentIndexPattern: IndexPattern;
|
||||
|
@ -50,8 +52,7 @@ export function FieldSelect({
|
|||
onDeleteColumn,
|
||||
existingFields,
|
||||
}: FieldSelectProps) {
|
||||
const { operationByDocument, operationByField } = operationFieldSupportMatrix;
|
||||
|
||||
const { operationByField } = operationFieldSupportMatrix;
|
||||
const memoizedFieldOptions = useMemo(() => {
|
||||
const fields = Object.keys(operationByField).sort();
|
||||
|
||||
|
@ -65,68 +66,58 @@ export function FieldSelect({
|
|||
);
|
||||
}
|
||||
|
||||
const isCurrentOperationApplicableWithoutField =
|
||||
(!selectedColumnOperationType && !incompatibleSelectedOperationType) ||
|
||||
operationByDocument.includes(
|
||||
incompatibleSelectedOperationType || selectedColumnOperationType!
|
||||
);
|
||||
const [specialFields, normalFields] = _.partition(
|
||||
fields,
|
||||
field => fieldMap[field].type === 'document'
|
||||
);
|
||||
|
||||
const fieldOptions = [];
|
||||
|
||||
if (operationByDocument.length > 0) {
|
||||
fieldOptions.push({
|
||||
label: i18n.translate('xpack.lens.indexPattern.documentField', {
|
||||
defaultMessage: 'Document',
|
||||
}),
|
||||
value: { type: 'document' },
|
||||
className: classNames({
|
||||
'lnFieldSelect__option--incompatible': !isCurrentOperationApplicableWithoutField,
|
||||
}),
|
||||
'data-test-subj': `lns-documentOption${
|
||||
isCurrentOperationApplicableWithoutField ? '' : 'Incompatible'
|
||||
}`,
|
||||
});
|
||||
function fieldNamesToOptions(items: string[]) {
|
||||
return items
|
||||
.map(field => ({
|
||||
label: field,
|
||||
value: {
|
||||
type: 'field',
|
||||
field,
|
||||
dataType: fieldMap[field].type,
|
||||
operationType:
|
||||
selectedColumnOperationType && isCompatibleWithCurrentOperation(field)
|
||||
? selectedColumnOperationType
|
||||
: undefined,
|
||||
},
|
||||
exists:
|
||||
fieldMap[field].type === 'document' ||
|
||||
fieldExists(existingFields, currentIndexPattern.title, field),
|
||||
compatible: isCompatibleWithCurrentOperation(field),
|
||||
}))
|
||||
.filter(field => showEmptyFields || field.exists)
|
||||
.sort((a, b) => {
|
||||
if (a.compatible && !b.compatible) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.compatible && b.compatible) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map(({ label, value, compatible, exists }) => ({
|
||||
label,
|
||||
value,
|
||||
className: classNames({
|
||||
'lnFieldSelect__option--incompatible': !compatible,
|
||||
'lnFieldSelect__option--nonExistant': !exists,
|
||||
}),
|
||||
'data-test-subj': `lns-fieldOption${compatible ? '' : 'Incompatible'}-${label}`,
|
||||
}));
|
||||
}
|
||||
|
||||
const fieldOptions: unknown[] = fieldNamesToOptions(specialFields);
|
||||
|
||||
if (fields.length > 0) {
|
||||
fieldOptions.push({
|
||||
label: i18n.translate('xpack.lens.indexPattern.individualFieldsLabel', {
|
||||
defaultMessage: 'Individual fields',
|
||||
}),
|
||||
options: fields
|
||||
.map(field => ({
|
||||
label: field,
|
||||
value: {
|
||||
type: 'field',
|
||||
field,
|
||||
dataType: fieldMap[field].type,
|
||||
operationType:
|
||||
selectedColumnOperationType && isCompatibleWithCurrentOperation(field)
|
||||
? selectedColumnOperationType
|
||||
: undefined,
|
||||
},
|
||||
exists: fieldExists(existingFields, currentIndexPattern.title, field),
|
||||
compatible: isCompatibleWithCurrentOperation(field),
|
||||
}))
|
||||
.filter(field => showEmptyFields || field.exists)
|
||||
.sort((a, b) => {
|
||||
if (a.compatible && !b.compatible) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.compatible && b.compatible) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map(({ label, value, compatible, exists }) => ({
|
||||
label,
|
||||
value,
|
||||
className: classNames({
|
||||
'lnFieldSelect__option--incompatible': !compatible,
|
||||
'lnFieldSelect__option--nonExistant': !exists,
|
||||
}),
|
||||
'data-test-subj': `lns-fieldOption${compatible ? '' : 'Incompatible'}-${label}`,
|
||||
})),
|
||||
options: fieldNamesToOptions(normalFields),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -38,10 +38,13 @@ import { trackUiEvent } from '../../lens_ui_telemetry';
|
|||
|
||||
const operationPanels = getOperationDisplay();
|
||||
|
||||
export function asOperationOptions(
|
||||
operationTypes: OperationType[],
|
||||
compatibleWithCurrentField: boolean
|
||||
) {
|
||||
export interface PopoverEditorProps extends IndexPatternDimensionPanelProps {
|
||||
selectedColumn?: IndexPatternColumn;
|
||||
operationFieldSupportMatrix: OperationFieldSupportMatrix;
|
||||
currentIndexPattern: IndexPattern;
|
||||
}
|
||||
|
||||
function asOperationOptions(operationTypes: OperationType[], compatibleWithCurrentField: boolean) {
|
||||
return [...operationTypes]
|
||||
.sort((opType1, opType2) => {
|
||||
return operationPanels[opType1].displayName.localeCompare(
|
||||
|
@ -54,12 +57,6 @@ export function asOperationOptions(
|
|||
}));
|
||||
}
|
||||
|
||||
export interface PopoverEditorProps extends IndexPatternDimensionPanelProps {
|
||||
selectedColumn?: IndexPatternColumn;
|
||||
operationFieldSupportMatrix: OperationFieldSupportMatrix;
|
||||
currentIndexPattern: IndexPattern;
|
||||
}
|
||||
|
||||
export function PopoverEditor(props: PopoverEditorProps) {
|
||||
const {
|
||||
selectedColumn,
|
||||
|
@ -72,7 +69,7 @@ export function PopoverEditor(props: PopoverEditorProps) {
|
|||
uniqueLabel,
|
||||
hideGrouping,
|
||||
} = props;
|
||||
const { operationByDocument, operationByField, fieldByOperation } = operationFieldSupportMatrix;
|
||||
const { operationByField, fieldByOperation } = operationFieldSupportMatrix;
|
||||
const [isPopoverOpen, setPopoverOpen] = useState(false);
|
||||
const [
|
||||
incompatibleSelectedOperationType,
|
||||
|
@ -87,19 +84,12 @@ export function PopoverEditor(props: PopoverEditorProps) {
|
|||
currentIndexPattern.fields.forEach(field => {
|
||||
fields[field.name] = field;
|
||||
});
|
||||
|
||||
return fields;
|
||||
}, [currentIndexPattern]);
|
||||
|
||||
function getOperationTypes() {
|
||||
const possibleOperationTypes = Object.keys(fieldByOperation).concat(
|
||||
operationByDocument
|
||||
) as OperationType[];
|
||||
|
||||
const possibleOperationTypes = Object.keys(fieldByOperation) as OperationType[];
|
||||
const validOperationTypes: OperationType[] = [];
|
||||
if (!selectedColumn || !hasField(selectedColumn)) {
|
||||
validOperationTypes.push(...operationByDocument);
|
||||
}
|
||||
|
||||
if (!selectedColumn) {
|
||||
validOperationTypes.push(...(Object.keys(fieldByOperation) as OperationType[]));
|
||||
|
@ -139,12 +129,8 @@ export function PopoverEditor(props: PopoverEditorProps) {
|
|||
onClick() {
|
||||
if (!selectedColumn) {
|
||||
const possibleFields = fieldByOperation[operationType] || [];
|
||||
const isFieldlessPossible = operationByDocument.includes(operationType);
|
||||
|
||||
if (
|
||||
possibleFields.length === 1 ||
|
||||
(possibleFields.length === 0 && isFieldlessPossible)
|
||||
) {
|
||||
if (possibleFields.length === 1) {
|
||||
setState(
|
||||
changeColumn({
|
||||
state,
|
||||
|
@ -156,8 +142,7 @@ export function PopoverEditor(props: PopoverEditorProps) {
|
|||
layerId: props.layerId,
|
||||
op: operationType,
|
||||
indexPattern: currentIndexPattern,
|
||||
field: possibleFields.length === 1 ? fieldMap[possibleFields[0]] : undefined,
|
||||
asDocumentOperation: possibleFields.length === 0,
|
||||
field: fieldMap[possibleFields[0]],
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
@ -184,7 +169,7 @@ export function PopoverEditor(props: PopoverEditorProps) {
|
|||
layerId: props.layerId,
|
||||
op: operationType,
|
||||
indexPattern: currentIndexPattern,
|
||||
field: hasField(selectedColumn) ? fieldMap[selectedColumn.sourceField] : undefined,
|
||||
field: fieldMap[selectedColumn.sourceField],
|
||||
});
|
||||
trackUiEvent(
|
||||
`indexpattern_dimension_operation_from_${selectedColumn.operationType}_to_${operationType}`
|
||||
|
@ -307,12 +292,11 @@ export function PopoverEditor(props: PopoverEditorProps) {
|
|||
}
|
||||
column = buildColumn({
|
||||
columns: props.state.layers[props.layerId].columns,
|
||||
field: 'field' in choice ? fieldMap[choice.field] : undefined,
|
||||
field: fieldMap[choice.field],
|
||||
indexPattern: currentIndexPattern,
|
||||
layerId: props.layerId,
|
||||
suggestedPriority: props.suggestedPriority,
|
||||
op: operation as OperationType,
|
||||
asDocumentOperation: choice.type === 'document',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
/**
|
||||
* This is a special-case field which allows us to perform
|
||||
* document-level operations such as count.
|
||||
*/
|
||||
export const documentField = {
|
||||
name: i18n.translate('xpack.lens.indexPattern.records', {
|
||||
defaultMessage: 'Records',
|
||||
}),
|
||||
type: 'document',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
};
|
|
@ -56,6 +56,7 @@ export interface FieldItemProps {
|
|||
query: Query;
|
||||
dateRange: DatasourceDataPanelProps['dateRange'];
|
||||
filters: Filter[];
|
||||
hideDetails?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -75,7 +76,17 @@ function wrapOnDot(str?: string) {
|
|||
}
|
||||
|
||||
export function FieldItem(props: FieldItemProps) {
|
||||
const { core, field, indexPattern, highlight, exists, query, dateRange, filters } = props;
|
||||
const {
|
||||
core,
|
||||
field,
|
||||
indexPattern,
|
||||
highlight,
|
||||
exists,
|
||||
query,
|
||||
dateRange,
|
||||
filters,
|
||||
hideDetails,
|
||||
} = props;
|
||||
|
||||
const [infoIsOpen, setOpen] = useState(false);
|
||||
|
||||
|
@ -140,6 +151,10 @@ export function FieldItem(props: FieldItemProps) {
|
|||
}
|
||||
|
||||
function togglePopover() {
|
||||
if (hideDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(!infoIsOpen);
|
||||
if (!infoIsOpen) {
|
||||
trackUiEvent('indexpattern_field_info_click');
|
||||
|
@ -175,7 +190,7 @@ export function FieldItem(props: FieldItemProps) {
|
|||
}
|
||||
}}
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.fieldStatsButtonLabel', {
|
||||
defaultMessage: 'Click for a field preview. Or, drag and drop to visualize.',
|
||||
defaultMessage: 'Click for a field preview, or drag and drop to visualize.',
|
||||
})}
|
||||
>
|
||||
<LensFieldIcon type={field.type as DataType} />
|
||||
|
@ -186,9 +201,15 @@ export function FieldItem(props: FieldItemProps) {
|
|||
|
||||
<EuiIconTip
|
||||
anchorClassName="lnsFieldItem__infoIcon"
|
||||
content={i18n.translate('xpack.lens.indexPattern.fieldStatsButtonLabel', {
|
||||
defaultMessage: 'Click for a field preview. Or, drag and drop to visualize.',
|
||||
})}
|
||||
content={
|
||||
hideDetails
|
||||
? i18n.translate('xpack.lens.indexPattern.fieldItemTooltip', {
|
||||
defaultMessage: 'Drag and drop to visualize.',
|
||||
})
|
||||
: i18n.translate('xpack.lens.indexPattern.fieldStatsButtonLabel', {
|
||||
defaultMessage: 'Click for a field preview, or drag and drop to visualize.',
|
||||
})
|
||||
}
|
||||
type="iInCircle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
|
@ -461,7 +482,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
|
|||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" textAlign="left" color={euiTextColor}>
|
||||
<EuiText size="xs" textAlign="left" color={euiTextColor}>
|
||||
{Math.round((topValue.count / props.sampledValues!) * 100)}%
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -183,6 +183,7 @@ describe('IndexPattern Data Source', () => {
|
|||
isBucketed: false,
|
||||
label: 'Foo',
|
||||
operationType: 'count',
|
||||
sourceField: 'Records',
|
||||
};
|
||||
const map = uniqueLabels({
|
||||
a: {
|
||||
|
@ -243,16 +244,13 @@ describe('IndexPattern Data Source', () => {
|
|||
label: 'Count of records',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
sourceField: 'Records',
|
||||
operationType: 'count',
|
||||
},
|
||||
col2: {
|
||||
label: 'Date',
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
|
||||
// Private
|
||||
operationType: 'date_histogram',
|
||||
sourceField: 'timestamp',
|
||||
params: {
|
||||
|
@ -272,7 +270,7 @@ describe('IndexPattern Data Source', () => {
|
|||
metricsAtAllLevels=false
|
||||
partialRows=false
|
||||
includeFormatHints=true
|
||||
aggConfigs={lens_auto_date aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]'} | lens_rename_columns idMap='{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}'"
|
||||
aggConfigs={lens_auto_date aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]'} | lens_rename_columns idMap='{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}'"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
getDatasourceSuggestionsFromCurrentState,
|
||||
} from './indexpattern_suggestions';
|
||||
|
||||
import { isDraggedField } from './utils';
|
||||
import { isDraggedField, normalizeOperationDataType } from './utils';
|
||||
import { LayerPanel } from './layerpanel';
|
||||
import { IndexPatternColumn } from './operations';
|
||||
import {
|
||||
|
@ -51,7 +51,7 @@ export interface DraggedField {
|
|||
export function columnToOperation(column: IndexPatternColumn, uniqueLabel?: string): Operation {
|
||||
const { dataType, label, isBucketed, scale } = column;
|
||||
return {
|
||||
dataType,
|
||||
dataType: normalizeOperationDataType(dataType),
|
||||
isBucketed,
|
||||
scale,
|
||||
label: uniqueLabel || label,
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
IndexPatternLayer,
|
||||
IndexPatternField,
|
||||
} from './types';
|
||||
import { documentField } from './document_field';
|
||||
|
||||
type IndexPatternSugestion = DatasourceSuggestion<IndexPatternPrivateState>;
|
||||
|
||||
|
@ -285,6 +286,7 @@ function createNewLayerWithBucketAggregation(
|
|||
indexPattern,
|
||||
layerId,
|
||||
suggestedPriority: undefined,
|
||||
field: documentField,
|
||||
});
|
||||
|
||||
const col1 = generateId();
|
||||
|
@ -364,6 +366,9 @@ export function getDatasourceSuggestionsFromCurrentState(
|
|||
columnId =>
|
||||
layer.columns[columnId].isBucketed && layer.columns[columnId].dataType === 'date'
|
||||
);
|
||||
const timeField = indexPattern.fields.find(
|
||||
({ name }) => name === indexPattern.timeFieldName
|
||||
);
|
||||
|
||||
const suggestions: Array<DatasourceSuggestion<IndexPatternPrivateState>> = [];
|
||||
if (metrics.length === 0) {
|
||||
|
@ -376,9 +381,9 @@ export function getDatasourceSuggestionsFromCurrentState(
|
|||
})
|
||||
);
|
||||
} else if (buckets.length === 0) {
|
||||
if (indexPattern.timeFieldName) {
|
||||
if (timeField) {
|
||||
// suggest current metric over time if there is a default time field
|
||||
suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId));
|
||||
suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId, timeField));
|
||||
}
|
||||
suggestions.push(...createAlternativeMetricSuggestions(indexPattern, layerId, state));
|
||||
// also suggest simple current state
|
||||
|
@ -392,10 +397,10 @@ export function getDatasourceSuggestionsFromCurrentState(
|
|||
} else {
|
||||
suggestions.push(...createSimplifiedTableSuggestions(state, layerId));
|
||||
|
||||
if (!timeDimension && indexPattern.timeFieldName) {
|
||||
if (!timeDimension && timeField) {
|
||||
// suggest current configuration over time if there is a default time field
|
||||
// and no time dimension yet
|
||||
suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId));
|
||||
suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId, timeField));
|
||||
}
|
||||
|
||||
if (buckets.length === 2) {
|
||||
|
@ -439,20 +444,33 @@ function createMetricSuggestion(
|
|||
suggestedPriority: 0,
|
||||
})
|
||||
)
|
||||
.filter(op => op.dataType === 'number' && !op.isBucketed);
|
||||
.filter(op => (op.dataType === 'number' || op.dataType === 'document') && !op.isBucketed);
|
||||
|
||||
if (!column) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = generateId();
|
||||
|
||||
return buildSuggestion({
|
||||
layerId,
|
||||
state,
|
||||
changeType: 'initial',
|
||||
updatedLayer: {
|
||||
...layer,
|
||||
columns: { [newId]: column },
|
||||
columns: {
|
||||
[newId]:
|
||||
column.dataType !== 'document'
|
||||
? column
|
||||
: buildColumn({
|
||||
op: 'count',
|
||||
columns: {},
|
||||
indexPattern,
|
||||
layerId,
|
||||
suggestedPriority: undefined,
|
||||
field: documentField,
|
||||
}),
|
||||
},
|
||||
columnOrder: [newId],
|
||||
},
|
||||
});
|
||||
|
@ -462,8 +480,8 @@ function getNestedTitle([outerBucket, innerBucket]: IndexPatternColumn[]) {
|
|||
return i18n.translate('xpack.lens.indexpattern.suggestions.nestingChangeLabel', {
|
||||
defaultMessage: '{innerOperation} for each {outerOperation}',
|
||||
values: {
|
||||
innerOperation: hasField(innerBucket) ? innerBucket.sourceField : innerBucket.label,
|
||||
outerOperation: hasField(outerBucket) ? outerBucket.sourceField : outerBucket.label,
|
||||
innerOperation: innerBucket.sourceField,
|
||||
outerOperation: outerBucket.sourceField,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -515,7 +533,8 @@ function createAlternativeMetricSuggestions(
|
|||
|
||||
function createSuggestionWithDefaultDateHistogram(
|
||||
state: IndexPatternPrivateState,
|
||||
layerId: string
|
||||
layerId: string,
|
||||
timeField: IndexPatternField
|
||||
) {
|
||||
const layer = state.layers[layerId];
|
||||
const indexPattern = state.indexPatterns[layer.indexPatternId];
|
||||
|
@ -526,7 +545,7 @@ function createSuggestionWithDefaultDateHistogram(
|
|||
op: 'date_histogram',
|
||||
indexPattern,
|
||||
columns: layer.columns,
|
||||
field: indexPattern.fields.find(({ name }) => name === indexPattern.timeFieldName),
|
||||
field: timeField,
|
||||
suggestedPriority: undefined,
|
||||
});
|
||||
const updatedLayer = {
|
||||
|
|
|
@ -8,9 +8,10 @@ import React from 'react';
|
|||
import { palettes } from '@elastic/eui';
|
||||
import { FieldIcon, typeToEuiIconMap } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { DataType } from '../types';
|
||||
import { normalizeOperationDataType } from './utils';
|
||||
|
||||
export function getColorForDataType(type: string) {
|
||||
const iconMap = typeToEuiIconMap[type];
|
||||
const iconMap = typeToEuiIconMap[normalizeOperationDataType(type as DataType)];
|
||||
if (iconMap) {
|
||||
return iconMap.color;
|
||||
}
|
||||
|
@ -18,5 +19,12 @@ export function getColorForDataType(type: string) {
|
|||
}
|
||||
|
||||
export function LensFieldIcon({ type }: { type: DataType }) {
|
||||
return <FieldIcon type={type} className="lnsFieldListPanel__fieldIcon" size="m" useColor />;
|
||||
return (
|
||||
<FieldIcon
|
||||
type={normalizeOperationDataType(type)}
|
||||
className="lnsFieldListPanel__fieldIcon"
|
||||
size="m"
|
||||
useColor
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
syncExistingFields,
|
||||
} from './loader';
|
||||
import { IndexPatternPersistedState, IndexPatternPrivateState } from './types';
|
||||
import { documentField } from './document_field';
|
||||
|
||||
// TODO: This should not be necessary
|
||||
jest.mock('ui/new_platform');
|
||||
|
@ -63,6 +64,7 @@ const sampleIndexPatterns = {
|
|||
searchable: true,
|
||||
esTypes: ['keyword'],
|
||||
},
|
||||
documentField,
|
||||
],
|
||||
},
|
||||
b: {
|
||||
|
@ -121,6 +123,7 @@ const sampleIndexPatterns = {
|
|||
},
|
||||
esTypes: ['keyword'],
|
||||
},
|
||||
documentField,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -133,7 +136,7 @@ function indexPatternSavedObject({ id }: { id: keyof typeof sampleIndexPatterns
|
|||
attributes: {
|
||||
title: pattern.title,
|
||||
timeFieldName: pattern.timeFieldName,
|
||||
fields: JSON.stringify(pattern.fields),
|
||||
fields: JSON.stringify(pattern.fields.filter(f => f.type !== 'document')),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
import { updateLayerIndexPattern } from './state_helpers';
|
||||
import { DateRange, ExistingFields } from '../../common/types';
|
||||
import { BASE_API_URL } from '../../common';
|
||||
import { documentField } from './document_field';
|
||||
|
||||
interface SavedIndexPatternAttributes extends SavedObjectAttributes {
|
||||
title: string;
|
||||
|
@ -278,10 +279,12 @@ function fromSavedObject(
|
|||
id,
|
||||
type,
|
||||
title: attributes.title,
|
||||
fields: (JSON.parse(attributes.fields) as IndexPatternField[]).filter(
|
||||
({ type: fieldType, esTypes }) =>
|
||||
fieldType !== 'string' || (esTypes && esTypes.includes('keyword'))
|
||||
),
|
||||
fields: (JSON.parse(attributes.fields) as IndexPatternField[])
|
||||
.filter(
|
||||
({ type: fieldType, esTypes }) =>
|
||||
fieldType !== 'string' || (esTypes && esTypes.includes('keyword'))
|
||||
)
|
||||
.concat(documentField),
|
||||
typeMeta: attributes.typeMeta
|
||||
? (JSON.parse(attributes.typeMeta) as SavedRestrictionsInfo)
|
||||
: undefined,
|
||||
|
|
|
@ -14,6 +14,7 @@ import { Operation, DimensionPriority } from '../../../types';
|
|||
export interface BaseIndexPatternColumn extends Operation {
|
||||
// Private
|
||||
operationType: string;
|
||||
sourceField: string;
|
||||
suggestedPriority?: DimensionPriority;
|
||||
}
|
||||
|
||||
|
@ -35,6 +36,5 @@ export type ParameterlessIndexPatternColumn<
|
|||
> = TBase & { operationType: TOperationType };
|
||||
|
||||
export interface FieldBasedIndexPatternColumn extends BaseIndexPatternColumn {
|
||||
sourceField: string;
|
||||
suggestedPriority?: DimensionPriority;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { OperationDefinition } from '.';
|
||||
import { ParameterlessIndexPatternColumn, BaseIndexPatternColumn } from './column_types';
|
||||
import { IndexPatternField } from '../../types';
|
||||
|
||||
const countLabel = i18n.translate('xpack.lens.indexPattern.countOf', {
|
||||
defaultMessage: 'Count of records',
|
||||
|
@ -23,14 +24,23 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn> = {
|
|||
displayName: i18n.translate('xpack.lens.indexPattern.count', {
|
||||
defaultMessage: 'Count',
|
||||
}),
|
||||
getPossibleOperationForDocument: () => {
|
||||
onFieldChange: (oldColumn, indexPattern, field) => {
|
||||
return {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
...oldColumn,
|
||||
label: field.name,
|
||||
sourceField: field.name,
|
||||
};
|
||||
},
|
||||
buildColumn({ suggestedPriority }) {
|
||||
getPossibleOperationForField: (field: IndexPatternField) => {
|
||||
if (field.type === 'document') {
|
||||
return {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
};
|
||||
}
|
||||
},
|
||||
buildColumn({ suggestedPriority, field }) {
|
||||
return {
|
||||
label: countLabel,
|
||||
dataType: 'number',
|
||||
|
@ -38,6 +48,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn> = {
|
|||
suggestedPriority,
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
sourceField: field.name,
|
||||
};
|
||||
},
|
||||
toEsAggsConfig: (column, columnId) => ({
|
||||
|
|
|
@ -16,7 +16,7 @@ import { minOperation, averageOperation, sumOperation, maxOperation } from './me
|
|||
import { dateHistogramOperation } from './date_histogram';
|
||||
import { countOperation } from './count';
|
||||
import { DimensionPriority, StateSetter, OperationMetadata } from '../../../types';
|
||||
import { BaseIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types';
|
||||
import { BaseIndexPatternColumn } from './column_types';
|
||||
import { IndexPatternPrivateState, IndexPattern, IndexPatternField } from '../../types';
|
||||
import { DateRange } from '../../../../common';
|
||||
|
||||
|
@ -142,26 +142,14 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn>
|
|||
onFieldChange: (oldColumn: C, indexPattern: IndexPattern, field: IndexPatternField) => C;
|
||||
}
|
||||
|
||||
interface DocumentBasedOperationDefinition<C extends BaseIndexPatternColumn>
|
||||
extends BaseOperationDefinitionProps<C> {
|
||||
/**
|
||||
* Returns the meta data of the operation if applied to documents of the given index pattern.
|
||||
* Undefined if the operation is not applicable to the index pattern.
|
||||
*/
|
||||
getPossibleOperationForDocument: (indexPattern: IndexPattern) => OperationMetadata | undefined;
|
||||
buildColumn: (arg: BaseBuildColumnArgs) => C;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of an operation definition. If the type parameter of the definition
|
||||
* indicates a field based column, `getPossibleOperationForField` has to be
|
||||
* specified, otherwise `getPossibleOperationForDocument` has to be defined.
|
||||
*/
|
||||
export type OperationDefinition<
|
||||
C extends BaseIndexPatternColumn
|
||||
> = C extends FieldBasedIndexPatternColumn
|
||||
? FieldBasedOperationDefinition<C>
|
||||
: DocumentBasedOperationDefinition<C>;
|
||||
export type OperationDefinition<C extends BaseIndexPatternColumn> = FieldBasedOperationDefinition<
|
||||
C
|
||||
>;
|
||||
|
||||
// Helper to to infer the column type out of the operation definition.
|
||||
// This is done to avoid it to have to list out the column types along with
|
||||
|
@ -187,9 +175,7 @@ export type OperationType = (typeof internalOperationDefinitions)[number]['type'
|
|||
* This is an operation definition of an unspecified column out of all possible
|
||||
* column types. It
|
||||
*/
|
||||
export type GenericOperationDefinition =
|
||||
| FieldBasedOperationDefinition<IndexPatternColumn>
|
||||
| DocumentBasedOperationDefinition<IndexPatternColumn>;
|
||||
export type GenericOperationDefinition = FieldBasedOperationDefinition<IndexPatternColumn>;
|
||||
|
||||
/**
|
||||
* List of all available operation definitions
|
||||
|
|
|
@ -48,8 +48,6 @@ describe('terms', () => {
|
|||
label: 'Top value of category',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
|
||||
// Private
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
orderBy: { type: 'alphabetical' },
|
||||
|
@ -62,8 +60,7 @@ describe('terms', () => {
|
|||
label: 'Count',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
sourceField: 'Records',
|
||||
operationType: 'count',
|
||||
},
|
||||
},
|
||||
|
@ -214,8 +211,7 @@ describe('terms', () => {
|
|||
label: 'Count',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
sourceField: 'Records',
|
||||
operationType: 'count',
|
||||
},
|
||||
},
|
||||
|
@ -255,8 +251,7 @@ describe('terms', () => {
|
|||
label: 'Count',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
sourceField: 'Records',
|
||||
operationType: 'count',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@ import { getOperationTypesForField, getAvailableOperationsByMetadata, buildColum
|
|||
import { AvgIndexPatternColumn, MinIndexPatternColumn } from './definitions/metrics';
|
||||
import { CountIndexPatternColumn } from './definitions/count';
|
||||
import { IndexPatternPrivateState } from '../types';
|
||||
import { documentField } from '../document_field';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('../loader');
|
||||
|
@ -179,6 +180,7 @@ describe('getOperationTypesForField', () => {
|
|||
columns: state.layers.first.columns,
|
||||
suggestedPriority: 0,
|
||||
op: 'count',
|
||||
field: documentField,
|
||||
});
|
||||
expect(column.operationType).toEqual('count');
|
||||
});
|
||||
|
@ -216,7 +218,7 @@ describe('getOperationTypesForField', () => {
|
|||
indexPattern: expectedIndexPatterns[1],
|
||||
columns: state.layers.first.columns,
|
||||
suggestedPriority: 0,
|
||||
asDocumentOperation: true,
|
||||
field: documentField,
|
||||
}) as CountIndexPatternColumn;
|
||||
expect(column.operationType).toEqual('count');
|
||||
});
|
||||
|
@ -296,10 +298,6 @@ describe('getOperationTypesForField', () => {
|
|||
"operationType": "sum",
|
||||
"type": "field",
|
||||
},
|
||||
Object {
|
||||
"operationType": "count",
|
||||
"type": "document",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
IndexPatternColumn,
|
||||
} from './definitions';
|
||||
import { IndexPattern, IndexPatternField } from '../types';
|
||||
import { documentField } from '../document_field';
|
||||
|
||||
/**
|
||||
* Returns all available operation types as a list at runtime.
|
||||
|
@ -68,9 +69,21 @@ export function getOperationTypesForField(field: IndexPatternField) {
|
|||
.map(({ type }) => type);
|
||||
}
|
||||
|
||||
type OperationFieldTuple =
|
||||
| { type: 'field'; operationType: OperationType; field: string }
|
||||
| { type: 'document'; operationType: OperationType };
|
||||
let documentOperations: Set<string>;
|
||||
|
||||
export function isDocumentOperation(type: string) {
|
||||
// This can't be done at the root level, because it breaks tests, thanks to mocking oddities
|
||||
// so we do it here, and cache the result.
|
||||
documentOperations =
|
||||
documentOperations || new Set(getOperationTypesForField(documentField) as string[]);
|
||||
return documentOperations.has(type);
|
||||
}
|
||||
|
||||
interface OperationFieldTuple {
|
||||
type: 'field';
|
||||
operationType: OperationType;
|
||||
field: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all possible operations (matches between operations and fields of the index
|
||||
|
@ -119,11 +132,6 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) {
|
|||
};
|
||||
|
||||
operationDefinitions.forEach(operationDefinition => {
|
||||
addToMap(
|
||||
{ type: 'document', operationType: operationDefinition.type },
|
||||
getPossibleOperationForDocument(operationDefinition, indexPattern)
|
||||
);
|
||||
|
||||
indexPattern.fields.forEach(field => {
|
||||
addToMap(
|
||||
{
|
||||
|
@ -139,15 +147,6 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) {
|
|||
return Object.values(operationByMetadata);
|
||||
}
|
||||
|
||||
function getPossibleOperationForDocument(
|
||||
operationDefinition: GenericOperationDefinition,
|
||||
indexPattern: IndexPattern
|
||||
): OperationMetadata | undefined {
|
||||
return 'getPossibleOperationForDocument' in operationDefinition
|
||||
? operationDefinition.getPossibleOperationForDocument(indexPattern)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function getPossibleOperationForField(
|
||||
operationDefinition: GenericOperationDefinition,
|
||||
field: IndexPatternField
|
||||
|
@ -203,24 +202,18 @@ export function buildColumn({
|
|||
layerId,
|
||||
indexPattern,
|
||||
suggestedPriority,
|
||||
asDocumentOperation,
|
||||
}: {
|
||||
op?: OperationType;
|
||||
columns: Partial<Record<string, IndexPatternColumn>>;
|
||||
suggestedPriority: DimensionPriority | undefined;
|
||||
layerId: string;
|
||||
indexPattern: IndexPattern;
|
||||
field?: IndexPatternField;
|
||||
asDocumentOperation?: boolean;
|
||||
field: IndexPatternField;
|
||||
}): IndexPatternColumn {
|
||||
let operationDefinition: GenericOperationDefinition | undefined;
|
||||
|
||||
if (op) {
|
||||
operationDefinition = operationDefinitionMap[op];
|
||||
} else if (asDocumentOperation) {
|
||||
operationDefinition = getDefinition(definition =>
|
||||
Boolean(getPossibleOperationForDocument(definition, indexPattern))
|
||||
);
|
||||
} else if (field) {
|
||||
operationDefinition = getDefinition(definition =>
|
||||
Boolean(getPossibleOperationForField(definition, field))
|
||||
|
@ -238,19 +231,14 @@ export function buildColumn({
|
|||
indexPattern,
|
||||
};
|
||||
|
||||
// check for the operation for field getter to determine whether
|
||||
// this is a field based operation type
|
||||
if ('getPossibleOperationForField' in operationDefinition) {
|
||||
if (!field) {
|
||||
throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`);
|
||||
}
|
||||
return operationDefinition.buildColumn({
|
||||
...baseOptions,
|
||||
field,
|
||||
});
|
||||
} else {
|
||||
return operationDefinition.buildColumn(baseOptions);
|
||||
if (!field) {
|
||||
throw new Error(`Invariant error: ${operationDefinition.type} operation requires field`);
|
||||
}
|
||||
|
||||
return operationDefinition.buildColumn({
|
||||
...baseOptions,
|
||||
field,
|
||||
});
|
||||
}
|
||||
|
||||
export { operationDefinitionMap } from './definitions';
|
||||
|
|
|
@ -54,8 +54,7 @@ describe('state_helpers', () => {
|
|||
label: 'Count',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
sourceField: 'Records',
|
||||
operationType: 'count',
|
||||
},
|
||||
},
|
||||
|
@ -102,8 +101,7 @@ describe('state_helpers', () => {
|
|||
label: 'Count',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
sourceField: 'Records',
|
||||
operationType: 'count',
|
||||
},
|
||||
},
|
||||
|
@ -328,8 +326,7 @@ describe('state_helpers', () => {
|
|||
label: 'Count',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
||||
// Private
|
||||
sourceField: 'Records',
|
||||
operationType: 'count',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -10,6 +10,15 @@ import {
|
|||
BaseIndexPatternColumn,
|
||||
FieldBasedIndexPatternColumn,
|
||||
} from './operations/definitions/column_types';
|
||||
import { DataType } from '../types';
|
||||
|
||||
/**
|
||||
* Normalizes the specified operation type. (e.g. document operations
|
||||
* produce 'number')
|
||||
*/
|
||||
export function normalizeOperationDataType(type: DataType) {
|
||||
return type === 'document' ? 'number' : type;
|
||||
}
|
||||
|
||||
export function hasField(column: BaseIndexPatternColumn): column is FieldBasedIndexPatternColumn {
|
||||
return 'sourceField' in column;
|
||||
|
|
|
@ -205,7 +205,7 @@ export interface DatasourceLayerPanelProps {
|
|||
layerId: string;
|
||||
}
|
||||
|
||||
export type DataType = 'string' | 'number' | 'date' | 'boolean' | 'ip';
|
||||
export type DataType = 'document' | 'string' | 'number' | 'date' | 'boolean' | 'ip';
|
||||
|
||||
// An operation represents a column in a table, not any information
|
||||
// about how the column was created such as whether it is a sum or average.
|
||||
|
|
|
@ -19,11 +19,12 @@ import { generateId } from '../id_generator';
|
|||
import { getIconForSeries } from './state_helpers';
|
||||
|
||||
const columnSortOrder = {
|
||||
date: 0,
|
||||
string: 1,
|
||||
ip: 2,
|
||||
boolean: 3,
|
||||
number: 4,
|
||||
document: 0,
|
||||
date: 1,
|
||||
string: 2,
|
||||
ip: 3,
|
||||
boolean: 4,
|
||||
number: 5,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue