chore: Changes the AlertReportModal to use the new Select component (#16144)

* chore: Changes the AlertReportModal to use the new Select component

* Gives time to close the select before changing the type
This commit is contained in:
Michael S. Molina 2021-08-24 08:19:27 -03:00 committed by GitHub
parent c14364c616
commit 1fc9318594
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 233 additions and 271 deletions

View File

@ -19,8 +19,8 @@
import React, { useEffect, useRef } from 'react';
import moment from 'moment-timezone';
import { NativeGraySelect as Select } from 'src/components/Select';
import { t } from '@superset-ui/core';
import { Select } from 'src/components';
const DEFAULT_TIMEZONE = 'GMT Standard Time';
const MIN_SELECT_WIDTH = '400px';
@ -92,12 +92,6 @@ const TIMEZONE_OPTIONS = TIMEZONES.sort(
offsets: getOffsetKey(zone.name),
}));
const timezoneOptions = TIMEZONE_OPTIONS.map(option => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
));
const TimezoneSelector = ({ onTimezoneChange, timezone }: TimezoneProps) => {
const prevTimezone = useRef(timezone);
const matchTimezoneToOptions = (timezone: string) =>
@ -120,12 +114,12 @@ const TimezoneSelector = ({ onTimezoneChange, timezone }: TimezoneProps) => {
return (
<Select
ariaLabel={t('Timezone')}
css={{ minWidth: MIN_SELECT_WIDTH }} // smallest size for current values
onChange={onTimezoneChange}
value={timezone || DEFAULT_TIMEZONE}
>
{timezoneOptions}
</Select>
options={TIMEZONE_OPTIONS}
/>
);
};

View File

@ -24,7 +24,7 @@ import fetchMock from 'fetch-mock';
import { act } from 'react-dom/test-utils';
import AlertReportModal from 'src/views/CRUD/alert/AlertReportModal';
import Modal from 'src/components/Modal';
import { AsyncSelect, NativeGraySelect as Select } from 'src/components/Select';
import { Select } from 'src/components';
import { Switch } from 'src/components/Switch';
import { Radio } from 'src/components/Radio';
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
@ -161,11 +161,15 @@ describe('AlertReportModal', () => {
};
const editWrapper = await mountAndWait(props);
expect(editWrapper.find(AsyncSelect).at(1).props().value).toEqual({
expect(
editWrapper.find('[aria-label="Database"]').at(0).props().value,
).toEqual({
value: 1,
label: 'test database',
});
expect(editWrapper.find(AsyncSelect).at(2).props().value).toEqual({
expect(
editWrapper.find('[aria-label="Chart"]').at(0).props().value,
).toEqual({
value: 1,
label: 'test chart',
});
@ -176,21 +180,9 @@ describe('AlertReportModal', () => {
expect(wrapper.find('input[name="name"]')).toExist();
});
it('renders three async select elements when in report mode', () => {
expect(wrapper.find(AsyncSelect)).toExist();
expect(wrapper.find(AsyncSelect)).toHaveLength(3);
});
it('renders four async select elements when in alert mode', async () => {
const props = {
...mockedProps,
isReport: false,
};
const addWrapper = await mountAndWait(props);
expect(addWrapper.find(AsyncSelect)).toExist();
expect(addWrapper.find(AsyncSelect)).toHaveLength(4);
it('renders five select elements when in report mode', () => {
expect(wrapper.find(Select)).toExist();
expect(wrapper.find(Select)).toHaveLength(5);
});
it('renders Switch element', () => {
@ -226,12 +218,12 @@ describe('AlertReportModal', () => {
expect(input.props().value).toEqual('SELECT NaN');
});
it('renders two select element when in report mode', () => {
it('renders five select element when in report mode', () => {
expect(wrapper.find(Select)).toExist();
expect(wrapper.find(Select)).toHaveLength(2);
expect(wrapper.find(Select)).toHaveLength(5);
});
it('renders three select elements when in alert mode', async () => {
it('renders seven select elements when in alert mode', async () => {
const props = {
...mockedProps,
isReport: false,
@ -240,7 +232,7 @@ describe('AlertReportModal', () => {
const addWrapper = await mountAndWait(props);
expect(addWrapper.find(Select)).toExist();
expect(addWrapper.find(Select)).toHaveLength(3);
expect(addWrapper.find(Select)).toHaveLength(7);
});
it('renders value input element when in alert mode', async () => {

View File

@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { FunctionComponent, useState, useEffect } from 'react';
import React, {
FunctionComponent,
useState,
useEffect,
useMemo,
useCallback,
} from 'react';
import {
styled,
t,
@ -32,7 +38,7 @@ import { Switch } from 'src/components/Switch';
import Modal from 'src/components/Modal';
import TimezoneSelector from 'src/components/TimezoneSelector';
import { Radio } from 'src/components/Radio';
import { AsyncSelect, NativeGraySelect as Select } from 'src/components/Select';
import { Select } from 'src/components';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import Owner from 'src/types/Owner';
@ -215,10 +221,6 @@ const StyledSectionContainer = styled.div`
}
}
}
.hide-dropdown {
display: none;
}
`;
const StyledSectionTitle = styled.div`
@ -248,7 +250,7 @@ const StyledSwitchContainer = styled.div`
`;
export const StyledInputContainer = styled.div`
flex: 1 1 auto;
flex: 1;
margin: ${({ theme }) => theme.gridUnit * 2}px;
margin-top: 0;
@ -284,9 +286,7 @@ export const StyledInputContainer = styled.div`
}
input,
textarea,
.Select,
.ant-select {
textarea {
flex: 1 1 auto;
}
@ -300,36 +300,24 @@ export const StyledInputContainer = styled.div`
}
input::placeholder,
textarea::placeholder,
.Select__placeholder {
textarea::placeholder {
color: ${({ theme }) => theme.colors.grayscale.light1};
}
textarea,
input[type='text'],
input[type='number'],
.Select__control,
.ant-select-single .ant-select-selector {
padding: ${({ theme }) => theme.gridUnit * 1.5}px
input[type='number'] {
padding: ${({ theme }) => theme.gridUnit}px
${({ theme }) => theme.gridUnit * 2}px;
border-style: none;
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
border-radius: ${({ theme }) => theme.gridUnit}px;
.ant-select-selection-placeholder,
.ant-select-selection-item {
line-height: 24px;
}
&[name='description'] {
flex: 1 1 auto;
}
}
.Select__control {
padding: 2px 0;
}
.input-label {
margin-left: 10px;
}
@ -581,95 +569,101 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
};
// Fetch data to populate form dropdowns
const loadOwnerOptions = (input = '') => {
const query = rison.encode({ filter: input, page_size: SELECT_PAGE_SIZE });
return SupersetClient.get({
endpoint: `/api/v1/report/related/owners?q=${query}`,
}).then(
response =>
response.json.result.map((item: any) => ({
value: item.value,
label: item.text,
})),
badResponse => [],
);
};
const loadOwnerOptions = useMemo(
() => (input = '', page: number, pageSize: number) => {
const query = rison.encode({ filter: input, page, page_size: pageSize });
return SupersetClient.get({
endpoint: `/api/v1/report/related/owners?q=${query}`,
}).then(response => ({
data: response.json.result.map(
(item: { value: number; text: string }) => ({
value: item.value,
label: item.text,
}),
),
totalCount: response.json.count,
}));
},
[],
);
const loadSourceOptions = (input = '') => {
const query = rison.encode({ filter: input, page_size: SELECT_PAGE_SIZE });
return SupersetClient.get({
endpoint: `/api/v1/report/related/database?q=${query}`,
}).then(
response => {
const list = response.json.result.map((item: any) => ({
value: item.value,
label: item.text,
}));
const getSourceData = useCallback(
(db?: MetaObject) => {
const database = db || currentAlert?.database;
setSourceOptions(list);
// Find source if current alert has one set
if (
currentAlert &&
currentAlert.database &&
!currentAlert.database.label
) {
updateAlertState('database', getSourceData());
}
return list;
},
badResponse => [],
);
};
const getSourceData = (db?: MetaObject) => {
const database = db || currentAlert?.database;
if (!database || database.label) {
return null;
}
let result;
// Cycle through source options to find the selected option
sourceOptions.forEach(source => {
if (source.value === database.value || source.value === database.id) {
result = source;
if (!database || database.label) {
return null;
}
});
return result;
};
let result;
const loadDashboardOptions = (input = '') => {
const query = rison.encode({ filter: input, page_size: SELECT_PAGE_SIZE });
return SupersetClient.get({
endpoint: `/api/v1/report/related/dashboard?q=${query}`,
}).then(
response => {
const list = response.json.result.map((item: any) => ({
value: item.value,
label: item.text,
}));
setDashboardOptions(list);
// Find source if current alert has one set
if (
currentAlert &&
currentAlert.dashboard &&
!currentAlert.dashboard.label
) {
updateAlertState('dashboard', getDashboardData());
// Cycle through source options to find the selected option
sourceOptions.forEach(source => {
if (source.value === database.value || source.value === database.id) {
result = source;
}
});
return list;
},
badResponse => [],
);
return result;
},
[currentAlert?.database, sourceOptions],
);
// Updating alert/report state
const updateAlertState = (name: string, value: any) => {
setCurrentAlert(currentAlertData => ({
...currentAlertData,
[name]: value,
}));
};
const loadSourceOptions = useMemo(
() => (input = '', page: number, pageSize: number) => {
const query = rison.encode({ filter: input, page, page_size: pageSize });
return SupersetClient.get({
endpoint: `/api/v1/report/related/database?q=${query}`,
}).then(response => {
const list = response.json.result.map(
(item: { value: number; text: string }) => ({
value: item.value,
label: item.text,
}),
);
setSourceOptions(list);
return { data: list, totalCount: response.json.count };
});
},
[],
);
const databaseLabel =
currentAlert && currentAlert.database && !currentAlert.database.label;
useEffect(() => {
// Find source if current alert has one set
if (databaseLabel) {
updateAlertState('database', getSourceData());
}
}, [databaseLabel, getSourceData]);
const loadDashboardOptions = useMemo(
() => (input = '', page: number, pageSize: number) => {
const query = rison.encode({ filter: input, page, page_size: pageSize });
return SupersetClient.get({
endpoint: `/api/v1/report/related/dashboard?q=${query}`,
}).then(response => {
const list = response.json.result.map(
(item: { value: number; text: string }) => ({
value: item.value,
label: item.text,
}),
);
setDashboardOptions(list);
return { data: list, totalCount: response.json.count };
});
},
[],
);
const getDashboardData = (db?: MetaObject) => {
const dashboard = db || currentAlert?.dashboard;
@ -689,62 +683,62 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
return result;
};
const loadChartOptions = (input = '') => {
const query = rison.encode({ filter: input, page_size: SELECT_PAGE_SIZE });
return SupersetClient.get({
endpoint: `/api/v1/report/related/chart?q=${query}`,
}).then(
response => {
const list = response.json.result.map((item: any) => ({
value: item.value,
label: item.text,
}));
const getChartData = useCallback(
(chartData?: MetaObject) => {
const chart = chartData || currentAlert?.chart;
if (!chart || chart.label) {
return null;
}
let result;
// Cycle through chart options to find the selected option
chartOptions.forEach(slice => {
if (slice.value === chart.value || slice.value === chart.id) {
result = slice;
}
});
return result;
},
[chartOptions, currentAlert?.chart],
);
const noChartLabel =
currentAlert && currentAlert.chart && !currentAlert.chart.label;
useEffect(() => {
// Find source if current alert has one set
if (noChartLabel) {
updateAlertState('chart', getChartData());
}
}, [getChartData, noChartLabel]);
const loadChartOptions = useMemo(
() => (input = '', page: number, pageSize: number) => {
const query = rison.encode({ filter: input, page, page_size: pageSize });
return SupersetClient.get({
endpoint: `/api/v1/report/related/chart?q=${query}`,
}).then(response => {
const list = response.json.result.map(
(item: { value: number; text: string }) => ({
value: item.value,
label: item.text,
}),
);
setChartOptions(list);
// Find source if current alert has one set
if (currentAlert && currentAlert.chart && !currentAlert.chart.label) {
updateAlertState('chart', getChartData());
}
return list;
},
badResponse => [],
);
};
const getChartData = (chartData?: MetaObject) => {
const chart = chartData || currentAlert?.chart;
if (!chart || chart.label) {
return null;
}
let result;
// Cycle through chart options to find the selected option
chartOptions.forEach(slice => {
if (slice.value === chart.value || slice.value === chart.id) {
result = slice;
}
});
return result;
};
return { data: list, totalCount: response.json.count };
});
},
[],
);
const getChartVisualizationType = (chart: SelectValue) =>
SupersetClient.get({
endpoint: `/api/v1/chart/${chart.value}`,
}).then(response => setChartVizType(response.json.result.viz_type));
// Updating alert/report state
const updateAlertState = (name: string, value: any) => {
setCurrentAlert(currentAlertData => ({
...currentAlertData,
[name]: value,
}));
};
// Handle input/textarea updates
const onTextChange = (
event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
@ -775,11 +769,11 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
updateAlertState('sql', value || '');
};
const onOwnersChange = (value: Array<Owner>) => {
const onOwnersChange = (value: Array<SelectValue>) => {
updateAlertState('owners', value || []);
};
const onSourceChange = (value: Array<Owner>) => {
const onSourceChange = (value: Array<SelectValue>) => {
updateAlertState('database', value || []);
};
@ -832,8 +826,8 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
const onContentTypeChange = (event: any) => {
const { target } = event;
setContentType(target.value);
// Gives time to close the select before changing the type
setTimeout(() => setContentType(target.value), 200);
};
const onFormatChange = (event: any) => {
@ -1004,19 +998,6 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
setIsHidden(false);
}
// Dropdown options
const conditionOptions = CONDITIONS.map(condition => (
<Select.Option key={condition.value} value={condition.value}>
{condition.label}
</Select.Option>
));
const retentionOptions = RETENTION_OPTIONS.map(option => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
));
return (
<StyledModal
className="no-content-padding"
@ -1064,13 +1045,22 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
<span className="required">*</span>
</div>
<div data-test="owners-select" className="input-container">
<AsyncSelect
<Select
ariaLabel={t('Owners')}
allowClear
// TODO Use default page size (100). To do that, the server
// needs to send the total number of results. Currently, the
// number of results is limited to SELECT_PAGE_SIZE
pageSize={SELECT_PAGE_SIZE}
name="owners"
isMulti
value={currentAlert ? currentAlert.owners : []}
loadOptions={loadOwnerOptions}
defaultOptions // load options on render
cacheOptions
mode="multiple"
value={
(currentAlert?.owners as {
label: string;
value: number;
}[]) || []
}
options={loadOwnerOptions}
onChange={onOwnersChange}
/>
</div>
@ -1107,19 +1097,23 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
<span className="required">*</span>
</div>
<div className="input-container">
<AsyncSelect
<Select
ariaLabel={t('Database')}
// TODO Use default page size (100). To do that, the server
// needs to send the total number of results. Currently, the
// number of results is limited to SELECT_PAGE_SIZE
pageSize={SELECT_PAGE_SIZE}
name="source"
value={
currentAlert?.database
currentAlert?.database?.label &&
currentAlert?.database?.value
? {
value: currentAlert.database.value,
label: currentAlert.database.label,
}
: undefined
}
loadOptions={loadSourceOptions}
defaultOptions // load options on render
cacheOptions
options={loadSourceOptions}
onChange={onSourceChange}
/>
</div>
@ -1148,21 +1142,14 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
</div>
<div className="input-container">
<Select
ariaLabel={t('Condition')}
onChange={onConditionChange}
placeholder="Condition"
defaultValue={
currentAlert
? currentAlert.validator_config_json?.op || undefined
: undefined
}
value={
currentAlert
? currentAlert.validator_config_json?.op || undefined
: undefined
currentAlert?.validator_config_json?.op || undefined
}
>
{conditionOptions}
</Select>
options={CONDITIONS}
/>
</div>
</StyledInputContainer>
<StyledInputContainer>
@ -1224,21 +1211,12 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
</div>
<div className="input-container">
<Select
ariaLabel={t('Log retention')}
placeholder={t('Log retention')}
onChange={onLogRetentionChange}
placeholder
defaultValue={
currentAlert
? currentAlert.log_retention || DEFAULT_RETENTION
: DEFAULT_RETENTION
}
value={
currentAlert
? currentAlert.log_retention || DEFAULT_RETENTION
: DEFAULT_RETENTION
}
>
{retentionOptions}
</Select>
value={currentAlert?.log_retention || DEFAULT_RETENTION}
options={RETENTION_OPTIONS}
/>
</div>
</StyledInputContainer>
<StyledInputContainer>
@ -1284,44 +1262,46 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
<StyledRadio value="dashboard">{t('Dashboard')}</StyledRadio>
<StyledRadio value="chart">{t('Chart')}</StyledRadio>
</Radio.Group>
<AsyncSelect
className={
contentType === 'chart'
? 'async-select'
: 'hide-dropdown async-select'
}
<Select
ariaLabel={t('Chart')}
css={{
display: contentType === 'chart' ? 'inline' : 'none',
}}
// TODO Use default page size (100). To do that, the server
// needs to send the total number of results. Currently, the
// number of results is limited to SELECT_PAGE_SIZE
pageSize={SELECT_PAGE_SIZE}
name="chart"
value={
currentAlert && currentAlert.chart
currentAlert?.chart?.label && currentAlert?.chart?.value
? {
value: currentAlert.chart.value,
label: currentAlert.chart.label,
}
: undefined
}
loadOptions={loadChartOptions}
defaultOptions // load options on render
cacheOptions
options={loadChartOptions}
onChange={onChartChange}
/>
<AsyncSelect
className={
contentType === 'dashboard'
? 'async-select'
: 'hide-dropdown async-select'
}
<Select
ariaLabel={t('Dashboard')}
css={{
display: contentType === 'dashboard' ? 'inline' : 'none',
}}
// TODO Use default page size (100). To do that, the server
// needs to send the total number of results. Currently, the
// number of results is limited to SELECT_PAGE_SIZE
pageSize={SELECT_PAGE_SIZE}
name="dashboard"
value={
currentAlert && currentAlert.dashboard
currentAlert?.dashboard?.label && currentAlert?.dashboard?.value
? {
value: currentAlert.dashboard.value,
label: currentAlert.dashboard.label,
}
: undefined
}
loadOptions={loadDashboardOptions}
defaultOptions // load options on render
cacheOptions
options={loadDashboardOptions}
onChange={onDashboardChange}
/>
{formatOptionEnabled && (

View File

@ -18,7 +18,7 @@
*/
import React, { FunctionComponent, useState } from 'react';
import { styled, t, useTheme } from '@superset-ui/core';
import { NativeGraySelect as Select } from 'src/components/Select';
import { Select } from 'src/components';
import Icons from 'src/components/Icons';
import { StyledInputContainer } from '../AlertReportModal';
@ -116,26 +116,22 @@ export const NotificationMethod: FunctionComponent<NotificationMethodProps> = ({
setRecipientValue(recipients);
}
const methodOptions = (options || []).map((method: NotificationMethod) => (
<Select.Option key={method} value={method}>
{t(method)}
</Select.Option>
));
return (
<StyledNotificationMethod>
<div className="inline-container">
<StyledInputContainer>
<div className="input-container">
<Select
ariaLabel={t('Delivery method')}
data-test="select-delivery-method"
onChange={onMethodChange}
placeholder="Select Delivery Method"
defaultValue={method}
placeholder={t('Select Delivery Method')}
options={(options || []).map((method: NotificationMethod) => ({
label: method,
value: method,
}))}
value={method}
>
{methodOptions}
</Select>
/>
</div>
</StyledInputContainer>
{method !== undefined && !!onRemove ? (