From 4b260f173668d6bb3e44b72e5fd2670b96594268 Mon Sep 17 00:00:00 2001
From: Shailesh Parmar
Date: Tue, 28 Mar 2023 12:13:16 +0530
Subject: [PATCH] fixes 10357: Update Partitionning Setting Flow for Profiler
(#10743)
* initial commit for #10357
* added form based on partition condition
* localization sync
* added unit test
* updated sql editor with common component
* updated form based on switch
* addressing comment
* added form type and provided to form instance
* added default value for partitionValues in initialValue field
---
.../components/ParameterForm.tsx | 6 +-
.../Component/ProfilerSettingsModal.test.tsx | 57 ++-
.../Component/ProfilerSettingsModal.tsx | 419 ++++++++++++------
.../TableProfiler/TableProfiler.interface.ts | 10 +
.../ui/src/constants/profiler.constant.ts | 20 +-
.../ui/src/locale/languages/en-us.json | 2 +
.../ui/src/locale/languages/fr-fr.json | 2 +
.../ui/src/locale/languages/ja-jp.json | 2 +
.../ui/src/locale/languages/zh-cn.json | 2 +
9 files changed, 382 insertions(+), 138 deletions(-)
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AddDataQualityTest/components/ParameterForm.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AddDataQualityTest/components/ParameterForm.tsx
index a4d00dd922b..045ddb15856 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/AddDataQualityTest/components/ParameterForm.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/AddDataQualityTest/components/ParameterForm.tsx
@@ -14,7 +14,7 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, InputNumber, Select, Switch } from 'antd';
import 'codemirror/addon/fold/foldgutter.css';
-import { SUPPORTED_PARTITION_TYPE } from 'constants/profiler.constant';
+import { SUPPORTED_PARTITION_TYPE_FOR_DATE_TIME } from 'constants/profiler.constant';
import { isUndefined } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -46,7 +46,9 @@ const ParameterForm: React.FC = ({ definition, table }) => {
) {
const partitionColumnOptions = table.columns.reduce(
(result, column) => {
- if (SUPPORTED_PARTITION_TYPE.includes(column.dataType)) {
+ if (
+ SUPPORTED_PARTITION_TYPE_FOR_DATE_TIME.includes(column.dataType)
+ ) {
return [
...result,
{
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ProfilerSettingsModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ProfilerSettingsModal.test.tsx
index 1adb8c96491..025436cc54e 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ProfilerSettingsModal.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ProfilerSettingsModal.test.tsx
@@ -11,21 +11,18 @@
* limitations under the License.
*/
-import { cleanup, render, screen } from '@testing-library/react';
+import {
+ act,
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+} from '@testing-library/react';
import React from 'react';
import { MOCK_TABLE } from '../../../mocks/TableData.mock';
import { ProfilerSettingsModalProps } from '../TableProfiler.interface';
import ProfilerSettingsModal from './ProfilerSettingsModal';
-jest.mock('antd/lib/grid', () => ({
- Row: jest.fn().mockImplementation(({ children }) => {children}
),
- Col: jest
- .fn()
- .mockImplementation(({ children, ...props }) => (
- {children}
- )),
-}));
-
jest.mock('rest/tableAPI', () => ({
getTableProfilerConfig: jest
.fn()
@@ -55,11 +52,51 @@ describe('Test ProfilerSettingsModal component', () => {
const sqlEditor = await screen.findByTestId('sql-editor-container');
const includeSelect = await screen.findByTestId('include-column-container');
const excludeSelect = await screen.findByTestId('exclude-column-container');
+ const partitionSwitch = await screen.findByTestId(
+ 'enable-partition-switch'
+ );
+ const intervalType = await screen.findByTestId('interval-type');
+ const columnName = await screen.findByTestId('column-name');
expect(modal).toBeInTheDocument();
expect(sampleContainer).toBeInTheDocument();
expect(sqlEditor).toBeInTheDocument();
expect(includeSelect).toBeInTheDocument();
expect(excludeSelect).toBeInTheDocument();
+ expect(partitionSwitch).toBeInTheDocument();
+ expect(intervalType).toBeInTheDocument();
+ expect(columnName).toBeInTheDocument();
+ });
+
+ it('Interval Type and Column Name field should be disabled, when partition switch is off', async () => {
+ render();
+ const partitionSwitch = await screen.findByTestId(
+ 'enable-partition-switch'
+ );
+ const intervalType = await screen.findByTestId('interval-type');
+ const columnName = await screen.findByTestId('column-name');
+
+ expect(partitionSwitch).toHaveAttribute('aria-checked', 'false');
+ expect(intervalType).toHaveClass('ant-select-disabled');
+ expect(columnName).toHaveClass('ant-select-disabled');
+ });
+
+ it('Interval Type and Column Name field should be enabled, when partition switch is on', async () => {
+ render();
+ const partitionSwitch = await screen.findByTestId(
+ 'enable-partition-switch'
+ );
+ const intervalType = await screen.findByTestId('interval-type');
+ const columnName = await screen.findByTestId('column-name');
+
+ expect(partitionSwitch).toHaveAttribute('aria-checked', 'false');
+
+ await act(async () => {
+ fireEvent.click(partitionSwitch);
+ });
+
+ expect(partitionSwitch).toHaveAttribute('aria-checked', 'true');
+ expect(intervalType).not.toHaveClass('ant-select-disabled');
+ expect(columnName).not.toHaveClass('ant-select-disabled');
});
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ProfilerSettingsModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ProfilerSettingsModal.tsx
index 3d0e7dbfdd9..de809abf7ff 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ProfilerSettingsModal.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/Component/ProfilerSettingsModal.tsx
@@ -14,6 +14,7 @@
import { PlusOutlined } from '@ant-design/icons';
import {
Button,
+ Input,
InputNumber,
Modal,
Select,
@@ -27,7 +28,10 @@ import { Col, Row } from 'antd/lib/grid';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import 'codemirror/addon/fold/foldgutter.css';
-import { isEmpty, isEqual, isUndefined, omit, startCase } from 'lodash';
+import SchemaEditor from 'components/schema-editor/SchemaEditor';
+import { CSMode } from 'enums/codemirror.enum';
+import { PartitionIntervalType } from 'generated/api/data/createTable';
+import { isEmpty, isEqual, isNil, isUndefined, pick, startCase } from 'lodash';
import React, {
Reducer,
useCallback,
@@ -36,17 +40,17 @@ import React, {
useReducer,
useState,
} from 'react';
-import { Controlled as CodeMirror } from 'react-codemirror2';
import { useTranslation } from 'react-i18next';
import { getTableProfilerConfig, putTableProfileConfig } from 'rest/tableAPI';
import {
- codeMirrorOption,
DEFAULT_INCLUDE_PROFILE,
INTERVAL_TYPE_OPTIONS,
INTERVAL_UNIT_OPTIONS,
PROFILER_METRIC,
+ PROFILER_MODAL_LABEL_STYLE,
PROFILE_SAMPLE_OPTIONS,
- SUPPORTED_PARTITION_TYPE,
+ SUPPORTED_COLUMN_DATA_TYPE_FOR_INTERVAL,
+ TIME_BASED_PARTITION,
} from '../../../constants/profiler.constant';
import {
ProfileSampleType,
@@ -58,6 +62,7 @@ import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
import SliderWithInput from '../../SliderWithInput/SliderWithInput';
import {
+ ProfilerForm,
ProfilerSettingModalState,
ProfilerSettingsModalProps,
} from '../TableProfiler.interface';
@@ -70,7 +75,7 @@ const ProfilerSettingsModal: React.FC = ({
onVisibilityChange,
}) => {
const { t } = useTranslation();
- const [form] = Form.useForm();
+ const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
@@ -121,9 +126,14 @@ const ProfilerSettingsModal: React.FC = ({
return metricsOptions;
}, [columns]);
- const { partitionColumnOptions, isPartitionDisabled } = useMemo(() => {
+ const partitionIntervalType = Form.useWatch(['partitionIntervalType'], form);
+
+ const partitionColumnOptions = useMemo(() => {
const partitionColumnOptions = columns.reduce((result, column) => {
- if (SUPPORTED_PARTITION_TYPE.includes(column.dataType)) {
+ const filter = partitionIntervalType
+ ? SUPPORTED_COLUMN_DATA_TYPE_FOR_INTERVAL[partitionIntervalType]
+ : [];
+ if (filter.includes(column.dataType)) {
return [
...result,
{
@@ -135,13 +145,9 @@ const ProfilerSettingsModal: React.FC = ({
return result;
}, [] as { value: string; label: string }[]);
- const isPartitionDisabled = partitionColumnOptions.length === 0;
- return {
- partitionColumnOptions,
- isPartitionDisabled,
- };
- }, [columns]);
+ return partitionColumnOptions;
+ }, [columns, partitionIntervalType]);
const updateInitialConfig = (tableProfilerConfig: TableProfilerConfig) => {
const {
@@ -193,7 +199,9 @@ const ProfilerSettingsModal: React.FC = ({
enablePartition: partitioning.enablePartitioning || false,
});
- form.setFieldsValue({ ...partitioning });
+ form.setFieldsValue({
+ ...partitioning,
+ });
}
};
@@ -259,19 +267,23 @@ const ProfilerSettingsModal: React.FC = ({
const profileConfig: TableProfilerConfig = {
excludeColumns: excludeCol.length > 0 ? excludeCol : undefined,
profileQuery: !isEmpty(sqlQuery) ? sqlQuery : undefined,
- ...{
- profileSample:
- profileSampleType === ProfileSampleType.Percentage
- ? profileSamplePercentage
- : profileSampleRows,
- profileSampleType: profileSampleType,
- },
+ profileSample:
+ profileSampleType === ProfileSampleType.Percentage
+ ? profileSamplePercentage
+ : profileSampleRows,
+ profileSampleType: profileSampleType,
includeColumns: !isEqual(includeCol, DEFAULT_INCLUDE_PROFILE)
? getIncludesColumns()
: undefined,
partitioning: enablePartition
? {
...partitionData,
+ partitionValues:
+ partitionIntervalType === PartitionIntervalType.ColumnValue
+ ? partitionData?.partitionValues?.filter(
+ (value) => !isEmpty(value)
+ )
+ : undefined,
enablePartitioning: enablePartition,
}
: undefined,
@@ -320,19 +332,42 @@ const ProfilerSettingsModal: React.FC = ({
[]
);
- const handleCodeMirrorChange = useCallback(
- (_Editor, _EditorChange, value) => {
- handleStateChange({
- sqlQuery: value,
- });
- },
- []
- );
+ const handleCodeMirrorChange = useCallback((value) => {
+ handleStateChange({
+ sqlQuery: value,
+ });
+ }, []);
+
+ const handleIncludeColumnsProfiler = useCallback((changedValues, data) => {
+ const { partitionIntervalType, enablePartitioning } = changedValues;
+ if (partitionIntervalType || !isNil(enablePartitioning)) {
+ form.setFieldsValue({
+ partitionColumnName: undefined,
+ partitionIntegerRangeStart: undefined,
+ partitionIntegerRangeEnd: undefined,
+ partitionIntervalUnit: undefined,
+ partitionInterval: undefined,
+ partitionValues: [''],
+ });
+ }
+ if (!isNil(enablePartitioning)) {
+ form.setFieldsValue({
+ partitionIntervalType: undefined,
+ });
+ }
- const handleIncludeColumnsProfiler = useCallback((_, data) => {
handleStateChange({
includeCol: data.includeColumns,
- partitionData: omit(data, 'includeColumns'),
+ partitionData: pick(
+ data,
+ 'partitionColumnName',
+ 'partitionIntegerRangeEnd',
+ 'partitionIntegerRangeStart',
+ 'partitionInterval',
+ 'partitionIntervalType',
+ 'partitionIntervalUnit',
+ 'partitionValues'
+ ),
});
}, []);
@@ -437,12 +472,15 @@ const ProfilerSettingsModal: React.FC = ({
type: t('label.query'),
})}{' '}
-
@@ -469,6 +507,7 @@ const ProfilerSettingsModal: React.FC = ({
id="profiler-setting-form"
initialValues={{
includeColumns: state?.includeCol,
+ partitionData: [''],
...state?.data?.partitioning,
}}
layout="vertical"
@@ -546,54 +585,18 @@ const ProfilerSettingsModal: React.FC = ({
>
)}
-
-
- {t('label.enable-partition')}
-
-
-
-
-
- {t('label.column-entity', {
- entity: t('label.name'),
- })}
-
- }
- labelCol={{
- style: {
- paddingBottom: 8,
- },
- }}
- name="partitionColumnName"
- rules={[
- {
- required: state?.enablePartition,
- message: t('message.field-text-is-required', {
- fieldText: t('label.column-entity', {
- entity: t('label.name'),
- }),
- }),
- },
- ]}>
-
-
+
+
+ {t('label.enable-partition')}
+
+
+
+
= ({
label={
{t('label.interval-type')}
}
- labelCol={{
- style: {
- paddingBottom: 8,
- },
- }}
+ labelCol={PROFILER_MODAL_LABEL_STYLE}
name="partitionIntervalType"
rules={[
{
@@ -626,64 +625,234 @@ const ProfilerSettingsModal: React.FC = ({
/>
-
- {t('label.interval')}}
- labelCol={{
- style: {
- paddingBottom: 8,
- },
- }}
- name="partitionInterval"
- rules={[
- {
- required: state?.enablePartition,
- message: t('message.field-text-is-required', {
- fieldText: t('label.interval'),
- }),
- },
- ]}>
-
-
-
{t('label.interval-unit')}
+
+ {t('label.column-entity', {
+ entity: t('label.name'),
+ })}
+
}
- labelCol={{
- style: {
- paddingBottom: 8,
- },
- }}
- name="partitionIntervalUnit"
+ labelCol={PROFILER_MODAL_LABEL_STYLE}
+ name="partitionColumnName"
rules={[
{
required: state?.enablePartition,
message: t('message.field-text-is-required', {
- fieldText: t('label.interval-unit'),
+ fieldText: t('label.column-entity', {
+ entity: t('label.name'),
+ }),
}),
},
]}>
+ {partitionIntervalType &&
+ TIME_BASED_PARTITION.includes(partitionIntervalType) ? (
+ <>
+
+ {t('label.interval')}
+ }
+ labelCol={PROFILER_MODAL_LABEL_STYLE}
+ name="partitionInterval"
+ rules={[
+ {
+ required: state?.enablePartition,
+ message: t('message.field-text-is-required', {
+ fieldText: t('label.interval'),
+ }),
+ },
+ ]}>
+
+
+
+
+
+ {t('label.interval-unit')}
+
+ }
+ labelCol={PROFILER_MODAL_LABEL_STYLE}
+ name="partitionIntervalUnit"
+ rules={[
+ {
+ required: state?.enablePartition,
+ message: t('message.field-text-is-required', {
+ fieldText: t('label.interval-unit'),
+ }),
+ },
+ ]}>
+
+
+
+ >
+ ) : null}
+ {PartitionIntervalType.IntegerRange === partitionIntervalType ? (
+ <>
+
+
+ {t('label.start-entity', {
+ entity: t('label.range'),
+ })}
+
+ }
+ labelCol={PROFILER_MODAL_LABEL_STYLE}
+ name="partitionIntegerRangeStart"
+ rules={[
+ {
+ required: state?.enablePartition,
+ message: t('message.field-text-is-required', {
+ fieldText: t('label.start-entity', {
+ entity: t('label.range'),
+ }),
+ }),
+ },
+ ]}>
+
+
+
+
+
+ {t('label.end-entity', {
+ entity: t('label.range'),
+ })}
+
+ }
+ labelCol={PROFILER_MODAL_LABEL_STYLE}
+ name="partitionIntegerRangeEnd"
+ rules={[
+ {
+ required: state?.enablePartition,
+ message: t('message.field-text-is-required', {
+ fieldText: t('label.end-entity', {
+ entity: t('label.range'),
+ }),
+ }),
+ },
+ ]}>
+
+
+
+ >
+ ) : null}
+
+ {PartitionIntervalType.ColumnValue === partitionIntervalType ? (
+
+
+ {(fields, { add, remove }) => (
+ <>
+
+
+ {`${t('label.value')}:`}
+
+
}
+ size="small"
+ type="primary"
+ onClick={() => add()}
+ />
+
+
+ {fields.map(({ key, name, ...restField }) => (
+
+
+
+
+
+
+ }
+ type="text"
+ onClick={() => remove(name)}
+ />
+
+
+ ))}
+ >
+ )}
+
+
+ ) : null}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/TableProfiler.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/TableProfiler.interface.ts
index c57e966ae12..6dfd0064f64 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/TableProfiler.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/TableProfiler/TableProfiler.interface.ts
@@ -95,3 +95,13 @@ export interface ProfilerSettingModalState {
partitionData: PartitionProfilerConfig | undefined;
selectedProfileSampleType: ProfileSampleType | undefined;
}
+
+export interface ProfilerForm extends PartitionProfilerConfig {
+ profileSample: number | undefined;
+ selectedProfileSampleType: ProfileSampleType | undefined;
+ enablePartition: boolean;
+ profileSampleType: ProfileSampleType | undefined;
+ profileSamplePercentage: number;
+ profileSampleRows: number | undefined;
+ includeColumns: ColumnProfilerConfig[];
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/profiler.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/profiler.constant.ts
index 86e26dfff85..56da06d226b 100644
--- a/openmetadata-ui/src/main/resources/ui/src/constants/profiler.constant.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/constants/profiler.constant.ts
@@ -314,13 +314,20 @@ export const STEPS_FOR_ADD_TEST_CASE: Array = [
},
];
-export const SUPPORTED_PARTITION_TYPE = [
+export const SUPPORTED_PARTITION_TYPE_FOR_DATE_TIME = [
DataType.Timestamp,
DataType.Date,
DataType.Datetime,
DataType.Timestampz,
];
+export const SUPPORTED_COLUMN_DATA_TYPE_FOR_INTERVAL = {
+ [PartitionIntervalType.IngestionTime]: SUPPORTED_PARTITION_TYPE_FOR_DATE_TIME,
+ [PartitionIntervalType.TimeUnit]: SUPPORTED_PARTITION_TYPE_FOR_DATE_TIME,
+ [PartitionIntervalType.IntegerRange]: [DataType.Int, DataType.Bigint],
+ [PartitionIntervalType.ColumnValue]: [DataType.Varchar],
+};
+
export const INTERVAL_TYPE_OPTIONS = Object.values(PartitionIntervalType).map(
(value) => ({
value,
@@ -353,3 +360,14 @@ export const DEFAULT_HISTOGRAM_DATA = {
boundaries: [],
frequencies: [],
};
+
+export const PROFILER_MODAL_LABEL_STYLE = {
+ style: {
+ paddingBottom: 8,
+ },
+};
+
+export const TIME_BASED_PARTITION = [
+ PartitionIntervalType.IngestionTime,
+ PartitionIntervalType.TimeUnit,
+];
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json
index 685876fc2b0..40e96bae3a4 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json
@@ -256,6 +256,7 @@
"enable-partition": "Enable Partition",
"end-date": "End Date",
"end-date-time-zone": "End Date: ({{timeZone}})",
+ "end-entity": "End {{entity}}",
"endpoint": "Endpoint",
"endpoint-url": "Endpoint URL",
"endpoint-url-for-aws": "EndPoint URL for the AWS",
@@ -589,6 +590,7 @@
"query-log-duration": "Query Log Duration",
"query-lowercase": "query",
"query-plural": "Queries",
+ "range": "Range",
"re-deploy": "Re Deploy",
"re-enter-new-password": "Re-enter New Password",
"re-index-all": "Re-Index All",
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json
index 7ac40016c0d..05277e16d31 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json
@@ -256,6 +256,7 @@
"enable-partition": "Activer Partition",
"end-date": "Date de Fin",
"end-date-time-zone": "End Date: ({{timeZone}})",
+ "end-entity": "End {{entity}}",
"endpoint": "Point de Terminaison",
"endpoint-url": "Point de terminaison $t(label.url-uppercase)",
"endpoint-url-for-aws": "EndPoint URL for the AWS",
@@ -589,6 +590,7 @@
"query-log-duration": "Query Log Duration",
"query-lowercase": "query",
"query-plural": "Requêtes",
+ "range": "Range",
"re-deploy": "Re-Déployer",
"re-enter-new-password": "Re-enter New Password",
"re-index-all": "Re Index All",
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json
index 2dae5a9fdb9..6e16f4bb2df 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json
@@ -256,6 +256,7 @@
"enable-partition": "パーティションを有効化",
"end-date": "終了日時",
"end-date-time-zone": "終了日時: ({{timeZone}})",
+ "end-entity": "End {{entity}}",
"endpoint": "エンドポイント",
"endpoint-url": "エンドポイントURL",
"endpoint-url-for-aws": "AWSのエンドポイントURL",
@@ -589,6 +590,7 @@
"query-log-duration": "クエリログの時間",
"query-lowercase": "クエリ",
"query-plural": "クエリ",
+ "range": "Range",
"re-deploy": "再デプロイ",
"re-enter-new-password": "新しいパスワードを再度入力してください",
"re-index-all": "Re-Index All",
diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json
index 79e2ffb65e1..72796b6d8c8 100644
--- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json
+++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json
@@ -256,6 +256,7 @@
"enable-partition": "Enable Partition",
"end-date": "结束日期",
"end-date-time-zone": "结束日期: ({{timeZone}})",
+ "end-entity": "End {{entity}}",
"endpoint": "终点",
"endpoint-url": "终点 URL",
"endpoint-url-for-aws": "EndPoint URL for the AWS",
@@ -589,6 +590,7 @@
"query-log-duration": "Query Log Duration",
"query-lowercase": "查询",
"query-plural": "查询",
+ "range": "Range",
"re-deploy": "Re Deploy",
"re-enter-new-password": "Re-enter New Password",
"re-index-all": "Re Index All",