[data views] data view management - provide detail modal for conflicted fields (#118286) (#119507)

* provide modal for conflicted fields

# Conflicts:
#	src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx
This commit is contained in:
Matthew Kime 2021-11-23 12:00:03 -06:00 committed by GitHub
parent b400823889
commit e924e491e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 297 additions and 51 deletions

View file

@ -116,7 +116,7 @@ exports[`IndexedFieldsTable IndexedFieldsTable with rollup index pattern should
"isUserEditable": false,
"kbnType": undefined,
"name": "conflictingField",
"type": "keyword, long",
"type": "conflict",
},
Object {
"displayName": "amount",
@ -274,7 +274,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
"isUserEditable": false,
"kbnType": undefined,
"name": "conflictingField",
"type": "keyword, long",
"type": "conflict",
},
Object {
"displayName": "amount",

View file

@ -1,5 +1,86 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Table render conflict summary modal 1`] = `
<React.Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle>
<h1>
<FormattedMessage
defaultMessage="This field has a type conflict"
id="indexPatternManagement.editIndexPattern.fields.conflictModal.title"
values={Object {}}
/>
</h1>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText>
<p>
<FormattedMessage
defaultMessage="The type of the {fieldName} field changes across indices and might not be available for search, visualizations, and other analysis."
id="indexPatternManagement.editIndexPattern.fields.conflictModal.description"
values={
Object {
"fieldName": <EuiCode>
message
</EuiCode>,
}
}
/>
</p>
<EuiBasicTable
columns={
Array [
Object {
"field": "type",
"name": "Type",
},
Object {
"field": "indices",
"name": "Indices",
},
]
}
items={
Array [
Object {
"indices": "index_a",
"type": "keyword",
},
Object {
"indices": "index_b",
"type": "long",
},
]
}
noItemsMessage={
<EuiI18n
default="No items found"
token="euiBasicTable.noItemsMessage"
/>
}
responsive={true}
rowHeader="firstName"
tableCaption="Demo of EuiBasicTable"
tableLayout="auto"
/>
</EuiText>
</EuiModalBody>
<EuiModalFooter>
<EuiButton
fill={true}
onClick={[Function]}
>
<FormattedMessage
defaultMessage="Close"
id="indexPatternManagement.editIndexPattern.fields.conflictModal.closeBtn"
values={Object {}}
/>
</EuiButton>
</EuiModalFooter>
</React.Fragment>
`;
exports[`Table render name 1`] = `
<span>
customer
@ -26,15 +107,17 @@ exports[`Table render name 2`] = `
exports[`Table should render conflicting type 1`] = `
<span>
conflict
<span>
 
<EuiIconTip
aria-label="Multiple type field"
<EuiBadge
color="warning"
content="The type of this field changes across indices. It is unavailable for many analysis functions."
type="alert"
/>
iconOnClick={[Function]}
iconOnClickAriaLabel="Conflict Detail"
iconType="alert"
onClick={[Function]}
onClickAriaLabel="Conflict Detail"
>
Conflict
</EuiBadge>
</span>
</span>
`;
@ -160,6 +243,14 @@ exports[`Table should render normally 1`] = `
"type": "date",
},
Object {
"conflictDescriptions": Object {
"keyword": Array [
"index_a",
],
"long": Array [
"index_b",
],
},
"displayName": "conflictingField",
"excluded": false,
"hasRuntime": false,

View file

@ -10,7 +10,8 @@ import React from 'react';
import { shallow } from 'enzyme';
import { IndexPattern } from 'src/plugins/data/public';
import { IndexedFieldItem } from '../../types';
import { Table, renderFieldName } from './table';
import { Table, renderFieldName, getConflictModalContent } from './table';
import { overlayServiceMock } from 'src/core/public/mocks';
const indexPattern = {
timeFieldName: 'timestamp',
@ -43,6 +44,7 @@ const items: IndexedFieldItem[] = [
{
name: 'conflictingField',
displayName: 'conflictingField',
conflictDescriptions: { keyword: ['index_a'], long: ['index_b'] },
type: 'text, long',
kbnType: 'conflict',
info: [],
@ -81,7 +83,13 @@ const renderTable = (
}
) =>
shallow(
<Table indexPattern={indexPattern} items={items} editField={editField} deleteField={() => {}} />
<Table
indexPattern={indexPattern}
items={items}
editField={editField}
deleteField={() => {}}
openModal={overlayServiceMock.createStartContract().openModal}
/>
);
describe('Table', () => {
@ -116,7 +124,12 @@ describe('Table', () => {
test('should render conflicting type', () => {
const tableCell = shallow(
renderTable().prop('columns')[1].render('conflict', { kbnType: 'conflict' })
renderTable()
.prop('columns')[1]
.render('conflict', {
kbnType: 'conflict',
conflictDescriptions: { keyword: ['index_a'], long: ['index_b'] },
})
);
expect(tableCell).toMatchSnapshot();
});
@ -163,4 +176,14 @@ describe('Table', () => {
expect(renderFieldName(runtimeField)).toMatchSnapshot();
});
test('render conflict summary modal ', () => {
expect(
getConflictModalContent({
closeFn: () => {},
fieldName: 'message',
conflictDescriptions: { keyword: ['index_a'], long: ['index_b'] },
})
).toMatchSnapshot();
});
});

View file

@ -7,6 +7,7 @@
*/
import React, { PureComponent } from 'react';
import { OverlayModalStart } from 'src/core/public';
import {
EuiIcon,
@ -15,9 +16,19 @@ import {
EuiBasicTableColumn,
EuiBadge,
EuiToolTip,
EuiModalHeader,
EuiModalFooter,
EuiModalBody,
EuiButton,
EuiModalHeaderTitle,
EuiText,
EuiBasicTable,
EuiCode,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { toMountPoint } from '../../../../../../../kibana_react/public';
import { IIndexPattern } from '../../../../../../../data/public';
import { IndexedFieldItem } from '../../types';
@ -28,6 +39,11 @@ const additionalInfoAriaLabel = i18n.translate(
{ defaultMessage: 'Additional field information' }
);
const conflictDetailIconAria = i18n.translate(
'indexPatternManagement.editIndexPattern.fields.table.conflictDetailIconAria',
{ defaultMessage: 'Conflict Detail' }
);
const primaryTimeAriaLabel = i18n.translate(
'indexPatternManagement.editIndexPattern.fields.table.primaryTimeAriaLabel',
{ defaultMessage: 'Primary time field' }
@ -38,21 +54,6 @@ const primaryTimeTooltip = i18n.translate(
{ defaultMessage: 'This field represents the time that events occurred.' }
);
const multiTypeAriaLabel = i18n.translate(
'indexPatternManagement.editIndexPattern.fields.table.multiTypeAria',
{
defaultMessage: 'Multiple type field',
}
);
const multiTypeTooltip = i18n.translate(
'indexPatternManagement.editIndexPattern.fields.table.multiTypeTooltip',
{
defaultMessage:
'The type of this field changes across indices. It is unavailable for many analysis functions.',
}
);
const nameHeader = i18n.translate(
'indexPatternManagement.editIndexPattern.fields.table.nameHeader',
{
@ -167,13 +168,31 @@ const runtimeIconTipText = i18n.translate(
{ defaultMessage: 'This field exists on the data view only.' }
);
const conflictType = i18n.translate(
'indexPatternManagement.editDataView.fields.table.conflictType',
{ defaultMessage: 'Conflict' }
);
interface IndexedFieldProps {
indexPattern: IIndexPattern;
items: IndexedFieldItem[];
editField: (field: IndexedFieldItem) => void;
deleteField: (fieldName: string) => void;
openModal: OverlayModalStart['open'];
}
const getItems = (conflictDescriptions: IndexedFieldItem['conflictDescriptions']) => {
const typesAndIndices: Array<{ type: string; indices: string }> = [];
Object.keys(conflictDescriptions!).forEach((type) => {
// only show first 100 indices just incase the list is CRAZY long
typesAndIndices.push({
type,
indices: conflictDescriptions![type].slice(0, 99).join(', '),
});
});
return typesAndIndices;
};
export const renderFieldName = (field: IndexedFieldItem, timeFieldName?: string) => (
<span>
{field.name}
@ -223,28 +242,119 @@ export const renderFieldName = (field: IndexedFieldItem, timeFieldName?: string)
</span>
);
const conflictColumns = [
{
field: 'type',
name: i18n.translate(
'indexPatternManagement.editIndexPattern.fields.table.conflictModalTypeColumn',
{ defaultMessage: 'Type' }
),
},
{
field: 'indices',
name: i18n.translate(
'indexPatternManagement.editIndexPattern.fields.table.conflictModalIndicesColumn',
{ defaultMessage: 'Indices' }
),
},
];
export const getConflictModalContent = ({
closeFn,
fieldName,
conflictDescriptions,
}: {
closeFn: () => void;
fieldName: string;
conflictDescriptions: IndexedFieldItem['conflictDescriptions'];
}) => (
<>
<EuiModalHeader>
<EuiModalHeaderTitle>
<h1>
<FormattedMessage
id="indexPatternManagement.editIndexPattern.fields.conflictModal.title"
defaultMessage="This field has a type conflict"
/>
</h1>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText>
<p>
<FormattedMessage
id="indexPatternManagement.editIndexPattern.fields.conflictModal.description"
defaultMessage="The type of the {fieldName} field changes across indices and might not be available for search, visualizations, and other analysis."
values={{ fieldName: <EuiCode>{fieldName}</EuiCode> }}
/>
</p>
<EuiBasicTable
tableCaption="Demo of EuiBasicTable"
items={getItems(conflictDescriptions)}
rowHeader="firstName"
columns={conflictColumns}
tableLayout="auto"
/>
</EuiText>
</EuiModalBody>
<EuiModalFooter>
<EuiButton onClick={closeFn} fill>
<FormattedMessage
id="indexPatternManagement.editIndexPattern.fields.conflictModal.closeBtn"
defaultMessage="Close"
/>
</EuiButton>
</EuiModalFooter>
</>
);
const getConflictBtn = (
fieldName: string,
conflictDescriptions: IndexedFieldItem['conflictDescriptions'],
openModal: IndexedFieldProps['openModal']
) => {
const onClick = () => {
const overlayRef = openModal(
toMountPoint(
getConflictModalContent({
closeFn: () => {
overlayRef.close();
},
fieldName,
conflictDescriptions,
})
)
);
};
return (
<span>
<EuiBadge
color="warning"
iconType="alert"
onClick={onClick}
iconOnClick={onClick}
iconOnClickAriaLabel={conflictDetailIconAria}
onClickAriaLabel={conflictDetailIconAria}
>
{conflictType}
</EuiBadge>
</span>
);
};
export class Table extends PureComponent<IndexedFieldProps> {
renderBooleanTemplate(value: string, arialLabel: string) {
return value ? <EuiIcon type="dot" color="secondary" aria-label={arialLabel} /> : <span />;
}
renderFieldType(type: string, isConflict: boolean) {
renderFieldType(type: string, field: IndexedFieldItem) {
return (
<span>
{type}
{isConflict ? (
<span>
&nbsp;
<EuiIconTip
type="alert"
color="warning"
aria-label={multiTypeAriaLabel}
content={multiTypeTooltip}
/>
</span>
) : (
''
)}
{type !== 'conflict' ? type : ''}
{field.conflictDescriptions
? getConflictBtn(field.name, field.conflictDescriptions, this.props.openModal)
: ''}
</span>
);
}
@ -275,7 +385,7 @@ export class Table extends PureComponent<IndexedFieldProps> {
dataType: 'string',
sortable: true,
render: (value: string, field: IndexedFieldItem) => {
return this.renderFieldType(value, field.kbnType === 'conflict');
return this.renderFieldType(value, field);
},
'data-test-subj': 'indexedFieldType',
},

View file

@ -7,7 +7,9 @@
*/
import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import { createSelector } from 'reselect';
import { OverlayStart } from 'src/core/public';
import { IndexPatternField, IndexPattern } from '../../../../../../plugins/data/public';
import { useKibana } from '../../../../../../plugins/kibana_react/public';
import { Table } from './components/table';
@ -26,6 +28,7 @@ interface IndexedFieldsTableProps {
};
fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean;
userEditPermission: boolean;
openModal: OverlayStart['openModal'];
}
interface IndexedFieldsTableState {
@ -65,12 +68,25 @@ class IndexedFields extends Component<IndexedFieldsTableProps, IndexedFieldsTabl
indexPattern.sourceFilters.map((f: Record<string, any>) => f.value);
const fieldWildcardMatch = fieldWildcardMatcher(sourceFilters || []);
const getDisplayEsType = (arr: string[]): string => {
const length = arr.length;
if (length < 1) {
return '';
}
if (length > 1) {
return i18n.translate('indexPatternManagement.editIndexPattern.fields.conflictType', {
defaultMessage: 'conflict',
});
}
return arr[0];
};
return (
(fields &&
fields.map((field) => {
return {
...field.spec,
type: field.esTypes?.join(', ') || '',
type: getDisplayEsType(field.esTypes || []),
kbnType: field.type,
displayName: field.displayName,
format: indexPattern.getFormatterForFieldNoDefault(field.name)?.type?.title || '',
@ -119,6 +135,7 @@ class IndexedFields extends Component<IndexedFieldsTableProps, IndexedFieldsTabl
items={fields}
editField={(field) => this.props.helpers.editField(field.name)}
deleteField={(fieldName) => this.props.helpers.deleteField(fieldName)}
openModal={this.props.openModal}
/>
</div>
);

View file

@ -80,7 +80,7 @@ export function Tabs({
location,
refreshFields,
}: TabsProps) {
const { application, uiSettings, docLinks, indexPatternFieldEditor } =
const { application, uiSettings, docLinks, indexPatternFieldEditor, overlays } =
useKibana<IndexPatternManagmentContext>().services;
const [fieldFilter, setFieldFilter] = useState<string>('');
const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState<string>('');
@ -93,6 +93,13 @@ export function Tabs({
const closeEditorHandler = useRef<() => void | undefined>();
const { DeleteRuntimeFieldProvider } = indexPatternFieldEditor;
const conflict = i18n.translate(
'indexPatternManagement.editIndexPattern.fieldTypes.conflictType',
{
defaultMessage: 'conflict',
}
);
const refreshFilters = useCallback(() => {
const tempIndexedFieldTypes: string[] = [];
const tempScriptedFieldLanguages: string[] = [];
@ -103,7 +110,7 @@ export function Tabs({
}
} else {
if (field.esTypes) {
tempIndexedFieldTypes.push(field.esTypes?.join(', '));
tempIndexedFieldTypes.push(field.esTypes.length === 1 ? field.esTypes[0] : conflict);
}
}
});
@ -112,7 +119,7 @@ export function Tabs({
setScriptedFieldLanguages(
convertToEuiSelectOption(tempScriptedFieldLanguages, 'scriptedFieldLanguages')
);
}, [indexPattern]);
}, [indexPattern, conflict]);
const closeFieldEditor = useCallback(() => {
if (closeEditorHandler.current) {
@ -230,6 +237,7 @@ export function Tabs({
deleteField,
getFieldInfo,
}}
openModal={overlays.openModal}
/>
)}
</DeleteRuntimeFieldProvider>
@ -288,6 +296,7 @@ export function Tabs({
openFieldEditor,
DeleteRuntimeFieldProvider,
refreshFields,
overlays,
]
);

View file

@ -3268,8 +3268,6 @@
"indexPatternManagement.editIndexPattern.fields.table.isAggregatableAria": "は集約可能です",
"indexPatternManagement.editIndexPattern.fields.table.isExcludedAria": "は除外されています",
"indexPatternManagement.editIndexPattern.fields.table.isSearchableAria": "は検索可能です",
"indexPatternManagement.editIndexPattern.fields.table.multiTypeAria": "複数タイプのフィールド",
"indexPatternManagement.editIndexPattern.fields.table.multiTypeTooltip": "このフィールドのタイプはインデックスごとに変わります。多くの分析機能には使用できません。",
"indexPatternManagement.editIndexPattern.fields.table.nameHeader": "名前",
"indexPatternManagement.editIndexPattern.fields.table.primaryTimeAriaLabel": "プライマリ時間フィールド",
"indexPatternManagement.editIndexPattern.fields.table.primaryTimeTooltip": "このフィールドはイベントの発生時刻を表します。",

View file

@ -3291,8 +3291,6 @@
"indexPatternManagement.editIndexPattern.fields.table.isAggregatableAria": "可聚合",
"indexPatternManagement.editIndexPattern.fields.table.isExcludedAria": "已排除",
"indexPatternManagement.editIndexPattern.fields.table.isSearchableAria": "可搜索",
"indexPatternManagement.editIndexPattern.fields.table.multiTypeAria": "多类型字段",
"indexPatternManagement.editIndexPattern.fields.table.multiTypeTooltip": "此字段的类型在不同的索引中会有所不同。其不可用于许多分析功能。",
"indexPatternManagement.editIndexPattern.fields.table.nameHeader": "名称",
"indexPatternManagement.editIndexPattern.fields.table.primaryTimeAriaLabel": "主要时间字段",
"indexPatternManagement.editIndexPattern.fields.table.primaryTimeTooltip": "此字段表示事件发生的时间。",