mirror of https://github.com/apache/superset.git
feat: add cron picker to AlertReportModal (#12032)
* humanize cron display in list view
This commit is contained in:
parent
fda3a2fe7c
commit
6fcda5dac1
|
@ -24,6 +24,7 @@ chardet==3.0.4 # via aiohttp
|
|||
click==7.1.2 # via apache-superset, flask, flask-appbuilder
|
||||
colorama==0.4.4 # via apache-superset, flask-appbuilder
|
||||
contextlib2==0.6.0.post1 # via apache-superset
|
||||
cron-descriptor==1.2.24 # via apache-superset
|
||||
croniter==0.3.36 # via apache-superset
|
||||
cryptography==3.2.1 # via apache-superset
|
||||
decorator==4.4.2 # via retry
|
||||
|
|
|
@ -30,7 +30,7 @@ combine_as_imports = true
|
|||
include_trailing_comma = true
|
||||
line_length = 88
|
||||
known_first_party = superset
|
||||
known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,contextlib2,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parameterized,parsedatetime,pathlib2,pgsanity,polyline,prison,pyarrow,pyhive,pytest,pytz,redis,retry,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,werkzeug,wtforms,wtforms_json,yaml
|
||||
known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,contextlib2,cron_descriptor,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parameterized,parsedatetime,pathlib2,pgsanity,polyline,prison,pyarrow,pyhive,pytest,pytz,redis,retry,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,werkzeug,wtforms,wtforms_json,yaml
|
||||
multi_line_output = 3
|
||||
order_by_type = false
|
||||
|
||||
|
|
1
setup.py
1
setup.py
|
@ -72,6 +72,7 @@ setup(
|
|||
"colorama",
|
||||
"contextlib2",
|
||||
"croniter>=0.3.28",
|
||||
"cron-descriptor",
|
||||
"cryptography>=3.2.1",
|
||||
"flask>=1.1.0, <2.0.0",
|
||||
"flask-appbuilder>=3.1.1, <4.0.0",
|
||||
|
|
|
@ -43514,6 +43514,11 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is-mounted-hook/-/react-is-mounted-hook-1.0.3.tgz",
|
||||
"integrity": "sha512-YCCYcTVYMPfTi6WhWIwM9EYBcpHoivjjkE90O5ScsE9wXSbeXGZvLDMGt4mdSNcWshhc8JD0AzgBmsleCSdSFA=="
|
||||
},
|
||||
"react-js-cron": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-js-cron/-/react-js-cron-1.2.0.tgz",
|
||||
"integrity": "sha512-mWxTmXkqP58ughdziS3qjEUVl1O03XEo8WDvr45/kTQfbd0C6ITniAsG5wZzwmTOgmrOKQheHog7L0TP683WUA=="
|
||||
},
|
||||
"react-json-tree": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.11.2.tgz",
|
||||
|
|
|
@ -141,6 +141,7 @@
|
|||
"react-dom": "^16.13.0",
|
||||
"react-gravatar": "^2.6.1",
|
||||
"react-hot-loader": "^4.12.20",
|
||||
"react-js-cron": "^1.2.0",
|
||||
"react-json-tree": "^0.11.2",
|
||||
"react-jsonschema-form": "^1.2.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* 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, t } from '@superset-ui/core';
|
||||
import ReactCronPicker, { Locale, CronProps } from 'react-js-cron';
|
||||
|
||||
export * from 'react-js-cron';
|
||||
|
||||
export const LOCALE: Locale = {
|
||||
everyText: t('every'),
|
||||
emptyMonths: t('every month'),
|
||||
emptyMonthDays: t('every day of the month'),
|
||||
emptyMonthDaysShort: t('day of the month'),
|
||||
emptyWeekDays: t('every day of the week'),
|
||||
emptyWeekDaysShort: t('day of the week'),
|
||||
emptyHours: t('every hour'),
|
||||
emptyMinutes: t('every minute UTC'),
|
||||
emptyMinutesForHourPeriod: t('every'),
|
||||
yearOption: t('year'),
|
||||
monthOption: t('month'),
|
||||
weekOption: t('week'),
|
||||
dayOption: t('day'),
|
||||
hourOption: t('hour'),
|
||||
minuteOption: t('minute'),
|
||||
rebootOption: t('reboot'),
|
||||
prefixPeriod: t('Every'),
|
||||
prefixMonths: t('in'),
|
||||
prefixMonthDays: t('on'),
|
||||
prefixWeekDays: t('on'),
|
||||
prefixWeekDaysForMonthAndYearPeriod: t('and'),
|
||||
prefixHours: t('at'),
|
||||
prefixMinutes: t(':'),
|
||||
prefixMinutesForHourPeriod: t('at'),
|
||||
suffixMinutesForHourPeriod: t('minute(s) UTC'),
|
||||
errorInvalidCron: t('Invalid cron expression'),
|
||||
clearButtonText: t('Clear'),
|
||||
weekDays: [
|
||||
// Order is important, the index will be used as value
|
||||
t('Sunday'), // Sunday must always be first, it's "0"
|
||||
t('Monday'),
|
||||
t('Tuesday'),
|
||||
t('Wednesday'),
|
||||
t('Thursday'),
|
||||
t('Friday'),
|
||||
t('Saturday'),
|
||||
],
|
||||
months: [
|
||||
// Order is important, the index will be used as value
|
||||
t('January'),
|
||||
t('February'),
|
||||
t('March'),
|
||||
t('April'),
|
||||
t('May'),
|
||||
t('June'),
|
||||
t('July'),
|
||||
t('August'),
|
||||
t('September'),
|
||||
t('October'),
|
||||
t('November'),
|
||||
t('December'),
|
||||
],
|
||||
// Order is important, the index will be used as value
|
||||
altWeekDays: [
|
||||
t('SUN'), // Sunday must always be first, it's "0"
|
||||
t('MON'),
|
||||
t('TUE'),
|
||||
t('WED'),
|
||||
t('THU'),
|
||||
t('FRI'),
|
||||
t('SAT'),
|
||||
],
|
||||
// Order is important, the index will be used as value
|
||||
altMonths: [
|
||||
t('JAN'),
|
||||
t('FEB'),
|
||||
t('MAR'),
|
||||
t('APR'),
|
||||
t('MAY'),
|
||||
t('JUN'),
|
||||
t('JUL'),
|
||||
t('AUG'),
|
||||
t('SEP'),
|
||||
t('OCT'),
|
||||
t('NOV'),
|
||||
t('DEC'),
|
||||
],
|
||||
};
|
||||
|
||||
export const CronPicker = styled((props: CronProps) => (
|
||||
<ReactCronPicker locale={LOCALE} {...props} />
|
||||
))`
|
||||
.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;
|
||||
}
|
||||
.react-js-cron-custom-select > div:first-of-type {
|
||||
border-radius: ${({ theme }) => theme.gridUnit}px;
|
||||
}
|
||||
`;
|
|
@ -39,7 +39,7 @@ const StyledSelect = styled(BaseSelect)`
|
|||
}
|
||||
}
|
||||
`;
|
||||
const StyledOption = styled(BaseSelect.Option)``;
|
||||
const StyledOption = BaseSelect.Option;
|
||||
|
||||
export const Select = Object.assign(StyledSelect, {
|
||||
Option: StyledOption,
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { withKnobs, boolean, select } from '@storybook/addon-knobs';
|
||||
import Button from 'src/components/Button';
|
||||
|
@ -24,8 +24,8 @@ import Modal from './Modal';
|
|||
import Tabs, { EditableTabs } from './Tabs';
|
||||
import AntdPopover from './Popover';
|
||||
import { Tooltip as AntdTooltip } from './Tooltip';
|
||||
import { Menu } from '.';
|
||||
import { Switch as AntdSwitch } from './Switch';
|
||||
import { Menu, Input, Divider } from '.';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import InfoTooltip from './InfoTooltip';
|
||||
import {
|
||||
|
@ -35,6 +35,7 @@ import {
|
|||
import Badge from './Badge';
|
||||
import ProgressBar from './ProgressBar';
|
||||
import Collapse from './Collapse';
|
||||
import { CronPicker, CronError } from './CronPicker';
|
||||
|
||||
export default {
|
||||
title: 'Common Components',
|
||||
|
@ -321,3 +322,43 @@ export const CollapseTextLight = () => (
|
|||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
);
|
||||
export function StyledCronPicker() {
|
||||
const inputRef = useRef<Input>(null);
|
||||
const defaultValue = '30 5 * * 1,6';
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const customSetValue = useCallback(
|
||||
(newValue: string) => {
|
||||
setValue(newValue);
|
||||
inputRef.current?.setValue(newValue);
|
||||
},
|
||||
[inputRef],
|
||||
);
|
||||
const [error, onError] = useState<CronError>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onBlur={event => {
|
||||
setValue(event.target.value);
|
||||
}}
|
||||
onPressEnter={() => {
|
||||
setValue(inputRef.current?.input.value || '');
|
||||
}}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<CronPicker
|
||||
clearButton={false}
|
||||
value={value}
|
||||
setValue={customSetValue}
|
||||
onError={onError}
|
||||
/>
|
||||
|
||||
<p style={{ marginTop: 20 }}>
|
||||
Error: {error ? error.description : 'undefined'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ export {
|
|||
Button,
|
||||
Card,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Input,
|
||||
|
|
|
@ -24,6 +24,7 @@ import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
|
|||
import Button from 'src/components/Button';
|
||||
import FacePile from 'src/components/FacePile';
|
||||
import { IconName } from 'src/components/Icon';
|
||||
import { Tooltip } from 'src/common/components/Tooltip';
|
||||
import ListView, { FilterOperators, Filters } from 'src/components/ListView';
|
||||
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||
import { Switch } from 'src/common/components/Switch';
|
||||
|
@ -55,7 +56,7 @@ function AlertList({
|
|||
isReportEnabled = false,
|
||||
user,
|
||||
}: AlertListProps) {
|
||||
const title = isReportEnabled ? 'report' : 'alert';
|
||||
const title = isReportEnabled ? t('report') : t('alert');
|
||||
const pathName = isReportEnabled ? 'Reports' : 'Alerts';
|
||||
const initalFilters = useMemo(
|
||||
() => [
|
||||
|
@ -139,20 +140,31 @@ function AlertList({
|
|||
}: any) =>
|
||||
recipients.map((r: any) => (
|
||||
<RecipientIcon key={r.id} type={r.type} />
|
||||
// <Icon key={r.id} name={r.type.toLowerCase() as IconName} />
|
||||
)),
|
||||
accessor: 'recipients',
|
||||
Header: t('Notification Method'),
|
||||
disableSortBy: true,
|
||||
size: 'xl',
|
||||
},
|
||||
{
|
||||
Header: t('Schedule'),
|
||||
accessor: 'crontab',
|
||||
accessor: 'crontab_humanized',
|
||||
size: 'xl',
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { crontab_humanized = '' },
|
||||
},
|
||||
}: any) => (
|
||||
<Tooltip title={crontab_humanized} placement="topLeft">
|
||||
<span>{crontab_humanized}</span>,
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: 'created_by',
|
||||
disableSortBy: true,
|
||||
hidden: true,
|
||||
size: 'xl',
|
||||
},
|
||||
{
|
||||
Cell: ({
|
||||
|
@ -163,7 +175,7 @@ function AlertList({
|
|||
Header: t('Owners'),
|
||||
id: 'owners',
|
||||
disableSortBy: true,
|
||||
size: 'lg',
|
||||
size: 'xl',
|
||||
},
|
||||
{
|
||||
Cell: ({ row: { original } }: any) => (
|
||||
|
@ -177,6 +189,7 @@ function AlertList({
|
|||
Header: t('Active'),
|
||||
accessor: 'active',
|
||||
id: 'active',
|
||||
size: 'xl',
|
||||
},
|
||||
{
|
||||
Cell: ({ row: { original } }: any) => {
|
||||
|
@ -234,7 +247,7 @@ function AlertList({
|
|||
subMenuButtons.push({
|
||||
name: (
|
||||
<>
|
||||
<i className="fa fa-plus" /> {t(`${title}`)}
|
||||
<i className="fa fa-plus" /> {title}
|
||||
</>
|
||||
),
|
||||
buttonStyle: 'primary',
|
||||
|
@ -245,8 +258,8 @@ function AlertList({
|
|||
}
|
||||
|
||||
const EmptyStateButton = (
|
||||
<Button buttonStyle="primary" onClick={() => {}}>
|
||||
<i className="fa fa-plus" /> {t(`${title}`)}
|
||||
<Button buttonStyle="primary" onClick={() => handleAlertEdit(null)}>
|
||||
<i className="fa fa-plus" /> {title}
|
||||
</Button>
|
||||
);
|
||||
|
||||
|
|
|
@ -28,8 +28,9 @@ import { Select } from 'src/common/components/Select';
|
|||
import { Radio } from 'src/common/components/Radio';
|
||||
import { AsyncSelect } from 'src/components/Select';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
|
||||
import Owner from 'src/types/Owner';
|
||||
|
||||
import { AlertReportCronScheduler } from './components/AlertReportCronScheduler';
|
||||
import { AlertObject, Operator, Recipient, MetaObject } from './types';
|
||||
|
||||
type SelectValue = {
|
||||
|
@ -121,7 +122,7 @@ const StyledSectionContainer = styled.div`
|
|||
|
||||
.column {
|
||||
flex: 1 1 auto;
|
||||
min-width: 33.33%;
|
||||
min-width: calc(33.33% - ${({ theme }) => theme.gridUnit * 8}px);
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
|
||||
.async-select {
|
||||
|
@ -142,6 +143,9 @@ const StyledSectionContainer = styled.div`
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
&.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
|
@ -180,7 +184,7 @@ const StyledSwitchContainer = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
export const StyledInputContainer = styled.div`
|
||||
flex: 1 1 auto;
|
||||
margin: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
margin-top: 0;
|
||||
|
@ -449,10 +453,6 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
const [currentAlert, setCurrentAlert] = useState<AlertObject | null>();
|
||||
const [isHidden, setIsHidden] = useState<boolean>(true);
|
||||
const [contentType, setContentType] = useState<string>('dashboard');
|
||||
const [scheduleFormat, setScheduleFormat] = useState<string>(
|
||||
'dropdown-format',
|
||||
);
|
||||
|
||||
// Dropdown options
|
||||
const [sourceOptions, setSourceOptions] = useState<MetaObject[]>([]);
|
||||
const [dashboardOptions, setDashboardOptions] = useState<MetaObject[]>([]);
|
||||
|
@ -806,12 +806,6 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
updateAlertState('validator_config_json', config);
|
||||
};
|
||||
|
||||
const onScheduleFormatChange = (event: any) => {
|
||||
const { target } = event;
|
||||
|
||||
setScheduleFormat(target.value);
|
||||
};
|
||||
|
||||
const onLogRetentionChange = (retention: number) => {
|
||||
updateAlertState('log_retention', retention);
|
||||
};
|
||||
|
@ -976,12 +970,18 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
// Dropdown options
|
||||
const conditionOptions = CONDITIONS.map(condition => {
|
||||
return (
|
||||
<Select.Option value={condition.value}>{condition.label}</Select.Option>
|
||||
<Select.Option key={condition.value} value={condition.value}>
|
||||
{condition.label}
|
||||
</Select.Option>
|
||||
);
|
||||
});
|
||||
|
||||
const retentionOptions = RETENTION_OPTIONS.map(option => {
|
||||
return <Select.Option value={option.value}>{option.label}</Select.Option>;
|
||||
return (
|
||||
<Select.Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Select.Option>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -1102,7 +1102,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
<div className="inline-container">
|
||||
<div className="inline-container wrap">
|
||||
<StyledInputContainer>
|
||||
<div className="control-label">
|
||||
{t('Alert If...')}
|
||||
|
@ -1153,32 +1153,10 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
|
|||
<StyledSectionTitle>
|
||||
<h4>{t('Alert Condition Schedule')}</h4>
|
||||
</StyledSectionTitle>
|
||||
<Radio.Group
|
||||
onChange={onScheduleFormatChange}
|
||||
value={scheduleFormat}
|
||||
>
|
||||
<div className="inline-container add-margin">
|
||||
<Radio value="dropdown-format" />
|
||||
<span className="input-label">
|
||||
Every x Minutes (should be set of dropdown options)
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-container add-margin">
|
||||
<Radio value="cron-format" />
|
||||
<span className="input-label">CRON Schedule</span>
|
||||
<StyledInputContainer className="styled-input">
|
||||
<div className="input-container">
|
||||
<input
|
||||
type="text"
|
||||
name="crontab"
|
||||
value={currentAlert ? currentAlert.crontab || '' : ''}
|
||||
placeholder={t('CRON Expression')}
|
||||
onChange={onTextChange}
|
||||
<AlertReportCronScheduler
|
||||
value={(currentAlert && currentAlert.crontab) || undefined}
|
||||
onChange={newVal => updateAlertState('crontab', newVal)}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
</div>
|
||||
</Radio.Group>
|
||||
<StyledSectionTitle>
|
||||
<h4>{t('Schedule Settings')}</h4>
|
||||
</StyledSectionTitle>
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* 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 { ReactWrapper } from 'enzyme';
|
||||
import { styledMount as mount } from 'spec/helpers/theming';
|
||||
import { CronPicker } from 'src/common/components/CronPicker';
|
||||
import { Input } from 'src/common/components';
|
||||
|
||||
import { AlertReportCronScheduler } from './AlertReportCronScheduler';
|
||||
|
||||
describe('AlertReportCronScheduler', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
it('calls onChnage when value chnages', () => {
|
||||
const onChangeMock = jest.fn();
|
||||
wrapper = mount(
|
||||
<AlertReportCronScheduler value="* * * * *" onChange={onChangeMock} />,
|
||||
);
|
||||
|
||||
const changeValue = '1,7 * * * *';
|
||||
|
||||
wrapper.find(CronPicker).props().setValue(changeValue);
|
||||
expect(onChangeMock).toHaveBeenLastCalledWith(changeValue);
|
||||
});
|
||||
|
||||
it('sets input value when cron picker changes', () => {
|
||||
const onChangeMock = jest.fn();
|
||||
wrapper = mount(
|
||||
<AlertReportCronScheduler value="* * * * *" onChange={onChangeMock} />,
|
||||
);
|
||||
|
||||
const changeValue = '1,7 * * * *';
|
||||
|
||||
wrapper.find(CronPicker).props().setValue(changeValue);
|
||||
expect(wrapper.find(Input).state().value).toEqual(changeValue);
|
||||
});
|
||||
|
||||
it('calls onChange when input value changes', () => {
|
||||
const onChangeMock = jest.fn();
|
||||
wrapper = mount(
|
||||
<AlertReportCronScheduler value="* * * * *" onChange={onChangeMock} />,
|
||||
);
|
||||
|
||||
const changeValue = '1,7 * * * *';
|
||||
const event = {
|
||||
target: { value: changeValue },
|
||||
} as React.FocusEvent<HTMLInputElement>;
|
||||
|
||||
const inputProps = wrapper.find(Input).props();
|
||||
if (inputProps.onBlur) {
|
||||
inputProps.onBlur(event);
|
||||
}
|
||||
expect(onChangeMock).toHaveBeenLastCalledWith(changeValue);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* 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, { useState, useCallback, useRef, FunctionComponent } from 'react';
|
||||
import { t, useTheme } from '@superset-ui/core';
|
||||
|
||||
import { Input } from 'src/common/components';
|
||||
import { Radio } from 'src/common/components/Radio';
|
||||
import { CronPicker, CronError } from 'src/common/components/CronPicker';
|
||||
import { StyledInputContainer } from '../AlertReportModal';
|
||||
|
||||
interface AlertReportCronSchedulerProps {
|
||||
value?: string;
|
||||
onChange: (change: string) => any;
|
||||
}
|
||||
|
||||
export const AlertReportCronScheduler: FunctionComponent<AlertReportCronSchedulerProps> = ({
|
||||
value = '* * * * *',
|
||||
onChange,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef<Input>(null);
|
||||
const [scheduleFormat, setScheduleFormat] = useState<'picker' | 'input'>(
|
||||
'picker',
|
||||
);
|
||||
const customSetValue = useCallback(
|
||||
(newValue: string) => {
|
||||
onChange(newValue);
|
||||
inputRef.current?.setValue(newValue);
|
||||
},
|
||||
[inputRef, onChange],
|
||||
);
|
||||
const [error, onError] = useState<CronError>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Radio.Group
|
||||
onChange={e => setScheduleFormat(e.target.value)}
|
||||
value={scheduleFormat}
|
||||
>
|
||||
<div className="inline-container add-margin">
|
||||
<Radio value="picker" />
|
||||
<CronPicker
|
||||
clearButton={false}
|
||||
value={value}
|
||||
setValue={customSetValue}
|
||||
disabled={scheduleFormat !== 'picker'}
|
||||
displayError={scheduleFormat === 'picker'}
|
||||
onError={onError}
|
||||
/>
|
||||
</div>
|
||||
<div className="inline-container add-margin">
|
||||
<Radio value="input" />
|
||||
<span className="input-label">CRON Schedule</span>
|
||||
<StyledInputContainer className="styled-input">
|
||||
<div className="input-container">
|
||||
<Input
|
||||
type="text"
|
||||
name="crontab"
|
||||
ref={inputRef}
|
||||
style={error ? { borderColor: theme.colors.error.base } : {}}
|
||||
placeholder={t('CRON Expression')}
|
||||
disabled={scheduleFormat !== 'input'}
|
||||
onBlur={event => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
onPressEnter={() => {
|
||||
onChange(inputRef.current?.input.value || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</StyledInputContainer>
|
||||
</div>
|
||||
</Radio.Group>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -17,7 +17,9 @@
|
|||
"""A collection of ORM sqlalchemy models for Superset"""
|
||||
import enum
|
||||
|
||||
from cron_descriptor import get_description
|
||||
from flask_appbuilder import Model
|
||||
from flask_appbuilder.models.decorators import renders
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
|
@ -131,6 +133,10 @@ class ReportSchedule(Model, AuditMixinNullable):
|
|||
def __repr__(self) -> str:
|
||||
return str(self.name)
|
||||
|
||||
@renders("crontab")
|
||||
def crontab_humanized(self) -> str:
|
||||
return get_description(self.crontab)
|
||||
|
||||
|
||||
class ReportRecipients(
|
||||
Model, AuditMixinNullable
|
||||
|
|
|
@ -104,6 +104,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
|
|||
"created_by.last_name",
|
||||
"created_on",
|
||||
"crontab",
|
||||
"crontab_humanized",
|
||||
"id",
|
||||
"last_eval_dttm",
|
||||
"last_state",
|
||||
|
@ -147,6 +148,7 @@ class ReportScheduleRestApi(BaseSupersetModelRestApi):
|
|||
"created_on",
|
||||
"name",
|
||||
"type",
|
||||
"crontab_humanized",
|
||||
]
|
||||
search_columns = ["name", "active", "created_by", "type", "last_state"]
|
||||
search_filters = {"name": [ReportScheduleAllTextFilter]}
|
||||
|
|
|
@ -187,6 +187,7 @@ class TestReportSchedulesApi(SupersetTestCase):
|
|||
"created_by",
|
||||
"created_on",
|
||||
"crontab",
|
||||
"crontab_humanized",
|
||||
"id",
|
||||
"last_eval_dttm",
|
||||
"last_state",
|
||||
|
|
Loading…
Reference in New Issue