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,8 +505,10 @@ test(
await page.reload(); await page.reload();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
await test.step('Update profiler setting', async () => {
await page.click('[data-testid="profiler-setting-btn"]'); await page.click('[data-testid="profiler-setting-btn"]');
await page.waitForSelector('.ant-modal-body'); await page.waitForSelector('.ant-modal-body');
await page.locator('[data-testid="slider-input"]').clear(); await page.locator('[data-testid="slider-input"]').clear();
await page await page
.locator('[data-testid="slider-input"]') .locator('[data-testid="slider-input"]')
@ -577,6 +579,63 @@ test(
sampleDataCount: 100, 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,23 +175,22 @@ 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:
profileSample && profileSampleType === ProfileSampleType.Rows
? profileSample
: undefined, : undefined,
}); });
@ -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">
<div className="flex items-center gap-2">
<InputNumber <InputNumber
data-testid="slider-input" data-testid="slider-input"
formatter={formatter} formatter={percentageFormatter}
max={100} max={100}
min={0} min={0}
step={1} step={1}
value={value} value={value}
onChange={onChange} 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}%` : '';
};