fix: #19621 Introduce "clear sample" in entity config to have an explicit null (#19859)

* fix: #19621 Introduce "clear sample" in entity config to have an explicit `null`

* added playwright test

* added clear button

* fixed playwright failure

* addressing review comment
This commit is contained in:
Shailesh Parmar 2025-03-12 09:29:10 +05:30 committed by GitHub
parent 1a18c7d7f8
commit a5eb90f797
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 217 additions and 102 deletions

View File

@ -505,78 +505,137 @@ test(
await page.reload(); await page.reload();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
await page.click('[data-testid="profiler-setting-btn"]'); await test.step('Update profiler setting', async () => {
await page.waitForSelector('.ant-modal-body'); await page.click('[data-testid="profiler-setting-btn"]');
await page.locator('[data-testid="slider-input"]').clear(); await page.waitForSelector('.ant-modal-body');
await page
.locator('[data-testid="slider-input"]')
.fill(profilerSetting.profileSample);
await page.locator('[data-testid="sample-data-count-input"]').clear(); await page.locator('[data-testid="slider-input"]').clear();
await page await page
.locator('[data-testid="sample-data-count-input"]') .locator('[data-testid="slider-input"]')
.fill(profilerSetting.sampleDataCount); .fill(profilerSetting.profileSample);
await page.locator('[data-testid="exclude-column-select"]').click();
await page.keyboard.type(`${profilerSetting.excludeColumns}`);
await page.keyboard.press('Enter');
await page.locator('.CodeMirror-scroll').click();
await page.keyboard.type(profilerSetting.profileQuery);
await page.locator('[data-testid="include-column-select"]').click(); await page.locator('[data-testid="sample-data-count-input"]').clear();
await page await page
.locator('.ant-select-dropdown') .locator('[data-testid="sample-data-count-input"]')
.locator( .fill(profilerSetting.sampleDataCount);
`[title="${profilerSetting.includeColumns}"]:not(.ant-select-dropdown-hidden)` await page.locator('[data-testid="exclude-column-select"]').click();
) await page.keyboard.type(`${profilerSetting.excludeColumns}`);
.last() await page.keyboard.press('Enter');
.click(); await page.locator('.CodeMirror-scroll').click();
await page.locator('[data-testid="enable-partition-switch"]').click(); await page.keyboard.type(profilerSetting.profileQuery);
await page.locator('[data-testid="interval-type"]').click();
await page
.locator('.ant-select-dropdown')
.locator(
`[title="${profilerSetting.partitionIntervalType}"]:not(.ant-select-dropdown-hidden)`
)
.click();
await page.locator('#includeColumnsProfiler_partitionColumnName').click(); await page.locator('[data-testid="include-column-select"]').click();
await page await page
.locator('.ant-select-dropdown') .locator('.ant-select-dropdown')
.locator( .locator(
`[title="${profilerSetting.partitionColumnName}"]:not(.ant-select-dropdown-hidden)` `[title="${profilerSetting.includeColumns}"]:not(.ant-select-dropdown-hidden)`
) )
.last() .last()
.click(); .click();
await page await page.locator('[data-testid="enable-partition-switch"]').click();
.locator('[data-testid="partition-value"]') await page.locator('[data-testid="interval-type"]').click();
.fill(profilerSetting.partitionValues); await page
.locator('.ant-select-dropdown')
.locator(
`[title="${profilerSetting.partitionIntervalType}"]:not(.ant-select-dropdown-hidden)`
)
.click();
const updateTableProfilerConfigResponse = page.waitForResponse( await page.locator('#includeColumnsProfiler_partitionColumnName').click();
(response) => await page
response.url().includes('/api/v1/tables/') && .locator('.ant-select-dropdown')
response.url().includes('/tableProfilerConfig') && .locator(
response.request().method() === 'PUT' `[title="${profilerSetting.partitionColumnName}"]:not(.ant-select-dropdown-hidden)`
); )
await page.getByRole('button', { name: 'Save' }).click(); .last()
const updateResponse = await updateTableProfilerConfigResponse; .click();
const requestBody = await updateResponse.request().postData(); await page
.locator('[data-testid="partition-value"]')
.fill(profilerSetting.partitionValues);
expect(requestBody).toEqual( const updateTableProfilerConfigResponse = page.waitForResponse(
JSON.stringify({ (response) =>
excludeColumns: [table1.entity?.columns[0].name], response.url().includes('/api/v1/tables/') &&
profileQuery: 'select * from table', response.url().includes('/tableProfilerConfig') &&
profileSample: 60, response.request().method() === 'PUT'
profileSampleType: 'PERCENTAGE', );
includeColumns: [{ columnName: table1.entity?.columns[1].name }], await page.getByRole('button', { name: 'Save' }).click();
partitioning: { const updateResponse = await updateTableProfilerConfigResponse;
partitionColumnName: table1.entity?.columns[2].name, const requestBody = await updateResponse.request().postData();
partitionIntervalType: 'COLUMN-VALUE',
partitionValues: ['test'], expect(requestBody).toEqual(
enablePartitioning: true, JSON.stringify({
}, excludeColumns: [table1.entity?.columns[0].name],
sampleDataCount: 100, profileQuery: 'select * from table',
}) profileSample: 60,
); profileSampleType: 'PERCENTAGE',
includeColumns: [{ columnName: table1.entity?.columns[1].name }],
partitioning: {
partitionColumnName: table1.entity?.columns[2].name,
partitionIntervalType: 'COLUMN-VALUE',
partitionValues: ['test'],
enablePartitioning: true,
},
sampleDataCount: 100,
})
);
});
await test.step('Reset profile sample type', async () => {
await page.click('[data-testid="profiler-setting-btn"]');
await page.waitForSelector('.ant-modal-body');
await expect(
page.locator('[data-testid="profile-sample"]')
).toBeVisible();
await page.getByTestId('clear-slider-input').click();
await expect(page.locator('[data-testid="slider-input"]')).toBeEmpty();
const updateTableProfilerConfigResponse = page.waitForResponse(
(response) =>
response.url().includes('/api/v1/tables/') &&
response.url().includes('/tableProfilerConfig') &&
response.request().method() === 'PUT'
);
await page.getByRole('button', { name: 'Save' }).click();
const updateResponse = await updateTableProfilerConfigResponse;
const requestBody = await updateResponse.request().postData();
expect(requestBody).toEqual(
JSON.stringify({
excludeColumns: [table1.entity?.columns[0].name],
profileQuery: 'select * from table',
profileSample: null,
profileSampleType: 'PERCENTAGE',
includeColumns: [{ columnName: table1.entity?.columns[1].name }],
partitioning: {
partitionColumnName: table1.entity?.columns[2].name,
partitionIntervalType: 'COLUMN-VALUE',
partitionValues: ['test'],
enablePartitioning: true,
},
sampleDataCount: 100,
})
);
await page.waitForSelector('.ant-modal-body', {
state: 'detached',
});
// Validate the profiler setting is updated
await page.click('[data-testid="profiler-setting-btn"]');
await page.waitForSelector('.ant-modal-body');
await expect(
page.locator('[data-testid="profile-sample"]')
).toBeVisible();
await expect(page.locator('[data-testid="slider-input"]')).toBeEmpty();
await expect(
page.getByTestId('profile-sample').locator('div')
).toBeVisible();
});
} }
); );

