mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* provide modal for conflicted fields # Conflicts: # src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx
This commit is contained in:
parent
b400823889
commit
e924e491e2
8 changed files with 297 additions and 51 deletions
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
||||
<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',
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -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": "このフィールドはイベントの発生時刻を表します。",
|
||||
|
|
|
@ -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": "此字段表示事件发生的时间。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue