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
This commit is contained in:
Shailesh Parmar 2023-03-28 12:13:16 +05:30 committed by GitHub
parent 65297e5b1d
commit 4b260f1736
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 382 additions and 138 deletions

View File

@ -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<ParameterFormProps> = ({ 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,
{

View File

@ -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 }) => <div>{children}</div>),
Col: jest
.fn()
.mockImplementation(({ children, ...props }) => (
<div data-testid={props['data-testid']}>{children}</div>
)),
}));
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(<ProfilerSettingsModal {...mockProps} />);
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(<ProfilerSettingsModal {...mockProps} />);
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');
});
});

View File

@ -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<ProfilerSettingsModalProps> = ({
onVisibilityChange,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [form] = Form.useForm<ProfilerForm>();
const [isLoading, setIsLoading] = useState(false);
@ -121,9 +126,14 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
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<ProfilerSettingsModalProps> = ({
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<ProfilerSettingsModalProps> = ({
enablePartition: partitioning.enablePartitioning || false,
});
form.setFieldsValue({ ...partitioning });
form.setFieldsValue({
...partitioning,
});
}
};
@ -259,19 +267,23 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
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<ProfilerSettingsModalProps> = ({
[]
);
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<ProfilerSettingsModalProps> = ({
type: t('label.query'),
})}{' '}
</p>
<CodeMirror
<SchemaEditor
className="profiler-setting-sql-editor"
data-testid="profiler-setting-sql-editor"
options={codeMirrorOption}
value={state?.sqlQuery}
onBeforeChange={handleCodeMirrorChange}
mode={{ name: CSMode.SQL }}
options={{
readOnly: false,
}}
value={state?.sqlQuery || ''}
onChange={handleCodeMirrorChange}
/>
</Col>
@ -469,6 +507,7 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
id="profiler-setting-form"
initialValues={{
includeColumns: state?.includeCol,
partitionData: [''],
...state?.data?.partitioning,
}}
layout="vertical"
@ -546,54 +585,18 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
</>
)}
</List>
<Form.Item className="m-b-xs">
<Space size={12}>
<p>{t('label.enable-partition')}</p>
<Switch
checked={state?.enablePartition}
data-testid="enable-partition-switch"
disabled={isPartitionDisabled}
onChange={handleEnablePartition}
/>
</Space>
</Form.Item>
<Row gutter={[16, 16]}>
<Col span={12}>
<Form.Item
className="m-b-0"
label={
<span className="text-xs">
{t('label.column-entity', {
entity: t('label.name'),
})}
</span>
}
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'),
}),
}),
},
]}>
<Select
allowClear
className="w-full"
data-testid="column-name"
disabled={!state?.enablePartition}
options={partitionColumnOptions}
placeholder={t('message.select-column-name')}
size="middle"
/>
</Form.Item>
<Col span={24}>
<Space align="center" size={12}>
<p>{t('label.enable-partition')}</p>
<Form.Item className="m-b-0" name="enablePartitioning">
<Switch
checked={state?.enablePartition}
data-testid="enable-partition-switch"
onChange={handleEnablePartition}
/>
</Form.Item>
</Space>
</Col>
<Col span={12}>
<Form.Item
@ -601,11 +604,7 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
label={
<span className="text-xs">{t('label.interval-type')}</span>
}
labelCol={{
style: {
paddingBottom: 8,
},
}}
labelCol={PROFILER_MODAL_LABEL_STYLE}
name="partitionIntervalType"
rules={[
{
@ -626,64 +625,234 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
className="m-b-0"
label={<span className="text-xs">{t('label.interval')}</span>}
labelCol={{
style: {
paddingBottom: 8,
},
}}
name="partitionInterval"
rules={[
{
required: state?.enablePartition,
message: t('message.field-text-is-required', {
fieldText: t('label.interval'),
}),
},
]}>
<InputNumber
className="w-full"
data-testid="interval-required"
disabled={!state?.enablePartition}
placeholder={t('message.enter-interval')}
size="middle"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
className="m-b-0"
label={
<span className="text-xs">{t('label.interval-unit')}</span>
<span className="text-xs">
{t('label.column-entity', {
entity: t('label.name'),
})}
</span>
}
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'),
}),
}),
},
]}>
<Select
allowClear
className="w-full"
data-testid="select-interval-unit"
data-testid="column-name"
disabled={!state?.enablePartition}
options={INTERVAL_UNIT_OPTIONS}
placeholder={t('message.select-interval-unit')}
options={partitionColumnOptions}
placeholder={t('message.select-column-name')}
size="middle"
/>
</Form.Item>
</Col>
{partitionIntervalType &&
TIME_BASED_PARTITION.includes(partitionIntervalType) ? (
<>
<Col span={12}>
<Form.Item
className="m-b-0"
label={
<span className="text-xs">{t('label.interval')}</span>
}
labelCol={PROFILER_MODAL_LABEL_STYLE}
name="partitionInterval"
rules={[
{
required: state?.enablePartition,
message: t('message.field-text-is-required', {
fieldText: t('label.interval'),
}),
},
]}>
<InputNumber
className="w-full"
data-testid="interval-required"
disabled={!state?.enablePartition}
placeholder={t('message.enter-interval')}
size="middle"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
className="m-b-0"
label={
<span className="text-xs">
{t('label.interval-unit')}
</span>
}
labelCol={PROFILER_MODAL_LABEL_STYLE}
name="partitionIntervalUnit"
rules={[
{
required: state?.enablePartition,
message: t('message.field-text-is-required', {
fieldText: t('label.interval-unit'),
}),
},
]}>
<Select
allowClear
className="w-full"
data-testid="select-interval-unit"
disabled={!state?.enablePartition}
options={INTERVAL_UNIT_OPTIONS}
placeholder={t('message.select-interval-unit')}
size="middle"
/>
</Form.Item>
</Col>
</>
) : null}
{PartitionIntervalType.IntegerRange === partitionIntervalType ? (
<>
<Col span={12}>
<Form.Item
className="m-b-0"
label={
<span className="text-xs">
{t('label.start-entity', {
entity: t('label.range'),
})}
</span>
}
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'),
}),
}),
},
]}>
<InputNumber
className="w-full"
data-testid="start-range"
placeholder={t('message.enter-a-field', {
field: t('label.start-entity', {
entity: t('label.range'),
}),
})}
size="middle"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
className="m-b-0"
label={
<span className="text-xs">
{t('label.end-entity', {
entity: t('label.range'),
})}
</span>
}
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'),
}),
}),
},
]}>
<InputNumber
className="w-full"
data-testid="end-range"
placeholder={t('message.enter-a-field', {
field: t('label.end-entity', {
entity: t('label.range'),
}),
})}
size="middle"
/>
</Form.Item>
</Col>
</>
) : null}
{PartitionIntervalType.ColumnValue === partitionIntervalType ? (
<Col span={24}>
<List name="partitionValues">
{(fields, { add, remove }) => (
<>
<div className="flex items-center tw-mb-1.5">
<p className="w-form-label text-xs m-r-sm">
{`${t('label.value')}:`}
</p>
<Button
className="include-columns-add-button"
icon={<PlusOutlined />}
size="small"
type="primary"
onClick={() => add()}
/>
</div>
{fields.map(({ key, name, ...restField }) => (
<Row gutter={16} key={key}>
<Col className="flex" span={24}>
<Form.Item
className="w-full m-b-md"
{...restField}
name={name}
rules={[
{
required: state?.enablePartition,
message: t(
'message.field-text-is-required',
{
fieldText: t('label.value'),
}
),
},
]}>
<Input
className="w-full"
data-testid="partition-value"
placeholder={t('message.enter-a-field', {
field: t('label.value'),
})}
/>
</Form.Item>
<Button
icon={
<SVGIcons
alt={t('label.delete')}
className="w-4"
icon={Icons.DELETE}
/>
}
type="text"
onClick={() => remove(name)}
/>
</Col>
</Row>
))}
</>
)}
</List>
</Col>
) : null}
</Row>
</Form>
</Col>

View File

@ -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[];
}

View File

@ -314,13 +314,20 @@ export const STEPS_FOR_ADD_TEST_CASE: Array<StepperStepType> = [
},
];
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,
];

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",