View File

@ -175,24 +175,23 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
sqlQuery: profileQuery ?? '', sqlQuery: profileQuery ?? '',
profileSample: profileSample, profileSample: profileSample,
excludeCol: excludeColumns ?? [], excludeCol: excludeColumns ?? [],
selectedProfileSampleType: selectedProfileSampleType: profileSampleType,
profileSampleType ?? ProfileSampleType.Percentage,
sampleDataCount, sampleDataCount,
}); });
form.setFieldsValue({ form.setFieldsValue({
sampleDataCount: sampleDataCount ?? initialState.sampleDataCount, sampleDataCount: sampleDataCount ?? initialState.sampleDataCount,
}); });
const profileSampleTypeCheck =
profileSampleType === ProfileSampleType.Percentage;
form.setFieldsValue({ form.setFieldsValue({
profileSampleType, profileSampleType,
profileSamplePercentage: profileSampleTypeCheck profileSamplePercentage:
? profileSample ?? 100 profileSample && profileSampleType === ProfileSampleType.Percentage
: 100, ? profileSample
profileSampleRows: !profileSampleTypeCheck : undefined,
? profileSample ?? 100 profileSampleRows:
: undefined, profileSample && profileSampleType === ProfileSampleType.Rows
? profileSample
: undefined,
}); });
if (includeColumns && includeColumns?.length > 0) { if (includeColumns && includeColumns?.length > 0) {
@ -287,11 +286,14 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
const profileConfig: TableProfilerConfig = { const profileConfig: TableProfilerConfig = {
excludeColumns: excludeCol.length > 0 ? excludeCol : undefined, excludeColumns: excludeCol.length > 0 ? excludeCol : undefined,
profileQuery: !isEmpty(sqlQuery) ? sqlQuery : undefined, profileQuery: !isEmpty(sqlQuery) ? sqlQuery : undefined,
profileSample: profileSample: profileSampleType
profileSampleType === ProfileSampleType.Percentage ? profileSampleType === ProfileSampleType.Percentage
? profileSamplePercentage ? profileSamplePercentage
: profileSampleRows, : profileSampleRows
profileSampleType: profileSampleType, : undefined,
profileSampleType: isUndefined(profileSampleType)
? null
: profileSampleType,
includeColumns: !isEqual(includeCol, DEFAULT_INCLUDE_PROFILE) includeColumns: !isEqual(includeCol, DEFAULT_INCLUDE_PROFILE)
? getIncludesColumns() ? getIncludesColumns()
: undefined, : undefined,
@ -461,31 +463,37 @@ const ProfilerSettingsModal: React.FC<ProfilerSettingsModalProps> = ({
})} })}
name="profileSampleType"> name="profileSampleType">
<Select <Select
allowClear
autoFocus autoFocus
className="w-full" className="w-full"
data-testid="profile-sample" data-testid="profile-sample"
options={PROFILE_SAMPLE_OPTIONS} options={PROFILE_SAMPLE_OPTIONS}
placeholder={t('label.please-select-entity', {
entity: t('label.profile-sample-type', {
type: '',
}),
})}
onChange={handleProfileSampleType} onChange={handleProfileSampleType}
/> />
</Form.Item> </Form.Item>
{state?.selectedProfileSampleType === {state?.selectedProfileSampleType ===
ProfileSampleType.Percentage ? ( ProfileSampleType.Percentage && (
<Form.Item <Form.Item
className="m-b-0"
label={t('label.profile-sample-type', { label={t('label.profile-sample-type', {
type: t('label.value'), type: t('label.value'),
})} })}
name="profileSamplePercentage"> name="profileSamplePercentage">
<SliderWithInput <SliderWithInput
className="p-x-xs" className="p-x-xs"
value={state?.profileSample || 0} value={state?.profileSample}
onChange={handleProfileSample} onChange={handleProfileSample}
/> />
</Form.Item> </Form.Item>
) : ( )}
{state?.selectedProfileSampleType === ProfileSampleType.Rows && (
<Form.Item <Form.Item
className="m-b-0"
label={t('label.profile-sample-type', { label={t('label.profile-sample-type', {
type: t('label.value'), type: t('label.value'),
})} })}

View File

@ -12,7 +12,7 @@
*/ */
export interface SliderWithInputProps { export interface SliderWithInputProps {
value: number; value?: number;
onChange: (value: number | null) => void; onChange: (value: number | null) => void;
className?: string; className?: string;
} }

View File

@ -11,20 +11,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { Col, InputNumber, Row, Slider } from 'antd'; import { CloseOutlined } from '@ant-design/icons';
import React, { useCallback } from 'react'; import { Button, Col, InputNumber, Row, Slider, Tooltip } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { percentageFormatter } from '../../../utils/ChartUtils';
import { SliderWithInputProps } from './SliderWithInput.interface'; import { SliderWithInputProps } from './SliderWithInput.interface';
const SliderWithInput = ({ const SliderWithInput = ({
value, value,
onChange, onChange,
className, className,
}: SliderWithInputProps) => { }: SliderWithInputProps) => {
const formatter = useCallback((value) => `${value}%`, [value]); const { t } = useTranslation();
return ( return (
<Row className={className} data-testid="percentage-input" gutter={20}> <Row className={className} data-testid="percentage-input" gutter={20}>
<Col span={20}> <Col flex="auto">
<Slider <Slider
marks={{ marks={{
0: '0%', 0: '0%',
@ -37,16 +39,27 @@ const SliderWithInput = ({
onChange={onChange} onChange={onChange}
/> />
</Col> </Col>
<Col span={4}> <Col className="w-32">
<InputNumber <div className="flex items-center gap-2">
data-testid="slider-input" <InputNumber
formatter={formatter} data-testid="slider-input"
max={100} formatter={percentageFormatter}
min={0} max={100}
step={1} min={0}
value={value} step={1}
onChange={onChange} value={value}
/> onChange={onChange}
/>
<Tooltip title={t('label.clear')}>
<Button
className="p-0"
data-testid="clear-slider-input"
type="text"
onClick={() => onChange(null)}>
<CloseOutlined />
</Button>
</Tooltip>
</div>
</Col> </Col>
</Row> </Row>
); );

View File

@ -0,0 +1,31 @@
/*
* Copyright 2025 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { percentageFormatter } from './ChartUtils';
describe('ChartUtils', () => {
describe('percentageFormatter', () => {
it('should format number with percentage symbol', () => {
expect(percentageFormatter(50)).toBe('50%');
expect(percentageFormatter(100)).toBe('100%');
});
it('should handle decimal numbers', () => {
expect(percentageFormatter(50.5)).toBe('50.5%');
expect(percentageFormatter(33.33)).toBe('33.33%');
});
it('should return empty string for undefined value', () => {
expect(percentageFormatter(undefined)).toBe('');
});
});
});

View File

@ -42,3 +42,7 @@ export const updateActiveChartFilter = (
return updatedData; return updatedData;
}; };
export const percentageFormatter = (value?: number) => {
return value ? `${value}%` : '';
};