kgabryje commented on code in PR #26202: URL: https://github.com/apache/superset/pull/26202#discussion_r1473407080
########## package-lock.json: ########## @@ -0,0 +1,6 @@ +{ Review Comment: Delete this file too, I suppose it got created when you accidentally run `npm install` in the root superset dir :) ########## superset-frontend/src/components/CronPicker/CronPicker.tsx: ########## @@ -110,22 +110,81 @@ export const CronPicker = styled((props: CronProps) => ( <ReactCronPicker locale={LOCALE} {...props} /> </ConfigProvider> ))` - .react-js-cron-field { - margin-bottom: 0px; +:has(.react-js-cron-months){ Review Comment: Suggestion - if you wrap the whole css code in `${({ theme }) => ...}`, you won't need to call it in every line. For example `grid-column-gap: ${({ theme }) => theme.gridUnit}px;` will become `grid-column-gap: ${theme.gridUnit}px;` You can see an example in `DashboardWrapper.tsx` and many other files <img width="589" alt="image" src="https://github.com/apache/superset/assets/15073128/73a826ea-f8c0-4d73-af1e-b240230d5797"> ########## superset-frontend/src/components/Button/index.tsx: ########## @@ -209,7 +209,7 @@ export default function Button(props: ButtonProps) { return ( <Tooltip placement={placement} - id={`${kebabCase(tooltip)}-tooltip`} + // id={`${kebabCase(tooltip)}-tooltip`} Review Comment: I don't think this change is relevant, could you uncomment? ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -385,47 +354,83 @@ interface NotificationMethodAddProps { } export const TRANSLATIONS = { - ADD_NOTIFICATION_METHOD_TEXT: t('Add notification method'), - ADD_DELIVERY_METHOD_TEXT: t('Add delivery method'), - SAVE_TEXT: t('Save'), - ADD_TEXT: t('Add'), + // Modal header EDIT_REPORT_TEXT: t('Edit Report'), EDIT_ALERT_TEXT: t('Edit Alert'), ADD_REPORT_TEXT: t('Add Report'), ADD_ALERT_TEXT: t('Add Alert'), + // Panel titles + GENERAL_TITLE: t('General information'), + ALERT_CONDITION_TITLE: t('Alert condition'), + ALERT_CONTENTS_TITLE: t('Alert contents'), + REPORT_CONTENTS_TITLE: t('Report contents'), + SCHEDULE_TITLE: t('Schedule'), + NOTIFICATION_TITLE: t('Notification method'), + // Panel subtitles + GENERAL_SUBTITLE: t('Set up basic details, such as name and description.'), + ALERT_CONDITION_SUBTITLE: t( + 'Define the database, SQL query, and triggering conditions for alert.', + ), + CONTENTS_SUBTITLE: t('Customize data source, filters, and layout.'), + SCHEDULE_SUBTITLE: t( + 'Define delivery schedule, timezone, and frequency settings.', + ), + NOTIFICATION_SUBTITLE: t('Choose notification method and recipients.'), + // General info panel inputs REPORT_NAME_TEXT: t('Report name'), + REPORT_NAME_PLACEHOLDER: t('Enter report name'), ALERT_NAME_TEXT: t('Alert name'), + ALERT_NAME_PLACEHOLDER: t('Enter alert name'), OWNERS_TEXT: t('Owners'), + OWNERS_PLACEHOLDER: t('Select owners'), DESCRIPTION_TEXT: t('Description'), - ACTIVE_TEXT: t('Active'), - ALERT_CONDITION_TEXT: t('Alert condition'), + REPORT_DESCRIPTION_PLACEHOLDER: t( + 'Include description to be sent with report', + ), + ALERT_DESCRIPTION_PLACEHOLDER: t('Include description to be sent with alert'), + ACTIVE_REPORT_TEXT: t('Report is active'), + ACTIVE_ALERT_TEXT: t('Alert is active'), + // Alert condition panel inputs DATABASE_TEXT: t('Database'), + DATABASE_PLACEHOLDER: t('Select database'), SQL_QUERY_TEXT: t('SQL Query'), SQL_QUERY_TOOLTIP: t( - 'The result of this query should be a numeric-esque value', + 'The result of this query must be a value capable of numeric interpretation e.g. 1, 1.0, or "1" (compatible with Python\'s float() function).', ), TRIGGER_ALERT_IF_TEXT: t('Trigger Alert If...'), CONDITION_TEXT: t('Condition'), + CONDITION_PLACEHOLDER: t('Condition'), VALUE_TEXT: t('Value'), - REPORT_SCHEDULE_TEXT: t('Report schedule'), - ALERT_CONDITION_SCHEDULE_TEXT: t('Alert condition schedule'), + // Contents panel inputs + CONTENT_TYPE_LABEL: t('Content type'), + CONTENT_TYPE_PLACEHOLDER: t('Select content type'), + SELECT_DASHBOARD_LABEL: t('Select dashboard'), + SELECT_DASHBOARD_PLACEHOLDER: t('Select dashboard to use'), + SELECT_CHART_LABEL: t('Select chart'), + SELECT_CHART_PLACEHOLDER: t('Select chart to use'), + DASHBOARD_TEXT: t('Dashboard'), + CHART_TEXT: t('Chart'), + FORMAT_TYPE_LABEL: t('Content Format'), Review Comment: nit - lowercase `format` ########## superset-frontend/src/features/alerts/components/AlertReportCronScheduler.test.tsx: ########## @@ -32,7 +32,7 @@ const createProps = (props: Partial<AlertReportCronSchedulerProps> = {}) => ({ ...props, }); -test('should render', () => { +test.skip('should render', () => { Review Comment: Can we unskip/fix those tests? ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -1190,367 +1313,456 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ </h4> } > - <StyledSectionContainer> - <div className="header-section"> - <StyledInputContainer> - <div className="control-label"> - {isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="text" - name="name" - value={currentAlert ? currentAlert.name : ''} - placeholder={ - isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT - } - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.OWNERS_TEXT} - <span className="required">*</span> - </div> - <div data-test="owners-select" className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.OWNERS_TEXT} - allowClear - name="owners" - mode="multiple" - value={ - (currentAlert?.owners as { - label: string; - value: number; - }[]) || [] - } - options={loadOwnerOptions} - onChange={onOwnersChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label">{TRANSLATIONS.DESCRIPTION_TEXT}</div> - <div className="input-container"> - <input - type="text" - name="description" - value={currentAlert ? currentAlert.description || '' : ''} - placeholder={TRANSLATIONS.DESCRIPTION_TEXT} - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledSwitchContainer> - <Switch - onChange={onActiveSwitch} - checked={currentAlert ? currentAlert.active : true} + <Collapse + expandIconPosition="right" + defaultActiveKey="1" + accordion + style={{ border: 'none' }} + > + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.GENERAL_TITLE} + subtitle={TRANSLATIONS.GENERAL_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.GENERAL].status} + testId="general-information-panel" /> - <div className="switch-label">{TRANSLATIONS.ACTIVE_TEXT}</div> - </StyledSwitchContainer> - </div> - <div className="column-section"> - {!isReport && ( - <div className="column condition"> - <StyledSectionTitle> - <h4>{TRANSLATIONS.ALERT_CONDITION_TEXT}</h4> - </StyledSectionTitle> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.DATABASE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.DATABASE_TEXT} - name="source" - value={ - currentAlert?.database?.label && - currentAlert?.database?.value - ? { - value: currentAlert.database.value, - label: currentAlert.database.label, - } - : undefined - } - options={loadSourceOptions} - onChange={onSourceChange} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.SQL_QUERY_TEXT} - <StyledTooltip tooltip={TRANSLATIONS.SQL_QUERY_TOOLTIP} /> - <span className="required">*</span> - </div> - <TextAreaControl - name="sql" - language="sql" - offerEditInModal={false} - minLines={15} - maxLines={15} - onChange={onSQLChange} - readOnly={false} - initialValue={resource?.sql} - key={currentAlert?.id} + } + key="1" + style={panelBorder} + > + <div className="header-section"> + <StyledInputContainer> + <div className="control-label"> + {isReport + ? TRANSLATIONS.REPORT_NAME_TEXT + : TRANSLATIONS.ALERT_NAME_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <input + type="text" + name="name" + value={currentAlert ? currentAlert.name : ''} + placeholder={ + isReport + ? TRANSLATIONS.REPORT_NAME_PLACEHOLDER + : TRANSLATIONS.ALERT_NAME_PLACEHOLDER + } + onChange={onInputChange} /> - </StyledInputContainer> - <div className="inline-container wrap"> - <StyledInputContainer> - <div className="control-label" css={inputSpacer}> - {TRANSLATIONS.TRIGGER_ALERT_IF_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <Select - ariaLabel={TRANSLATIONS.CONDITION_TEXT} - onChange={onConditionChange} - placeholder="Condition" - value={ - currentAlert?.validator_config_json?.op || undefined - } - options={CONDITIONS} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.VALUE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="number" - name="threshold" - disabled={conditionNotNull} - value={ - currentAlert?.validator_config_json?.threshold !== - undefined - ? currentAlert.validator_config_json.threshold - : '' - } - placeholder={TRANSLATIONS.VALUE_TEXT} - onChange={onThresholdChange} - /> - </div> - </StyledInputContainer> </div> - </div> - )} - <div className="column schedule"> - <StyledSectionTitle> - <h4> - {isReport - ? TRANSLATIONS.REPORT_SCHEDULE_TEXT - : TRANSLATIONS.ALERT_CONDITION_SCHEDULE_TEXT} - </h4> - <span className="required">*</span> - </StyledSectionTitle> - <AlertReportCronScheduler - value={currentAlert?.crontab || ALERT_REPORTS_DEFAULT_CRON_VALUE} - onChange={newVal => updateAlertState('crontab', newVal)} - /> - <div className="control-label">{TRANSLATIONS.TIMEZONE_TEXT}</div> - <div - className="input-container" - css={(theme: SupersetTheme) => timezoneHeaderStyle(theme)} - > - <TimezoneSelector - onTimezoneChange={onTimezoneChange} - timezone={currentAlert?.timezone} - minWidth="100%" - /> - </div> - <StyledSectionTitle> - <h4>{TRANSLATIONS.SCHEDULE_SETTINGS_TEXT}</h4> - </StyledSectionTitle> + </StyledInputContainer> <StyledInputContainer> <div className="control-label"> - {TRANSLATIONS.LOG_RETENTION_TEXT} + {TRANSLATIONS.OWNERS_TEXT} <span className="required">*</span> </div> - <div className="input-container"> - <Select - ariaLabel={TRANSLATIONS.LOG_RETENTION_TEXT} - placeholder={TRANSLATIONS.LOG_RETENTION_TEXT} - onChange={onLogRetentionChange} + <div data-test="owners-select" className="input-container"> + <AsyncSelect + ariaLabel={TRANSLATIONS.OWNERS_TEXT} + allowClear + name="owners" + mode="multiple" + placeholder={TRANSLATIONS.OWNERS_PLACEHOLDER} value={ - typeof currentAlert?.log_retention === 'number' - ? currentAlert?.log_retention - : ALERT_REPORTS_DEFAULT_RETENTION + (currentAlert?.owners as { + label: string; + value: number; + }[]) || [] } - options={RETENTION_OPTIONS} - sortComparator={propertyComparator('value')} + options={loadOwnerOptions} + onChange={onOwnersChange} /> </div> </StyledInputContainer> <StyledInputContainer> <div className="control-label"> - {TRANSLATIONS.WORKING_TIMEOUT_TEXT} - <span className="required">*</span> + {TRANSLATIONS.DESCRIPTION_TEXT} </div> <div className="input-container"> <input - type="number" - min="1" - name="working_timeout" - value={currentAlert?.working_timeout || ''} - placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} - onChange={onTimeoutVerifyChange} + type="text" + name="description" + value={currentAlert ? currentAlert.description || '' : ''} + placeholder={ + isReport + ? TRANSLATIONS.REPORT_DESCRIPTION_PLACEHOLDER + : TRANSLATIONS.ALERT_DESCRIPTION_PLACEHOLDER + } + onChange={onInputChange} /> - <span className="input-label">{TRANSLATIONS.SECONDS_TEXT}</span> </div> </StyledInputContainer> - {!isReport && ( - <StyledInputContainer> + <StyledSwitchContainer> + <Switch + checked={currentAlert ? currentAlert.active : false} + defaultChecked + onChange={onActiveSwitch} + /> + <div className="switch-label"> + {isReport + ? TRANSLATIONS.ACTIVE_REPORT_TEXT + : TRANSLATIONS.ACTIVE_ALERT_TEXT} + </div> + </StyledSwitchContainer> + </div> + </StyledPanel> + {!isReport && ( + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.ALERT_CONDITION_TITLE} + subtitle={TRANSLATIONS.ALERT_CONDITION_SUBTITLE} + required={false} + validateCheckStatus={validationStatus[Sections.ALERT].status} + testId="alert-condition-panel" + /> + } + key="2" + style={{ borderBottom: 'none' }} + > + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.DATABASE_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <AsyncSelect + ariaLabel={TRANSLATIONS.DATABASE_TEXT} + name="source" + placeholder={TRANSLATIONS.DATABASE_PLACEHOLDER} + value={ + currentAlert?.database?.label && + currentAlert?.database?.value + ? { + value: currentAlert.database.value, + label: currentAlert.database.label, + } + : undefined + } + options={loadSourceOptions} + onChange={onSourceChange} + /> + </div> + </StyledInputContainer> + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.SQL_QUERY_TEXT} + <StyledTooltip tooltip={TRANSLATIONS.SQL_QUERY_TOOLTIP} /> + <span className="required">*</span> + </div> + <TextAreaControl + name="sql" + language="sql" + offerEditInModal={false} + minLines={15} + maxLines={15} + onChange={onSQLChange} + readOnly={false} + initialValue={resource?.sql} + key={currentAlert?.id} + /> + </StyledInputContainer> + <div className="inline-container wrap"> + <StyledInputContainer css={no_margin_bottom}> + <div className="control-label" css={inputSpacer}> + {TRANSLATIONS.TRIGGER_ALERT_IF_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <Select + ariaLabel={TRANSLATIONS.CONDITION_TEXT} + onChange={onConditionChange} + placeholder={TRANSLATIONS.CONDITION_PLACEHOLDER} + value={currentAlert?.validator_config_json?.op || undefined} + options={CONDITIONS} + css={inputSpacer} + /> + </div> + </StyledInputContainer> + <StyledInputContainer css={no_margin_bottom}> <div className="control-label"> - {TRANSLATIONS.GRACE_PERIOD_TEXT} + {TRANSLATIONS.VALUE_TEXT} <span className="required">*</span> </div> <div className="input-container"> <input type="number" - min="1" - name="grace_period" - value={currentAlert?.grace_period || ''} - placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} - onChange={onTimeoutVerifyChange} + name="threshold" + disabled={conditionNotNull} + value={ + currentAlert?.validator_config_json?.threshold !== + undefined + ? currentAlert.validator_config_json.threshold + : '' + } + placeholder={TRANSLATIONS.VALUE_TEXT} + onChange={onThresholdChange} /> - <span className="input-label"> - {TRANSLATIONS.SECONDS_TEXT} - </span> </div> </StyledInputContainer> - )} - </div> - <div className="column message"> - <StyledSectionTitle> - <h4>{TRANSLATIONS.MESSAGE_CONTENT_TEXT}</h4> + </div> + </StyledPanel> + )} + <StyledPanel + header={ + <ValidatedPanelHeader + title={ + isReport + ? TRANSLATIONS.REPORT_CONTENTS_TITLE + : TRANSLATIONS.ALERT_CONTENTS_TITLE + } + subtitle={TRANSLATIONS.CONTENTS_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.CONTENT].status} + testId="contents-panel" Review Comment: `data-test` ########## Pipfile.lock: ########## @@ -0,0 +1,20 @@ +{ Review Comment: Could you also delete this file? ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -966,18 +1016,14 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ updateAlertState('timezone', timezone); }; - const onContentTypeChange = (event: any) => { - const { target } = event; + const onContentTypeChange = (value: any) => { Review Comment: Could we use appropriate type here instead of `any`? ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -143,62 +148,60 @@ const RETENTION_OPTIONS = [ }, ]; -const StyledModal = styled(Modal)` - max-width: 1200px; - width: 100%; +const CONTENT_TYPE_OPTIONS = [ + { + label: t('Dashboard'), + value: 'dashboard', + }, + { + label: t('Chart'), + value: 'chart', + }, +]; +const FORMAT_OPTIONS = { + png: { + label: t('Send as PNG'), + value: 'PNG', + }, + csv: { + label: t('Send as CSV'), + value: 'CSV', + }, + txt: { + label: t('Send as text'), + value: 'TEXT', + }, +}; - .ant-modal-body { - overflow: initial; - } -`; +// Style Constants +const MODAL_BODY_HEIGHT = 180.5; -const StyledTooltip = styled(InfoTooltipWithTrigger)` - margin-left: ${({ theme }) => theme.gridUnit}px; -`; +// Apply to collapse panels as 'style' prop value +const panelBorder = { + borderBottom: 'none', +}; -const StyledIcon = (theme: SupersetTheme) => css` - margin: auto ${theme.gridUnit * 2}px auto 0; - color: ${theme.colors.grayscale.base}; +// Apply to final text input components of each collapse panel +const no_margin_bottom = css` + margin-bottom: 0; `; -const StyledSectionContainer = styled.div` - display: flex; - flex-direction: column; +// Styled Components - .control-label { - margin-top: ${({ theme }) => theme.gridUnit}px; - } +/* +Height of modal body defined here, total width defined at component invocation as antd prop. - .header-section { - display: flex; - flex: 0 0 auto; - align-items: center; - width: 100%; - padding: ${({ theme }) => theme.gridUnit * 4}px; - border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; +.inline-container applies to Alert Condition panel <div> containing trigger +and value inputs. + */ +const StyledModal = styled(Modal)` + .ant-modal-body { + height: ${({ theme }) => theme.gridUnit * MODAL_BODY_HEIGHT}px; + height: ${({ theme }) => theme.gridUnit * MODAL_BODY_HEIGHT}px; Review Comment: 1. height is duplicated 2. That value is not really related to grid units. I _think_ it's fine to just hardcode it to 720px. @rusackas @geido wdyt? ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -88,7 +93,7 @@ interface AlertReportModalProps { } const DEFAULT_WORKING_TIMEOUT = 3600; -const DEFAULT_CRON_VALUE = '0 * * * *'; // every hour +const DEFAULT_CRON_VALUE = '0 0 * * *'; // every day Review Comment: I don't see the default value in CRON expression control - instead it's empty ########## superset-frontend/src/components/CronPicker/CronPicker.tsx: ########## @@ -44,7 +44,7 @@ export const LOCALE: Locale = { prefixMonths: t('in'), prefixMonthDays: t('on'), prefixWeekDays: t('on'), - prefixWeekDaysForMonthAndYearPeriod: t('and'), + prefixWeekDaysForMonthAndYearPeriod: t('or'), Review Comment: What is this change related to? I can't find it ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -255,7 +230,10 @@ const StyledSwitchContainer = styled.div` export const StyledInputContainer = styled.div` flex: 1; - margin-top: 0; + margin-top: 0px; + margin-bottom: ${({ theme }) => theme.gridUnit * 4}px; + margin-top: 0px; + margin-bottom: ${({ theme }) => theme.gridUnit * 4}px; Review Comment: duplicated styles ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -497,7 +504,51 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ const [sourceOptions, setSourceOptions] = useState<MetaObject[]>([]); const [dashboardOptions, setDashboardOptions] = useState<MetaObject[]>([]); const [chartOptions, setChartOptions] = useState<MetaObject[]>([]); + // Validation + const [validationStatus, setValidationStatus] = useState<ValidationObject>({ + generalSection: { status: false, name: 'General information', errors: [] }, Review Comment: 1. Could we change the name `status` to something more explicit, like `hasErrors`? 2. I think `name` should be translated. We probably should use `TRANSLATIONS` const for this 3. The keys names are duplicated from `Sections` enum. You can use that enum to define keys in objects like this: ``` { [Sections.GENERAL]: { ... }, [Sections.CONTENT]: { ... }, ... } ``` ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -966,18 +1016,14 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ updateAlertState('timezone', timezone); }; - const onContentTypeChange = (event: any) => { - const { target } = event; + const onContentTypeChange = (value: any) => { // When switch content type, reset force_screenshot to false setForceScreenshot(false); - // Gives time to close the select before changing the type - setTimeout(() => setContentType(target.value), 200); + setContentType(value); }; - const onFormatChange = (event: any) => { - const { target } = event; - - setReportFormat(target.value); + const onFormatChange = (value: any) => { Review Comment: Same here - `any` ########## superset-frontend/src/components/CronPicker/CronPicker.tsx: ########## @@ -110,22 +110,81 @@ export const CronPicker = styled((props: CronProps) => ( <ReactCronPicker locale={LOCALE} {...props} /> </ConfigProvider> ))` - .react-js-cron-field { - margin-bottom: 0px; +:has(.react-js-cron-months){ + display: grid !important; + grid-template-columns: repeat(2, 50%); + grid-column-gap: ${({ theme }) => theme.gridUnit}px; + grid-row-gap: ${({ theme }) => theme.gridUnit * 2}px; + div:has(.react-js-cron-hours) { + grid-column: span 2; + display: flex; + justify-content: space-between; + .react-js-cron-field { + width:50%; + } } - .react-js-cron-select:not(.react-js-cron-custom-select) > div:first-of-type, - .react-js-cron-custom-select { - border-radius: ${({ theme }) => theme.gridUnit}px; - background-color: ${({ theme }) => - theme.colors.grayscale.light4} !important; +} + +:not(:has(.react-js-cron-months)) { + display: grid; + grid-template-columns: repeat(2, 50%); + grid-column-gap: ${({ theme }) => theme.gridUnit}px; + grid-row-gap: ${({ theme }) => theme.gridUnit * 2}px; + .react-js-cron-period { + grid-column: span 2; } - .react-js-cron-custom-select > div:first-of-type { - border-radius: ${({ theme }) => theme.gridUnit}px; + div:has(.react-js-cron-hours) { + grid-column: span 2; + display: flex; + justify-content: space-between; + .react-js-cron-field { + width: 50%; + } } +} + +:not(:has(.react-js-cron-month-days)) { + .react-js-cron-week-days { + grid-column: span 2; + } +} + +.react-js-cron-minutes > span { + padding-left: ${({ theme }) => theme.gridUnit}px; +} +grid-template-columns: repeat(1, 100%); +grid-column-gap: ${({ theme }) => theme.gridUnit}px; +grid-row-gap: ${({ theme }) => theme.gridUnit * 2}px; +div:has(.react-js-cron-hours) { + width: 100%; +} +:not(div:has(.react-js-cron-hours)) { + display: flex; + flex-wrap: nowrap; +} +.react-js-cron-select { + width: 100%; + .ant-select-selector { + flex-wrap: nowrap !important; Review Comment: Instead of using `!important` we could try to increase specificity of the css selector (for example `&.ant-select-selector` or `&&.ant-select-selector` which translates to `.react-js-cron-select.react-js-cron-select.ant-select-selector`) ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -1190,367 +1313,456 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ </h4> } > - <StyledSectionContainer> - <div className="header-section"> - <StyledInputContainer> - <div className="control-label"> - {isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="text" - name="name" - value={currentAlert ? currentAlert.name : ''} - placeholder={ - isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT - } - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.OWNERS_TEXT} - <span className="required">*</span> - </div> - <div data-test="owners-select" className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.OWNERS_TEXT} - allowClear - name="owners" - mode="multiple" - value={ - (currentAlert?.owners as { - label: string; - value: number; - }[]) || [] - } - options={loadOwnerOptions} - onChange={onOwnersChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label">{TRANSLATIONS.DESCRIPTION_TEXT}</div> - <div className="input-container"> - <input - type="text" - name="description" - value={currentAlert ? currentAlert.description || '' : ''} - placeholder={TRANSLATIONS.DESCRIPTION_TEXT} - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledSwitchContainer> - <Switch - onChange={onActiveSwitch} - checked={currentAlert ? currentAlert.active : true} + <Collapse + expandIconPosition="right" + defaultActiveKey="1" + accordion + style={{ border: 'none' }} + > + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.GENERAL_TITLE} + subtitle={TRANSLATIONS.GENERAL_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.GENERAL].status} + testId="general-information-panel" /> - <div className="switch-label">{TRANSLATIONS.ACTIVE_TEXT}</div> - </StyledSwitchContainer> - </div> - <div className="column-section"> - {!isReport && ( - <div className="column condition"> - <StyledSectionTitle> - <h4>{TRANSLATIONS.ALERT_CONDITION_TEXT}</h4> - </StyledSectionTitle> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.DATABASE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.DATABASE_TEXT} - name="source" - value={ - currentAlert?.database?.label && - currentAlert?.database?.value - ? { - value: currentAlert.database.value, - label: currentAlert.database.label, - } - : undefined - } - options={loadSourceOptions} - onChange={onSourceChange} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.SQL_QUERY_TEXT} - <StyledTooltip tooltip={TRANSLATIONS.SQL_QUERY_TOOLTIP} /> - <span className="required">*</span> - </div> - <TextAreaControl - name="sql" - language="sql" - offerEditInModal={false} - minLines={15} - maxLines={15} - onChange={onSQLChange} - readOnly={false} - initialValue={resource?.sql} - key={currentAlert?.id} + } + key="1" Review Comment: Could you change keys from "1,2,3" to more explicit names? ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -1001,30 +1047,102 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ return hasInfo; }; - const validate = () => { + const validateGeneralSection = () => { + const errors = []; + if (!currentAlert?.name?.length) { + errors.push('name'); Review Comment: I think errors should be translated too ########## superset-frontend/src/features/alerts/types.ts: ########## @@ -123,3 +123,25 @@ export interface AlertsReportsConfig { ALERT_REPORTS_DEFAULT_RETENTION: number; ALERT_REPORTS_DEFAULT_CRON_VALUE: string; } + +export type SectionValidationObject = { + status: boolean; + errors: string[]; + name: string; +}; + +export interface ValidationObject { + generalSection: SectionValidationObject; + contentSection: SectionValidationObject; + alertConditionSection: SectionValidationObject; + scheduleSection: SectionValidationObject; + notificationSection: SectionValidationObject; +} + +export enum Sections { + GENERAL = 'generalSection', Review Comment: We're very close to merging a [PR](https://github.com/apache/superset/pull/26875) which enforces PascalCase for enums. Could you rename those here so that we prevent conflicts? ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -1190,367 +1313,456 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ </h4> } > - <StyledSectionContainer> - <div className="header-section"> - <StyledInputContainer> - <div className="control-label"> - {isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="text" - name="name" - value={currentAlert ? currentAlert.name : ''} - placeholder={ - isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT - } - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.OWNERS_TEXT} - <span className="required">*</span> - </div> - <div data-test="owners-select" className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.OWNERS_TEXT} - allowClear - name="owners" - mode="multiple" - value={ - (currentAlert?.owners as { - label: string; - value: number; - }[]) || [] - } - options={loadOwnerOptions} - onChange={onOwnersChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label">{TRANSLATIONS.DESCRIPTION_TEXT}</div> - <div className="input-container"> - <input - type="text" - name="description" - value={currentAlert ? currentAlert.description || '' : ''} - placeholder={TRANSLATIONS.DESCRIPTION_TEXT} - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledSwitchContainer> - <Switch - onChange={onActiveSwitch} - checked={currentAlert ? currentAlert.active : true} + <Collapse + expandIconPosition="right" + defaultActiveKey="1" + accordion + style={{ border: 'none' }} + > + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.GENERAL_TITLE} + subtitle={TRANSLATIONS.GENERAL_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.GENERAL].status} + testId="general-information-panel" /> - <div className="switch-label">{TRANSLATIONS.ACTIVE_TEXT}</div> - </StyledSwitchContainer> - </div> - <div className="column-section"> - {!isReport && ( - <div className="column condition"> - <StyledSectionTitle> - <h4>{TRANSLATIONS.ALERT_CONDITION_TEXT}</h4> - </StyledSectionTitle> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.DATABASE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.DATABASE_TEXT} - name="source" - value={ - currentAlert?.database?.label && - currentAlert?.database?.value - ? { - value: currentAlert.database.value, - label: currentAlert.database.label, - } - : undefined - } - options={loadSourceOptions} - onChange={onSourceChange} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.SQL_QUERY_TEXT} - <StyledTooltip tooltip={TRANSLATIONS.SQL_QUERY_TOOLTIP} /> - <span className="required">*</span> - </div> - <TextAreaControl - name="sql" - language="sql" - offerEditInModal={false} - minLines={15} - maxLines={15} - onChange={onSQLChange} - readOnly={false} - initialValue={resource?.sql} - key={currentAlert?.id} + } + key="1" + style={panelBorder} + > + <div className="header-section"> + <StyledInputContainer> + <div className="control-label"> + {isReport + ? TRANSLATIONS.REPORT_NAME_TEXT + : TRANSLATIONS.ALERT_NAME_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <input + type="text" + name="name" + value={currentAlert ? currentAlert.name : ''} + placeholder={ + isReport + ? TRANSLATIONS.REPORT_NAME_PLACEHOLDER + : TRANSLATIONS.ALERT_NAME_PLACEHOLDER + } + onChange={onInputChange} /> - </StyledInputContainer> - <div className="inline-container wrap"> - <StyledInputContainer> - <div className="control-label" css={inputSpacer}> - {TRANSLATIONS.TRIGGER_ALERT_IF_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <Select - ariaLabel={TRANSLATIONS.CONDITION_TEXT} - onChange={onConditionChange} - placeholder="Condition" - value={ - currentAlert?.validator_config_json?.op || undefined - } - options={CONDITIONS} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.VALUE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="number" - name="threshold" - disabled={conditionNotNull} - value={ - currentAlert?.validator_config_json?.threshold !== - undefined - ? currentAlert.validator_config_json.threshold - : '' - } - placeholder={TRANSLATIONS.VALUE_TEXT} - onChange={onThresholdChange} - /> - </div> - </StyledInputContainer> </div> - </div> - )} - <div className="column schedule"> - <StyledSectionTitle> - <h4> - {isReport - ? TRANSLATIONS.REPORT_SCHEDULE_TEXT - : TRANSLATIONS.ALERT_CONDITION_SCHEDULE_TEXT} - </h4> - <span className="required">*</span> - </StyledSectionTitle> - <AlertReportCronScheduler - value={currentAlert?.crontab || ALERT_REPORTS_DEFAULT_CRON_VALUE} - onChange={newVal => updateAlertState('crontab', newVal)} - /> - <div className="control-label">{TRANSLATIONS.TIMEZONE_TEXT}</div> - <div - className="input-container" - css={(theme: SupersetTheme) => timezoneHeaderStyle(theme)} - > - <TimezoneSelector - onTimezoneChange={onTimezoneChange} - timezone={currentAlert?.timezone} - minWidth="100%" - /> - </div> - <StyledSectionTitle> - <h4>{TRANSLATIONS.SCHEDULE_SETTINGS_TEXT}</h4> - </StyledSectionTitle> + </StyledInputContainer> <StyledInputContainer> <div className="control-label"> - {TRANSLATIONS.LOG_RETENTION_TEXT} + {TRANSLATIONS.OWNERS_TEXT} <span className="required">*</span> </div> - <div className="input-container"> - <Select - ariaLabel={TRANSLATIONS.LOG_RETENTION_TEXT} - placeholder={TRANSLATIONS.LOG_RETENTION_TEXT} - onChange={onLogRetentionChange} + <div data-test="owners-select" className="input-container"> + <AsyncSelect + ariaLabel={TRANSLATIONS.OWNERS_TEXT} + allowClear + name="owners" + mode="multiple" + placeholder={TRANSLATIONS.OWNERS_PLACEHOLDER} value={ - typeof currentAlert?.log_retention === 'number' - ? currentAlert?.log_retention - : ALERT_REPORTS_DEFAULT_RETENTION + (currentAlert?.owners as { + label: string; + value: number; + }[]) || [] } - options={RETENTION_OPTIONS} - sortComparator={propertyComparator('value')} + options={loadOwnerOptions} + onChange={onOwnersChange} /> </div> </StyledInputContainer> <StyledInputContainer> <div className="control-label"> - {TRANSLATIONS.WORKING_TIMEOUT_TEXT} - <span className="required">*</span> + {TRANSLATIONS.DESCRIPTION_TEXT} </div> <div className="input-container"> <input - type="number" - min="1" - name="working_timeout" - value={currentAlert?.working_timeout || ''} - placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} - onChange={onTimeoutVerifyChange} + type="text" + name="description" + value={currentAlert ? currentAlert.description || '' : ''} + placeholder={ + isReport + ? TRANSLATIONS.REPORT_DESCRIPTION_PLACEHOLDER + : TRANSLATIONS.ALERT_DESCRIPTION_PLACEHOLDER + } + onChange={onInputChange} /> - <span className="input-label">{TRANSLATIONS.SECONDS_TEXT}</span> </div> </StyledInputContainer> - {!isReport && ( - <StyledInputContainer> + <StyledSwitchContainer> + <Switch + checked={currentAlert ? currentAlert.active : false} + defaultChecked + onChange={onActiveSwitch} + /> + <div className="switch-label"> + {isReport + ? TRANSLATIONS.ACTIVE_REPORT_TEXT + : TRANSLATIONS.ACTIVE_ALERT_TEXT} + </div> + </StyledSwitchContainer> + </div> + </StyledPanel> + {!isReport && ( + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.ALERT_CONDITION_TITLE} + subtitle={TRANSLATIONS.ALERT_CONDITION_SUBTITLE} + required={false} + validateCheckStatus={validationStatus[Sections.ALERT].status} + testId="alert-condition-panel" + /> + } + key="2" + style={{ borderBottom: 'none' }} Review Comment: We should avoid using `style`. Instead you could use `css` prop, like this: ``` <Styled Panel ... css={css` border-bottom: none; `} /> ``` (`css` can be imported from `@superset-core/ui`) ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -1190,367 +1313,456 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ </h4> } > - <StyledSectionContainer> - <div className="header-section"> - <StyledInputContainer> - <div className="control-label"> - {isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="text" - name="name" - value={currentAlert ? currentAlert.name : ''} - placeholder={ - isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT - } - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.OWNERS_TEXT} - <span className="required">*</span> - </div> - <div data-test="owners-select" className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.OWNERS_TEXT} - allowClear - name="owners" - mode="multiple" - value={ - (currentAlert?.owners as { - label: string; - value: number; - }[]) || [] - } - options={loadOwnerOptions} - onChange={onOwnersChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label">{TRANSLATIONS.DESCRIPTION_TEXT}</div> - <div className="input-container"> - <input - type="text" - name="description" - value={currentAlert ? currentAlert.description || '' : ''} - placeholder={TRANSLATIONS.DESCRIPTION_TEXT} - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledSwitchContainer> - <Switch - onChange={onActiveSwitch} - checked={currentAlert ? currentAlert.active : true} + <Collapse + expandIconPosition="right" + defaultActiveKey="1" + accordion + style={{ border: 'none' }} + > + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.GENERAL_TITLE} + subtitle={TRANSLATIONS.GENERAL_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.GENERAL].status} + testId="general-information-panel" /> - <div className="switch-label">{TRANSLATIONS.ACTIVE_TEXT}</div> - </StyledSwitchContainer> - </div> - <div className="column-section"> - {!isReport && ( - <div className="column condition"> - <StyledSectionTitle> - <h4>{TRANSLATIONS.ALERT_CONDITION_TEXT}</h4> - </StyledSectionTitle> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.DATABASE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.DATABASE_TEXT} - name="source" - value={ - currentAlert?.database?.label && - currentAlert?.database?.value - ? { - value: currentAlert.database.value, - label: currentAlert.database.label, - } - : undefined - } - options={loadSourceOptions} - onChange={onSourceChange} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.SQL_QUERY_TEXT} - <StyledTooltip tooltip={TRANSLATIONS.SQL_QUERY_TOOLTIP} /> - <span className="required">*</span> - </div> - <TextAreaControl - name="sql" - language="sql" - offerEditInModal={false} - minLines={15} - maxLines={15} - onChange={onSQLChange} - readOnly={false} - initialValue={resource?.sql} - key={currentAlert?.id} + } + key="1" + style={panelBorder} + > + <div className="header-section"> + <StyledInputContainer> + <div className="control-label"> + {isReport + ? TRANSLATIONS.REPORT_NAME_TEXT + : TRANSLATIONS.ALERT_NAME_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <input + type="text" + name="name" + value={currentAlert ? currentAlert.name : ''} + placeholder={ + isReport + ? TRANSLATIONS.REPORT_NAME_PLACEHOLDER + : TRANSLATIONS.ALERT_NAME_PLACEHOLDER + } + onChange={onInputChange} /> - </StyledInputContainer> - <div className="inline-container wrap"> - <StyledInputContainer> - <div className="control-label" css={inputSpacer}> - {TRANSLATIONS.TRIGGER_ALERT_IF_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <Select - ariaLabel={TRANSLATIONS.CONDITION_TEXT} - onChange={onConditionChange} - placeholder="Condition" - value={ - currentAlert?.validator_config_json?.op || undefined - } - options={CONDITIONS} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.VALUE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="number" - name="threshold" - disabled={conditionNotNull} - value={ - currentAlert?.validator_config_json?.threshold !== - undefined - ? currentAlert.validator_config_json.threshold - : '' - } - placeholder={TRANSLATIONS.VALUE_TEXT} - onChange={onThresholdChange} - /> - </div> - </StyledInputContainer> </div> - </div> - )} - <div className="column schedule"> - <StyledSectionTitle> - <h4> - {isReport - ? TRANSLATIONS.REPORT_SCHEDULE_TEXT - : TRANSLATIONS.ALERT_CONDITION_SCHEDULE_TEXT} - </h4> - <span className="required">*</span> - </StyledSectionTitle> - <AlertReportCronScheduler - value={currentAlert?.crontab || ALERT_REPORTS_DEFAULT_CRON_VALUE} - onChange={newVal => updateAlertState('crontab', newVal)} - /> - <div className="control-label">{TRANSLATIONS.TIMEZONE_TEXT}</div> - <div - className="input-container" - css={(theme: SupersetTheme) => timezoneHeaderStyle(theme)} - > - <TimezoneSelector - onTimezoneChange={onTimezoneChange} - timezone={currentAlert?.timezone} - minWidth="100%" - /> - </div> - <StyledSectionTitle> - <h4>{TRANSLATIONS.SCHEDULE_SETTINGS_TEXT}</h4> - </StyledSectionTitle> + </StyledInputContainer> <StyledInputContainer> <div className="control-label"> - {TRANSLATIONS.LOG_RETENTION_TEXT} + {TRANSLATIONS.OWNERS_TEXT} <span className="required">*</span> </div> - <div className="input-container"> - <Select - ariaLabel={TRANSLATIONS.LOG_RETENTION_TEXT} - placeholder={TRANSLATIONS.LOG_RETENTION_TEXT} - onChange={onLogRetentionChange} + <div data-test="owners-select" className="input-container"> + <AsyncSelect + ariaLabel={TRANSLATIONS.OWNERS_TEXT} + allowClear + name="owners" + mode="multiple" + placeholder={TRANSLATIONS.OWNERS_PLACEHOLDER} value={ - typeof currentAlert?.log_retention === 'number' - ? currentAlert?.log_retention - : ALERT_REPORTS_DEFAULT_RETENTION + (currentAlert?.owners as { + label: string; + value: number; + }[]) || [] } - options={RETENTION_OPTIONS} - sortComparator={propertyComparator('value')} + options={loadOwnerOptions} + onChange={onOwnersChange} /> </div> </StyledInputContainer> <StyledInputContainer> <div className="control-label"> - {TRANSLATIONS.WORKING_TIMEOUT_TEXT} - <span className="required">*</span> + {TRANSLATIONS.DESCRIPTION_TEXT} </div> <div className="input-container"> <input - type="number" - min="1" - name="working_timeout" - value={currentAlert?.working_timeout || ''} - placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} - onChange={onTimeoutVerifyChange} + type="text" + name="description" + value={currentAlert ? currentAlert.description || '' : ''} + placeholder={ + isReport + ? TRANSLATIONS.REPORT_DESCRIPTION_PLACEHOLDER + : TRANSLATIONS.ALERT_DESCRIPTION_PLACEHOLDER + } + onChange={onInputChange} /> - <span className="input-label">{TRANSLATIONS.SECONDS_TEXT}</span> </div> </StyledInputContainer> - {!isReport && ( - <StyledInputContainer> + <StyledSwitchContainer> + <Switch + checked={currentAlert ? currentAlert.active : false} + defaultChecked + onChange={onActiveSwitch} + /> + <div className="switch-label"> + {isReport + ? TRANSLATIONS.ACTIVE_REPORT_TEXT + : TRANSLATIONS.ACTIVE_ALERT_TEXT} + </div> + </StyledSwitchContainer> + </div> + </StyledPanel> + {!isReport && ( + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.ALERT_CONDITION_TITLE} + subtitle={TRANSLATIONS.ALERT_CONDITION_SUBTITLE} + required={false} + validateCheckStatus={validationStatus[Sections.ALERT].status} + testId="alert-condition-panel" Review Comment: here `data-test` too ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -1190,367 +1313,456 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ </h4> } > - <StyledSectionContainer> - <div className="header-section"> - <StyledInputContainer> - <div className="control-label"> - {isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="text" - name="name" - value={currentAlert ? currentAlert.name : ''} - placeholder={ - isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT - } - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.OWNERS_TEXT} - <span className="required">*</span> - </div> - <div data-test="owners-select" className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.OWNERS_TEXT} - allowClear - name="owners" - mode="multiple" - value={ - (currentAlert?.owners as { - label: string; - value: number; - }[]) || [] - } - options={loadOwnerOptions} - onChange={onOwnersChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label">{TRANSLATIONS.DESCRIPTION_TEXT}</div> - <div className="input-container"> - <input - type="text" - name="description" - value={currentAlert ? currentAlert.description || '' : ''} - placeholder={TRANSLATIONS.DESCRIPTION_TEXT} - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledSwitchContainer> - <Switch - onChange={onActiveSwitch} - checked={currentAlert ? currentAlert.active : true} + <Collapse + expandIconPosition="right" + defaultActiveKey="1" + accordion + style={{ border: 'none' }} + > + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.GENERAL_TITLE} + subtitle={TRANSLATIONS.GENERAL_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.GENERAL].status} + testId="general-information-panel" Review Comment: we're using `data-test` to mark test ids ########## superset-frontend/src/features/alerts/components/NotificationMethod.tsx: ########## @@ -133,25 +136,29 @@ export const NotificationMethod: FunctionComponent<NotificationMethodProps> = ({ )} value={method} /> + {method !== undefined && index !== 0 && !!onRemove ? ( Review Comment: 1. `index !== 0` why do we prevent user from deleting the first item? 2. I don't see "Add notification method" button anymore 🤔 <img width="756" alt="image" src="https://github.com/apache/superset/assets/15073128/80444de7-435e-4d20-a09c-c68c16332441"> ########## superset-frontend/src/features/alerts/buildErrorTooltipMessage.tsx: ########## @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 React from 'react'; +import { styled } from '@superset-ui/core'; +import { ValidationObject } from './types'; + +const StyledList = styled.ul` + margin: 0; + padding: 0; + li { + padding-left: 12px; + } +`; + +export const buildErrorTooltipMessage = ( + build = true, + setErrorTooltipMessage: Function, + validationStatus: ValidationObject, +) => { + if (build) { + const sectionErrors: string[] = []; + Object.values(validationStatus).forEach(validationData => { + if (!validationData.status) { + const sectionTitle = `${validationData.name}: `; + sectionErrors.push(sectionTitle + validationData.errors.join(', ')); + } + }); + setErrorTooltipMessage( + <div> + Not all required fields are complete. Please provide the following: + <StyledList> Review Comment: Dots in the list are too far left <img width="286" alt="image" src="https://github.com/apache/superset/assets/15073128/cadc7fe1-1d97-422c-8ced-854acc657ebd"> ########## superset-frontend/src/features/alerts/buildErrorTooltipMessage.tsx: ########## @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 React from 'react'; +import { styled } from '@superset-ui/core'; +import { ValidationObject } from './types'; + +const StyledList = styled.ul` + margin: 0; + padding: 0; + li { + padding-left: 12px; + } +`; + +export const buildErrorTooltipMessage = ( + build = true, + setErrorTooltipMessage: Function, + validationStatus: ValidationObject, +) => { + if (build) { + const sectionErrors: string[] = []; + Object.values(validationStatus).forEach(validationData => { + if (!validationData.status) { + const sectionTitle = `${validationData.name}: `; + sectionErrors.push(sectionTitle + validationData.errors.join(', ')); + } + }); + setErrorTooltipMessage( + <div> + Not all required fields are complete. Please provide the following: Review Comment: Translation ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -143,62 +148,60 @@ const RETENTION_OPTIONS = [ }, ]; -const StyledModal = styled(Modal)` - max-width: 1200px; - width: 100%; +const CONTENT_TYPE_OPTIONS = [ + { + label: t('Dashboard'), + value: 'dashboard', + }, + { + label: t('Chart'), + value: 'chart', + }, +]; +const FORMAT_OPTIONS = { + png: { + label: t('Send as PNG'), + value: 'PNG', + }, + csv: { + label: t('Send as CSV'), + value: 'CSV', + }, + txt: { + label: t('Send as text'), + value: 'TEXT', + }, +}; - .ant-modal-body { - overflow: initial; - } -`; +// Style Constants +const MODAL_BODY_HEIGHT = 180.5; -const StyledTooltip = styled(InfoTooltipWithTrigger)` - margin-left: ${({ theme }) => theme.gridUnit}px; -`; +// Apply to collapse panels as 'style' prop value +const panelBorder = { + borderBottom: 'none', +}; -const StyledIcon = (theme: SupersetTheme) => css` - margin: auto ${theme.gridUnit * 2}px auto 0; - color: ${theme.colors.grayscale.base}; +// Apply to final text input components of each collapse panel +const no_margin_bottom = css` Review Comment: We should use consistent naming conventions (though that's something that we fail to do in so many cases in Superset 😄). I'd suggest renaming it to `noMarginBottom` ########## superset-frontend/src/features/alerts/components/ValidatedPanelHeader.tsx: ########## @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 React from 'react'; +import { t } from '@superset-ui/core'; +import { CheckCircleOutlined } from '@ant-design/icons'; + +const ValidatedPanelHeader = ({ + title, + subtitle, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + required, + validateCheckStatus, + testId, +}: { + title: string; + subtitle: string; + required: boolean; + validateCheckStatus: boolean; + testId?: string; +}): JSX.Element => { + const asterisk = ' *'; + const checkmark = <CheckCircleOutlined />; Review Comment: I don't think we need to extract asterisk and checkmark to constants ########## superset-frontend/src/features/alerts/buildErrorTooltipMessage.tsx: ########## @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 React from 'react'; +import { styled } from '@superset-ui/core'; +import { ValidationObject } from './types'; + +const StyledList = styled.ul` + margin: 0; + padding: 0; + li { + padding-left: 12px; Review Comment: `theme.gridUnit * 3` ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -1190,367 +1313,456 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ </h4> } > - <StyledSectionContainer> - <div className="header-section"> - <StyledInputContainer> - <div className="control-label"> - {isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="text" - name="name" - value={currentAlert ? currentAlert.name : ''} - placeholder={ - isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT - } - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.OWNERS_TEXT} - <span className="required">*</span> - </div> - <div data-test="owners-select" className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.OWNERS_TEXT} - allowClear - name="owners" - mode="multiple" - value={ - (currentAlert?.owners as { - label: string; - value: number; - }[]) || [] - } - options={loadOwnerOptions} - onChange={onOwnersChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label">{TRANSLATIONS.DESCRIPTION_TEXT}</div> - <div className="input-container"> - <input - type="text" - name="description" - value={currentAlert ? currentAlert.description || '' : ''} - placeholder={TRANSLATIONS.DESCRIPTION_TEXT} - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledSwitchContainer> - <Switch - onChange={onActiveSwitch} - checked={currentAlert ? currentAlert.active : true} + <Collapse + expandIconPosition="right" + defaultActiveKey="1" + accordion + style={{ border: 'none' }} + > + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.GENERAL_TITLE} + subtitle={TRANSLATIONS.GENERAL_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.GENERAL].status} + testId="general-information-panel" /> - <div className="switch-label">{TRANSLATIONS.ACTIVE_TEXT}</div> - </StyledSwitchContainer> - </div> - <div className="column-section"> - {!isReport && ( - <div className="column condition"> - <StyledSectionTitle> - <h4>{TRANSLATIONS.ALERT_CONDITION_TEXT}</h4> - </StyledSectionTitle> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.DATABASE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.DATABASE_TEXT} - name="source" - value={ - currentAlert?.database?.label && - currentAlert?.database?.value - ? { - value: currentAlert.database.value, - label: currentAlert.database.label, - } - : undefined - } - options={loadSourceOptions} - onChange={onSourceChange} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.SQL_QUERY_TEXT} - <StyledTooltip tooltip={TRANSLATIONS.SQL_QUERY_TOOLTIP} /> - <span className="required">*</span> - </div> - <TextAreaControl - name="sql" - language="sql" - offerEditInModal={false} - minLines={15} - maxLines={15} - onChange={onSQLChange} - readOnly={false} - initialValue={resource?.sql} - key={currentAlert?.id} + } + key="1" + style={panelBorder} + > + <div className="header-section"> + <StyledInputContainer> + <div className="control-label"> + {isReport + ? TRANSLATIONS.REPORT_NAME_TEXT + : TRANSLATIONS.ALERT_NAME_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <input + type="text" + name="name" + value={currentAlert ? currentAlert.name : ''} + placeholder={ + isReport + ? TRANSLATIONS.REPORT_NAME_PLACEHOLDER + : TRANSLATIONS.ALERT_NAME_PLACEHOLDER + } + onChange={onInputChange} /> - </StyledInputContainer> - <div className="inline-container wrap"> - <StyledInputContainer> - <div className="control-label" css={inputSpacer}> - {TRANSLATIONS.TRIGGER_ALERT_IF_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <Select - ariaLabel={TRANSLATIONS.CONDITION_TEXT} - onChange={onConditionChange} - placeholder="Condition" - value={ - currentAlert?.validator_config_json?.op || undefined - } - options={CONDITIONS} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.VALUE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="number" - name="threshold" - disabled={conditionNotNull} - value={ - currentAlert?.validator_config_json?.threshold !== - undefined - ? currentAlert.validator_config_json.threshold - : '' - } - placeholder={TRANSLATIONS.VALUE_TEXT} - onChange={onThresholdChange} - /> - </div> - </StyledInputContainer> </div> - </div> - )} - <div className="column schedule"> - <StyledSectionTitle> - <h4> - {isReport - ? TRANSLATIONS.REPORT_SCHEDULE_TEXT - : TRANSLATIONS.ALERT_CONDITION_SCHEDULE_TEXT} - </h4> - <span className="required">*</span> - </StyledSectionTitle> - <AlertReportCronScheduler - value={currentAlert?.crontab || ALERT_REPORTS_DEFAULT_CRON_VALUE} - onChange={newVal => updateAlertState('crontab', newVal)} - /> - <div className="control-label">{TRANSLATIONS.TIMEZONE_TEXT}</div> - <div - className="input-container" - css={(theme: SupersetTheme) => timezoneHeaderStyle(theme)} - > - <TimezoneSelector - onTimezoneChange={onTimezoneChange} - timezone={currentAlert?.timezone} - minWidth="100%" - /> - </div> - <StyledSectionTitle> - <h4>{TRANSLATIONS.SCHEDULE_SETTINGS_TEXT}</h4> - </StyledSectionTitle> + </StyledInputContainer> <StyledInputContainer> <div className="control-label"> - {TRANSLATIONS.LOG_RETENTION_TEXT} + {TRANSLATIONS.OWNERS_TEXT} <span className="required">*</span> </div> - <div className="input-container"> - <Select - ariaLabel={TRANSLATIONS.LOG_RETENTION_TEXT} - placeholder={TRANSLATIONS.LOG_RETENTION_TEXT} - onChange={onLogRetentionChange} + <div data-test="owners-select" className="input-container"> + <AsyncSelect + ariaLabel={TRANSLATIONS.OWNERS_TEXT} + allowClear + name="owners" + mode="multiple" + placeholder={TRANSLATIONS.OWNERS_PLACEHOLDER} value={ - typeof currentAlert?.log_retention === 'number' - ? currentAlert?.log_retention - : ALERT_REPORTS_DEFAULT_RETENTION + (currentAlert?.owners as { + label: string; + value: number; + }[]) || [] } - options={RETENTION_OPTIONS} - sortComparator={propertyComparator('value')} + options={loadOwnerOptions} + onChange={onOwnersChange} /> </div> </StyledInputContainer> <StyledInputContainer> <div className="control-label"> - {TRANSLATIONS.WORKING_TIMEOUT_TEXT} - <span className="required">*</span> + {TRANSLATIONS.DESCRIPTION_TEXT} </div> <div className="input-container"> <input - type="number" - min="1" - name="working_timeout" - value={currentAlert?.working_timeout || ''} - placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} - onChange={onTimeoutVerifyChange} + type="text" + name="description" + value={currentAlert ? currentAlert.description || '' : ''} + placeholder={ + isReport + ? TRANSLATIONS.REPORT_DESCRIPTION_PLACEHOLDER + : TRANSLATIONS.ALERT_DESCRIPTION_PLACEHOLDER + } + onChange={onInputChange} /> - <span className="input-label">{TRANSLATIONS.SECONDS_TEXT}</span> </div> </StyledInputContainer> - {!isReport && ( - <StyledInputContainer> + <StyledSwitchContainer> + <Switch + checked={currentAlert ? currentAlert.active : false} + defaultChecked + onChange={onActiveSwitch} + /> + <div className="switch-label"> + {isReport + ? TRANSLATIONS.ACTIVE_REPORT_TEXT + : TRANSLATIONS.ACTIVE_ALERT_TEXT} + </div> + </StyledSwitchContainer> + </div> + </StyledPanel> + {!isReport && ( + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.ALERT_CONDITION_TITLE} + subtitle={TRANSLATIONS.ALERT_CONDITION_SUBTITLE} + required={false} + validateCheckStatus={validationStatus[Sections.ALERT].status} + testId="alert-condition-panel" + /> + } + key="2" + style={{ borderBottom: 'none' }} + > + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.DATABASE_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <AsyncSelect + ariaLabel={TRANSLATIONS.DATABASE_TEXT} + name="source" + placeholder={TRANSLATIONS.DATABASE_PLACEHOLDER} + value={ + currentAlert?.database?.label && + currentAlert?.database?.value + ? { + value: currentAlert.database.value, + label: currentAlert.database.label, + } + : undefined + } + options={loadSourceOptions} + onChange={onSourceChange} + /> + </div> + </StyledInputContainer> + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.SQL_QUERY_TEXT} + <StyledTooltip tooltip={TRANSLATIONS.SQL_QUERY_TOOLTIP} /> + <span className="required">*</span> + </div> + <TextAreaControl + name="sql" + language="sql" + offerEditInModal={false} + minLines={15} + maxLines={15} + onChange={onSQLChange} + readOnly={false} + initialValue={resource?.sql} + key={currentAlert?.id} + /> + </StyledInputContainer> + <div className="inline-container wrap"> + <StyledInputContainer css={no_margin_bottom}> + <div className="control-label" css={inputSpacer}> + {TRANSLATIONS.TRIGGER_ALERT_IF_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <Select + ariaLabel={TRANSLATIONS.CONDITION_TEXT} + onChange={onConditionChange} + placeholder={TRANSLATIONS.CONDITION_PLACEHOLDER} + value={currentAlert?.validator_config_json?.op || undefined} + options={CONDITIONS} + css={inputSpacer} + /> + </div> + </StyledInputContainer> + <StyledInputContainer css={no_margin_bottom}> <div className="control-label"> - {TRANSLATIONS.GRACE_PERIOD_TEXT} + {TRANSLATIONS.VALUE_TEXT} <span className="required">*</span> </div> <div className="input-container"> <input type="number" - min="1" - name="grace_period" - value={currentAlert?.grace_period || ''} - placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} - onChange={onTimeoutVerifyChange} + name="threshold" + disabled={conditionNotNull} + value={ + currentAlert?.validator_config_json?.threshold !== + undefined + ? currentAlert.validator_config_json.threshold + : '' + } + placeholder={TRANSLATIONS.VALUE_TEXT} + onChange={onThresholdChange} /> - <span className="input-label"> - {TRANSLATIONS.SECONDS_TEXT} - </span> </div> </StyledInputContainer> - )} - </div> - <div className="column message"> - <StyledSectionTitle> - <h4>{TRANSLATIONS.MESSAGE_CONTENT_TEXT}</h4> + </div> + </StyledPanel> + )} + <StyledPanel + header={ + <ValidatedPanelHeader + title={ + isReport + ? TRANSLATIONS.REPORT_CONTENTS_TITLE + : TRANSLATIONS.ALERT_CONTENTS_TITLE + } + subtitle={TRANSLATIONS.CONTENTS_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.CONTENT].status} + testId="contents-panel" + /> + } + key="3" + style={{ borderBottom: 'none' }} + > + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.CONTENT_TYPE_LABEL} <span className="required">*</span> - </StyledSectionTitle> - <Radio.Group onChange={onContentTypeChange} value={contentType}> - <StyledRadio value="dashboard"> - {TRANSLATIONS.DASHBOARD_TEXT} - </StyledRadio> - <StyledRadio value="chart">{TRANSLATIONS.CHART_TEXT}</StyledRadio> - </Radio.Group> + </div> + <Select + ariaLabel={TRANSLATIONS.CONTENT_TYPE_PLACEHOLDER} + onChange={onContentTypeChange} + value={contentType} + options={CONTENT_TYPE_OPTIONS} + placeholder={TRANSLATIONS.CONTENT_TYPE_PLACEHOLDER} + /> + </StyledInputContainer> + <StyledInputContainer> {contentType === 'chart' ? ( - <AsyncSelect - ariaLabel={TRANSLATIONS.CHART_TEXT} - name="chart" - value={ - currentAlert?.chart?.label && currentAlert?.chart?.value - ? { - value: currentAlert.chart.value, - label: currentAlert.chart.label, - } - : undefined - } - options={loadChartOptions} - onChange={onChartChange} - /> + <> + <div className="control-label"> + {TRANSLATIONS.SELECT_CHART_LABEL} + <span className="required">*</span> + </div> + <AsyncSelect + ariaLabel={TRANSLATIONS.CHART_TEXT} + name="chart" + value={ + currentAlert?.chart?.label && currentAlert?.chart?.value + ? { + value: currentAlert.chart.value, + label: currentAlert.chart.label, + } + : undefined + } + options={loadChartOptions} + onChange={onChartChange} + placeholder={TRANSLATIONS.SELECT_CHART_PLACEHOLDER} + /> + </> ) : ( - <AsyncSelect - ariaLabel={TRANSLATIONS.DASHBOARD_TEXT} - name="dashboard" - value={ - currentAlert?.dashboard?.label && - currentAlert?.dashboard?.value - ? { - value: currentAlert.dashboard.value, - label: currentAlert.dashboard.label, - } - : undefined - } - options={loadDashboardOptions} - onChange={onDashboardChange} - /> + <> + <div className="control-label"> + {TRANSLATIONS.SELECT_DASHBOARD_LABEL} + <span className="required">*</span> + </div> + <AsyncSelect + ariaLabel={TRANSLATIONS.DASHBOARD_TEXT} + name="dashboard" + value={ + currentAlert?.dashboard?.label && + currentAlert?.dashboard?.value + ? { + value: currentAlert.dashboard.value, + label: currentAlert.dashboard.label, + } + : undefined + } + options={loadDashboardOptions} + onChange={onDashboardChange} + placeholder={TRANSLATIONS.SELECT_DASHBOARD_PLACEHOLDER} + /> + </> )} + </StyledInputContainer> + <StyledInputContainer + css={['TEXT', 'CSV'].includes(reportFormat) && no_margin_bottom} + > {formatOptionEnabled && ( <> - <div className="inline-container"> - <StyledRadioGroup - onChange={onFormatChange} - value={reportFormat} - > - <StyledRadio value="PNG"> - {TRANSLATIONS.SEND_AS_PNG_TEXT} - </StyledRadio> - <StyledRadio value="CSV"> - {TRANSLATIONS.SEND_AS_CSV_TEXT} - </StyledRadio> - {TEXT_BASED_VISUALIZATION_TYPES.includes(chartVizType) && ( - <StyledRadio value="TEXT"> - {TRANSLATIONS.SEND_AS_TEXT} - </StyledRadio> - )} - </StyledRadioGroup> + <div className="control-label"> + {TRANSLATIONS.FORMAT_TYPE_LABEL} + <span className="required">*</span> </div> + <Select + ariaLabel={TRANSLATIONS.FORMAT_TYPE_PLACEHOLDER} + onChange={onFormatChange} + value={reportFormat} + options={ + /* If chart is of text based viz type: show text + format option */ + TEXT_BASED_VISUALIZATION_TYPES.includes(chartVizType) + ? Object.values(FORMAT_OPTIONS) + : ['png', 'csv'].map(key => FORMAT_OPTIONS[key]) + } + placeholder={TRANSLATIONS.FORMAT_TYPE_PLACEHOLDER} + /> </> )} - {isScreenshot && ( - <StyledInputContainer> - <div className="control-label" css={CustomWidthHeaderStyle}> - {TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_TEXT} + </StyledInputContainer> + {isScreenshot && ( + <StyledInputContainer + css={!isReport && contentType === 'chart' && no_margin_bottom} + > + <div className="control-label"> + {TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_TEXT} + </div> + <div className="input-container"> + <Input + type="number" + name="custom_width" + value={currentAlert?.custom_width || ''} + placeholder={ + TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_PLACEHOLDER_TEXT + } + onChange={onInputChange} + /> + </div> + </StyledInputContainer> + )} + {(isReport || contentType === 'dashboard') && ( + <div className="inline-container"> + <StyledCheckbox + data-test="bypass-cache" + className="checkbox" + checked={forceScreenshot} + onChange={onForceScreenshotChange} + > + {TRANSLATIONS.IGNORE_CACHE_TEXT} + </StyledCheckbox> + </div> + )} + </StyledPanel> + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.SCHEDULE_TITLE} + subtitle={TRANSLATIONS.SCHEDULE_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.SCHEDULE].status} + testId="schedule-panel" + /> + } + key="4" + style={{ borderBottom: 'none' }} + > + <AlertReportCronScheduler + value={currentAlert?.crontab || ALERT_REPORTS_DEFAULT_CRON_VALUE} + onChange={newVal => updateAlertState('crontab', newVal)} + /> + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.TIMEZONE_TEXT} <span className="required">*</span> + </div> + <TimezoneSelector + onTimezoneChange={onTimezoneChange} + timezone={currentAlert?.timezone} + minWidth="100%" + /> + </StyledInputContainer> + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.LOG_RETENTION_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <Select + ariaLabel={TRANSLATIONS.LOG_RETENTION_TEXT} + placeholder={TRANSLATIONS.LOG_RETENTION_TEXT} + onChange={onLogRetentionChange} + value={ + typeof currentAlert?.log_retention === 'number' + ? currentAlert?.log_retention + : ALERT_REPORTS_DEFAULT_RETENTION + } + options={RETENTION_OPTIONS} + sortComparator={propertyComparator('value')} + /> + </div> + </StyledInputContainer> + <StyledInputContainer css={no_margin_bottom}> + {isReport ? ( + <> + <div className="control-label"> + {TRANSLATIONS.WORKING_TIMEOUT_TEXT} + <span className="required">*</span> </div> <div className="input-container"> - <Input - type="number" - name="custom_width" - value={currentAlert?.custom_width || ''} - placeholder={ - TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_PLACEHOLDER_TEXT - } - onChange={onInputChange} + <NumberInput + min={1} + name="working_timeout" + value={currentAlert?.working_timeout || ''} + placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} + onChange={onTimeoutVerifyChange} + timeUnit="seconds" /> </div> - </StyledInputContainer> - )} - {(isReport || contentType === 'dashboard') && ( - <div className="inline-container"> - <StyledCheckbox - data-test="bypass-cache" - className="checkbox" - checked={forceScreenshot} - onChange={onForceScreenshotChange} - > - {TRANSLATIONS.IGNORE_CACHE_TEXT} - </StyledCheckbox> - </div> + </> + ) : ( + <> + <div className="control-label"> + {TRANSLATIONS.GRACE_PERIOD_TEXT} + </div> + <div className="input-container"> + <NumberInput + min={1} + name="grace_period" + value={currentAlert?.grace_period || ''} + placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} + onChange={onTimeoutVerifyChange} + timeUnit="seconds" Review Comment: translation here too ########## superset-frontend/src/features/alerts/AlertReportModal.tsx: ########## @@ -1190,367 +1313,456 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({ </h4> } > - <StyledSectionContainer> - <div className="header-section"> - <StyledInputContainer> - <div className="control-label"> - {isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="text" - name="name" - value={currentAlert ? currentAlert.name : ''} - placeholder={ - isReport - ? TRANSLATIONS.REPORT_NAME_TEXT - : TRANSLATIONS.ALERT_NAME_TEXT - } - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.OWNERS_TEXT} - <span className="required">*</span> - </div> - <div data-test="owners-select" className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.OWNERS_TEXT} - allowClear - name="owners" - mode="multiple" - value={ - (currentAlert?.owners as { - label: string; - value: number; - }[]) || [] - } - options={loadOwnerOptions} - onChange={onOwnersChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label">{TRANSLATIONS.DESCRIPTION_TEXT}</div> - <div className="input-container"> - <input - type="text" - name="description" - value={currentAlert ? currentAlert.description || '' : ''} - placeholder={TRANSLATIONS.DESCRIPTION_TEXT} - onChange={onInputChange} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledSwitchContainer> - <Switch - onChange={onActiveSwitch} - checked={currentAlert ? currentAlert.active : true} + <Collapse + expandIconPosition="right" + defaultActiveKey="1" + accordion + style={{ border: 'none' }} + > + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.GENERAL_TITLE} + subtitle={TRANSLATIONS.GENERAL_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.GENERAL].status} + testId="general-information-panel" /> - <div className="switch-label">{TRANSLATIONS.ACTIVE_TEXT}</div> - </StyledSwitchContainer> - </div> - <div className="column-section"> - {!isReport && ( - <div className="column condition"> - <StyledSectionTitle> - <h4>{TRANSLATIONS.ALERT_CONDITION_TEXT}</h4> - </StyledSectionTitle> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.DATABASE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <AsyncSelect - ariaLabel={TRANSLATIONS.DATABASE_TEXT} - name="source" - value={ - currentAlert?.database?.label && - currentAlert?.database?.value - ? { - value: currentAlert.database.value, - label: currentAlert.database.label, - } - : undefined - } - options={loadSourceOptions} - onChange={onSourceChange} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.SQL_QUERY_TEXT} - <StyledTooltip tooltip={TRANSLATIONS.SQL_QUERY_TOOLTIP} /> - <span className="required">*</span> - </div> - <TextAreaControl - name="sql" - language="sql" - offerEditInModal={false} - minLines={15} - maxLines={15} - onChange={onSQLChange} - readOnly={false} - initialValue={resource?.sql} - key={currentAlert?.id} + } + key="1" + style={panelBorder} + > + <div className="header-section"> + <StyledInputContainer> + <div className="control-label"> + {isReport + ? TRANSLATIONS.REPORT_NAME_TEXT + : TRANSLATIONS.ALERT_NAME_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <input + type="text" + name="name" + value={currentAlert ? currentAlert.name : ''} + placeholder={ + isReport + ? TRANSLATIONS.REPORT_NAME_PLACEHOLDER + : TRANSLATIONS.ALERT_NAME_PLACEHOLDER + } + onChange={onInputChange} /> - </StyledInputContainer> - <div className="inline-container wrap"> - <StyledInputContainer> - <div className="control-label" css={inputSpacer}> - {TRANSLATIONS.TRIGGER_ALERT_IF_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <Select - ariaLabel={TRANSLATIONS.CONDITION_TEXT} - onChange={onConditionChange} - placeholder="Condition" - value={ - currentAlert?.validator_config_json?.op || undefined - } - options={CONDITIONS} - css={inputSpacer} - /> - </div> - </StyledInputContainer> - <StyledInputContainer> - <div className="control-label"> - {TRANSLATIONS.VALUE_TEXT} - <span className="required">*</span> - </div> - <div className="input-container"> - <input - type="number" - name="threshold" - disabled={conditionNotNull} - value={ - currentAlert?.validator_config_json?.threshold !== - undefined - ? currentAlert.validator_config_json.threshold - : '' - } - placeholder={TRANSLATIONS.VALUE_TEXT} - onChange={onThresholdChange} - /> - </div> - </StyledInputContainer> </div> - </div> - )} - <div className="column schedule"> - <StyledSectionTitle> - <h4> - {isReport - ? TRANSLATIONS.REPORT_SCHEDULE_TEXT - : TRANSLATIONS.ALERT_CONDITION_SCHEDULE_TEXT} - </h4> - <span className="required">*</span> - </StyledSectionTitle> - <AlertReportCronScheduler - value={currentAlert?.crontab || ALERT_REPORTS_DEFAULT_CRON_VALUE} - onChange={newVal => updateAlertState('crontab', newVal)} - /> - <div className="control-label">{TRANSLATIONS.TIMEZONE_TEXT}</div> - <div - className="input-container" - css={(theme: SupersetTheme) => timezoneHeaderStyle(theme)} - > - <TimezoneSelector - onTimezoneChange={onTimezoneChange} - timezone={currentAlert?.timezone} - minWidth="100%" - /> - </div> - <StyledSectionTitle> - <h4>{TRANSLATIONS.SCHEDULE_SETTINGS_TEXT}</h4> - </StyledSectionTitle> + </StyledInputContainer> <StyledInputContainer> <div className="control-label"> - {TRANSLATIONS.LOG_RETENTION_TEXT} + {TRANSLATIONS.OWNERS_TEXT} <span className="required">*</span> </div> - <div className="input-container"> - <Select - ariaLabel={TRANSLATIONS.LOG_RETENTION_TEXT} - placeholder={TRANSLATIONS.LOG_RETENTION_TEXT} - onChange={onLogRetentionChange} + <div data-test="owners-select" className="input-container"> + <AsyncSelect + ariaLabel={TRANSLATIONS.OWNERS_TEXT} + allowClear + name="owners" + mode="multiple" + placeholder={TRANSLATIONS.OWNERS_PLACEHOLDER} value={ - typeof currentAlert?.log_retention === 'number' - ? currentAlert?.log_retention - : ALERT_REPORTS_DEFAULT_RETENTION + (currentAlert?.owners as { + label: string; + value: number; + }[]) || [] } - options={RETENTION_OPTIONS} - sortComparator={propertyComparator('value')} + options={loadOwnerOptions} + onChange={onOwnersChange} /> </div> </StyledInputContainer> <StyledInputContainer> <div className="control-label"> - {TRANSLATIONS.WORKING_TIMEOUT_TEXT} - <span className="required">*</span> + {TRANSLATIONS.DESCRIPTION_TEXT} </div> <div className="input-container"> <input - type="number" - min="1" - name="working_timeout" - value={currentAlert?.working_timeout || ''} - placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} - onChange={onTimeoutVerifyChange} + type="text" + name="description" + value={currentAlert ? currentAlert.description || '' : ''} + placeholder={ + isReport + ? TRANSLATIONS.REPORT_DESCRIPTION_PLACEHOLDER + : TRANSLATIONS.ALERT_DESCRIPTION_PLACEHOLDER + } + onChange={onInputChange} /> - <span className="input-label">{TRANSLATIONS.SECONDS_TEXT}</span> </div> </StyledInputContainer> - {!isReport && ( - <StyledInputContainer> + <StyledSwitchContainer> + <Switch + checked={currentAlert ? currentAlert.active : false} + defaultChecked + onChange={onActiveSwitch} + /> + <div className="switch-label"> + {isReport + ? TRANSLATIONS.ACTIVE_REPORT_TEXT + : TRANSLATIONS.ACTIVE_ALERT_TEXT} + </div> + </StyledSwitchContainer> + </div> + </StyledPanel> + {!isReport && ( + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.ALERT_CONDITION_TITLE} + subtitle={TRANSLATIONS.ALERT_CONDITION_SUBTITLE} + required={false} + validateCheckStatus={validationStatus[Sections.ALERT].status} + testId="alert-condition-panel" + /> + } + key="2" + style={{ borderBottom: 'none' }} + > + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.DATABASE_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <AsyncSelect + ariaLabel={TRANSLATIONS.DATABASE_TEXT} + name="source" + placeholder={TRANSLATIONS.DATABASE_PLACEHOLDER} + value={ + currentAlert?.database?.label && + currentAlert?.database?.value + ? { + value: currentAlert.database.value, + label: currentAlert.database.label, + } + : undefined + } + options={loadSourceOptions} + onChange={onSourceChange} + /> + </div> + </StyledInputContainer> + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.SQL_QUERY_TEXT} + <StyledTooltip tooltip={TRANSLATIONS.SQL_QUERY_TOOLTIP} /> + <span className="required">*</span> + </div> + <TextAreaControl + name="sql" + language="sql" + offerEditInModal={false} + minLines={15} + maxLines={15} + onChange={onSQLChange} + readOnly={false} + initialValue={resource?.sql} + key={currentAlert?.id} + /> + </StyledInputContainer> + <div className="inline-container wrap"> + <StyledInputContainer css={no_margin_bottom}> + <div className="control-label" css={inputSpacer}> + {TRANSLATIONS.TRIGGER_ALERT_IF_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <Select + ariaLabel={TRANSLATIONS.CONDITION_TEXT} + onChange={onConditionChange} + placeholder={TRANSLATIONS.CONDITION_PLACEHOLDER} + value={currentAlert?.validator_config_json?.op || undefined} + options={CONDITIONS} + css={inputSpacer} + /> + </div> + </StyledInputContainer> + <StyledInputContainer css={no_margin_bottom}> <div className="control-label"> - {TRANSLATIONS.GRACE_PERIOD_TEXT} + {TRANSLATIONS.VALUE_TEXT} <span className="required">*</span> </div> <div className="input-container"> <input type="number" - min="1" - name="grace_period" - value={currentAlert?.grace_period || ''} - placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} - onChange={onTimeoutVerifyChange} + name="threshold" + disabled={conditionNotNull} + value={ + currentAlert?.validator_config_json?.threshold !== + undefined + ? currentAlert.validator_config_json.threshold + : '' + } + placeholder={TRANSLATIONS.VALUE_TEXT} + onChange={onThresholdChange} /> - <span className="input-label"> - {TRANSLATIONS.SECONDS_TEXT} - </span> </div> </StyledInputContainer> - )} - </div> - <div className="column message"> - <StyledSectionTitle> - <h4>{TRANSLATIONS.MESSAGE_CONTENT_TEXT}</h4> + </div> + </StyledPanel> + )} + <StyledPanel + header={ + <ValidatedPanelHeader + title={ + isReport + ? TRANSLATIONS.REPORT_CONTENTS_TITLE + : TRANSLATIONS.ALERT_CONTENTS_TITLE + } + subtitle={TRANSLATIONS.CONTENTS_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.CONTENT].status} + testId="contents-panel" + /> + } + key="3" + style={{ borderBottom: 'none' }} + > + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.CONTENT_TYPE_LABEL} <span className="required">*</span> - </StyledSectionTitle> - <Radio.Group onChange={onContentTypeChange} value={contentType}> - <StyledRadio value="dashboard"> - {TRANSLATIONS.DASHBOARD_TEXT} - </StyledRadio> - <StyledRadio value="chart">{TRANSLATIONS.CHART_TEXT}</StyledRadio> - </Radio.Group> + </div> + <Select + ariaLabel={TRANSLATIONS.CONTENT_TYPE_PLACEHOLDER} + onChange={onContentTypeChange} + value={contentType} + options={CONTENT_TYPE_OPTIONS} + placeholder={TRANSLATIONS.CONTENT_TYPE_PLACEHOLDER} + /> + </StyledInputContainer> + <StyledInputContainer> {contentType === 'chart' ? ( - <AsyncSelect - ariaLabel={TRANSLATIONS.CHART_TEXT} - name="chart" - value={ - currentAlert?.chart?.label && currentAlert?.chart?.value - ? { - value: currentAlert.chart.value, - label: currentAlert.chart.label, - } - : undefined - } - options={loadChartOptions} - onChange={onChartChange} - /> + <> + <div className="control-label"> + {TRANSLATIONS.SELECT_CHART_LABEL} + <span className="required">*</span> + </div> + <AsyncSelect + ariaLabel={TRANSLATIONS.CHART_TEXT} + name="chart" + value={ + currentAlert?.chart?.label && currentAlert?.chart?.value + ? { + value: currentAlert.chart.value, + label: currentAlert.chart.label, + } + : undefined + } + options={loadChartOptions} + onChange={onChartChange} + placeholder={TRANSLATIONS.SELECT_CHART_PLACEHOLDER} + /> + </> ) : ( - <AsyncSelect - ariaLabel={TRANSLATIONS.DASHBOARD_TEXT} - name="dashboard" - value={ - currentAlert?.dashboard?.label && - currentAlert?.dashboard?.value - ? { - value: currentAlert.dashboard.value, - label: currentAlert.dashboard.label, - } - : undefined - } - options={loadDashboardOptions} - onChange={onDashboardChange} - /> + <> + <div className="control-label"> + {TRANSLATIONS.SELECT_DASHBOARD_LABEL} + <span className="required">*</span> + </div> + <AsyncSelect + ariaLabel={TRANSLATIONS.DASHBOARD_TEXT} + name="dashboard" + value={ + currentAlert?.dashboard?.label && + currentAlert?.dashboard?.value + ? { + value: currentAlert.dashboard.value, + label: currentAlert.dashboard.label, + } + : undefined + } + options={loadDashboardOptions} + onChange={onDashboardChange} + placeholder={TRANSLATIONS.SELECT_DASHBOARD_PLACEHOLDER} + /> + </> )} + </StyledInputContainer> + <StyledInputContainer + css={['TEXT', 'CSV'].includes(reportFormat) && no_margin_bottom} + > {formatOptionEnabled && ( <> - <div className="inline-container"> - <StyledRadioGroup - onChange={onFormatChange} - value={reportFormat} - > - <StyledRadio value="PNG"> - {TRANSLATIONS.SEND_AS_PNG_TEXT} - </StyledRadio> - <StyledRadio value="CSV"> - {TRANSLATIONS.SEND_AS_CSV_TEXT} - </StyledRadio> - {TEXT_BASED_VISUALIZATION_TYPES.includes(chartVizType) && ( - <StyledRadio value="TEXT"> - {TRANSLATIONS.SEND_AS_TEXT} - </StyledRadio> - )} - </StyledRadioGroup> + <div className="control-label"> + {TRANSLATIONS.FORMAT_TYPE_LABEL} + <span className="required">*</span> </div> + <Select + ariaLabel={TRANSLATIONS.FORMAT_TYPE_PLACEHOLDER} + onChange={onFormatChange} + value={reportFormat} + options={ + /* If chart is of text based viz type: show text + format option */ + TEXT_BASED_VISUALIZATION_TYPES.includes(chartVizType) + ? Object.values(FORMAT_OPTIONS) + : ['png', 'csv'].map(key => FORMAT_OPTIONS[key]) + } + placeholder={TRANSLATIONS.FORMAT_TYPE_PLACEHOLDER} + /> </> )} - {isScreenshot && ( - <StyledInputContainer> - <div className="control-label" css={CustomWidthHeaderStyle}> - {TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_TEXT} + </StyledInputContainer> + {isScreenshot && ( + <StyledInputContainer + css={!isReport && contentType === 'chart' && no_margin_bottom} + > + <div className="control-label"> + {TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_TEXT} + </div> + <div className="input-container"> + <Input + type="number" + name="custom_width" + value={currentAlert?.custom_width || ''} + placeholder={ + TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_PLACEHOLDER_TEXT + } + onChange={onInputChange} + /> + </div> + </StyledInputContainer> + )} + {(isReport || contentType === 'dashboard') && ( + <div className="inline-container"> + <StyledCheckbox + data-test="bypass-cache" + className="checkbox" + checked={forceScreenshot} + onChange={onForceScreenshotChange} + > + {TRANSLATIONS.IGNORE_CACHE_TEXT} + </StyledCheckbox> + </div> + )} + </StyledPanel> + <StyledPanel + header={ + <ValidatedPanelHeader + title={TRANSLATIONS.SCHEDULE_TITLE} + subtitle={TRANSLATIONS.SCHEDULE_SUBTITLE} + required + validateCheckStatus={validationStatus[Sections.SCHEDULE].status} + testId="schedule-panel" + /> + } + key="4" + style={{ borderBottom: 'none' }} + > + <AlertReportCronScheduler + value={currentAlert?.crontab || ALERT_REPORTS_DEFAULT_CRON_VALUE} + onChange={newVal => updateAlertState('crontab', newVal)} + /> + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.TIMEZONE_TEXT} <span className="required">*</span> + </div> + <TimezoneSelector + onTimezoneChange={onTimezoneChange} + timezone={currentAlert?.timezone} + minWidth="100%" + /> + </StyledInputContainer> + <StyledInputContainer> + <div className="control-label"> + {TRANSLATIONS.LOG_RETENTION_TEXT} + <span className="required">*</span> + </div> + <div className="input-container"> + <Select + ariaLabel={TRANSLATIONS.LOG_RETENTION_TEXT} + placeholder={TRANSLATIONS.LOG_RETENTION_TEXT} + onChange={onLogRetentionChange} + value={ + typeof currentAlert?.log_retention === 'number' + ? currentAlert?.log_retention + : ALERT_REPORTS_DEFAULT_RETENTION + } + options={RETENTION_OPTIONS} + sortComparator={propertyComparator('value')} + /> + </div> + </StyledInputContainer> + <StyledInputContainer css={no_margin_bottom}> + {isReport ? ( + <> + <div className="control-label"> + {TRANSLATIONS.WORKING_TIMEOUT_TEXT} + <span className="required">*</span> </div> <div className="input-container"> - <Input - type="number" - name="custom_width" - value={currentAlert?.custom_width || ''} - placeholder={ - TRANSLATIONS.CUSTOM_SCREENSHOT_WIDTH_PLACEHOLDER_TEXT - } - onChange={onInputChange} + <NumberInput + min={1} + name="working_timeout" + value={currentAlert?.working_timeout || ''} + placeholder={TRANSLATIONS.TIME_IN_SECONDS_TEXT} + onChange={onTimeoutVerifyChange} + timeUnit="seconds" Review Comment: translation for seconds? ########## superset-frontend/src/features/alerts/components/NotificationMethod.tsx: ########## @@ -36,7 +36,7 @@ const StyledNotificationMethod = styled.div` margin-bottom: 10px; .input-container { - margin-left: 10px; + // margin-left: 10px; Review Comment: Uncomment or delete -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: notifications-unsubscr...@superset.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@superset.apache.org For additional commands, e-mail: notifications-h...@superset.apache.org