diff --git a/package.json b/package.json index d4d8fc9c..dbb40b90 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "clsx": "2.1.1", "dayjs": "1.11.13", "dotenv-webpack": "8.1.0", + "rc-picker": "4.9.2", "react": "18.2.0", "react-dom": "18.2.0", "react-error-boundary": "4.0.13", diff --git a/src/features/transfer/MutateTransferForm/components/TransferSchedule/index.tsx b/src/features/transfer/MutateTransferForm/components/TransferSchedule/index.tsx index bd47215f..54013a70 100644 --- a/src/features/transfer/MutateTransferForm/components/TransferSchedule/index.tsx +++ b/src/features/transfer/MutateTransferForm/components/TransferSchedule/index.tsx @@ -1,4 +1,5 @@ -import { Form, Input, Switch } from 'antd'; +import { CronSelect } from '@shared/ui'; +import { Form, Switch } from 'antd'; import React, { useState } from 'react'; export const TransferSchedule = () => { @@ -16,7 +17,7 @@ export const TransferSchedule = () => { {isScheduled && ( - + )} diff --git a/src/features/transfer/TransferDetailInfo/index.tsx b/src/features/transfer/TransferDetailInfo/index.tsx index a5d08ea6..f3faeaab 100644 --- a/src/features/transfer/TransferDetailInfo/index.tsx +++ b/src/features/transfer/TransferDetailInfo/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Descriptions } from 'antd'; import { Link } from 'react-router-dom'; +import { CronService } from '@shared/services'; import { TransferDetailInfoProps } from './types'; import classes from './styles.module.less'; @@ -32,12 +33,11 @@ export const TransferDetailInfo = ({ {queue.name} - - {transfer.is_scheduled ? 'Yes' : 'No'} - - - {transfer.schedule} - + {transfer.is_scheduled && ( + + {new CronService(transfer.schedule).getSchedule()} + + )} {transfer.strategy_params.type} diff --git a/src/shared/services/cronService/constants.ts b/src/shared/services/cronService/constants.ts new file mode 100644 index 00000000..8f2765d6 --- /dev/null +++ b/src/shared/services/cronService/constants.ts @@ -0,0 +1,10 @@ +import { CronSegmentKey, CronSegmentValue, DayOfWeekName } from './types'; + +export const CRON_VALUE_DEFAULT = new Map([ + ['minute', new Date().getMinutes()], + ['hour', new Date().getHours()], + ['date', null], + ['day', null], +]); + +export const DAYS_OF_WEEK = Object.values(DayOfWeekName); diff --git a/src/shared/services/cronService/cronService.ts b/src/shared/services/cronService/cronService.ts new file mode 100644 index 00000000..5b0a6216 --- /dev/null +++ b/src/shared/services/cronService/cronService.ts @@ -0,0 +1,158 @@ +import { getOrdinalNumber } from '@shared/utils'; + +import { CRON_VALUE_DEFAULT, DAYS_OF_WEEK } from './constants'; +import { CronSegmentKey, CronSegmentValue, Period } from './types'; + +/** Class for convenient handling cron settings */ +export class CronService { + private initialValueLength = 5; + + private period: Period; + + private value: Map; + + constructor(initialValue?: string) { + this.value = this.transformInitialValueToMap(initialValue); + this.period = this.initPeriod(); + } + + private transformInitialValueToMap(initialValue?: string) { + const splittedValue = initialValue?.split(' '); + if (splittedValue?.length !== this.initialValueLength) { + return CRON_VALUE_DEFAULT; + } + + const cronValue = splittedValue.map((segment) => { + const parsedValue = parseInt(segment); + if (Number.isInteger(parsedValue)) { + return parsedValue; + } + return null; + }); + + return new Map([ + ['minute', cronValue[0]], + ['hour', cronValue[1]], + ['date', cronValue[2]], + ['day', cronValue[4]], + ]); + } + + private initPeriod() { + if (this.getMonthDay() === null && this.getWeekDay() === null) { + return Period.DAY; + } + if (this.getMonthDay()) { + return Period.MONTH; + } + return Period.WEEK; + } + + getPeriod() { + return this.period; + } + + getMinute(): number { + return this.value.get('minute')!; + } + + getHour(): number { + return this.value.get('hour')!; + } + + getTime() { + return `${this.getHour()}:${this.getMinute()}`; + } + + getMonthDay(): CronSegmentValue { + return this.value.get('date') ?? null; + } + + getWeekDay(): CronSegmentValue { + return this.value.get('day') ?? null; + } + + setPeriod(period: Period) { + this.period = period; + switch (period) { + case Period.DAY: + this.setMonthDay(null); + this.setWeekDay(null); + break; + case Period.WEEK: + this.setWeekDay(new Date().getDay()); + this.setMonthDay(null); + break; + case Period.MONTH: + this.setWeekDay(null); + this.setMonthDay(new Date().getDate()); + } + } + + setMinute(value: number) { + if (value < 0 || value > 59) { + throw new Error('Invalid value'); + } + this.value.set('minute', value); + } + + setHour(value: number) { + if (value < 0 || value > 23) { + throw new Error('Invalid value'); + } + this.value.set('hour', value); + } + + setTime(hour?: number, minute?: number) { + this.setHour(hour ?? new Date().getHours()); + this.setMinute(minute ?? new Date().getMinutes()); + } + + setMonthDay(value: CronSegmentValue) { + if (value === null) { + this.value.set('date', null); + return; + } + if (value < 1 || value > 31) { + throw new Error('Invalid value'); + } + this.value.set('date', value); + } + + setWeekDay(value: CronSegmentValue) { + if (value === null) { + this.value.set('day', null); + return; + } + if (value < 0 || value > 6) { + throw new Error('Invalid value'); + } + this.value.set('day', value); + } + + toString() { + const minute = this.getMinute(); + const hour = this.getHour(); + const date = this.getMonthDay() ?? '*'; + const day = this.getWeekDay() ?? '*'; + return `${minute} ${hour} ${date} * ${day}`; + } + + getSchedule() { + const time = this.getTime(); + const day = this.getWeekDay(); + const date = this.getMonthDay(); + + let schedule = `Every ${this.period} `; + + if (day !== null) { + schedule += `on ${DAYS_OF_WEEK[day]} `; + } else if (date) { + schedule += `${getOrdinalNumber(date)} `; + } + + schedule += `at ${time}`; + + return schedule; + } +} diff --git a/src/shared/services/cronService/index.ts b/src/shared/services/cronService/index.ts new file mode 100644 index 00000000..d9eddf23 --- /dev/null +++ b/src/shared/services/cronService/index.ts @@ -0,0 +1,2 @@ +export * from './cronService'; +export * from './types'; diff --git a/src/shared/services/cronService/types.ts b/src/shared/services/cronService/types.ts new file mode 100644 index 00000000..2c46f3b6 --- /dev/null +++ b/src/shared/services/cronService/types.ts @@ -0,0 +1,29 @@ +export type CronSegmentValue = number | null; + +export type CronSegmentKey = 'date' | 'day' | 'hour' | 'minute'; + +export enum Period { + DAY = 'day', + WEEK = 'week', + MONTH = 'month', +} + +export enum DayOfWeek { + SUNDAY, + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY, +} + +export enum DayOfWeekName { + SUNDAY = 'Sunday', + MONDAY = 'Monday', + TUESDAY = 'Tuesday', + WEDNESDAY = 'Wednesday', + THURSDAY = 'Thursday', + FRIDAY = 'Friday', + SATURDAY = 'Saturday', +} diff --git a/src/shared/services/index.ts b/src/shared/services/index.ts new file mode 100644 index 00000000..35c276dd --- /dev/null +++ b/src/shared/services/index.ts @@ -0,0 +1 @@ +export * from './cronService'; diff --git a/src/shared/ui/Calendar/index.tsx b/src/shared/ui/Calendar/index.tsx new file mode 100644 index 00000000..622df3bf --- /dev/null +++ b/src/shared/ui/Calendar/index.tsx @@ -0,0 +1,5 @@ +import { Dayjs } from 'dayjs'; +import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs'; +import generateCalendar from 'antd/es/calendar/generateCalendar'; + +export const Calendar = generateCalendar(dayjsGenerateConfig); diff --git a/src/shared/ui/CronSelect/components/DynamicSelect/index.tsx b/src/shared/ui/CronSelect/components/DynamicSelect/index.tsx new file mode 100644 index 00000000..a93d2117 --- /dev/null +++ b/src/shared/ui/CronSelect/components/DynamicSelect/index.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Select } from 'antd'; +import { getOrdinalNumber } from '@shared/utils'; +import { Period } from '@shared/services'; + +import classes from '../../styles.module.less'; +import { DAYS_OF_MONTH_SELECT_OPTIONS, DAYS_OF_WEEK_SELECT_OPTIONS } from '../../constants'; + +import { DynamicSelectProps } from './types'; + +export const DynamicSelect = ({ period, weekDay, monthDay, onChangeWeekDay, onChangeMonthDay }: DynamicSelectProps) => { + switch (period) { + case Period.WEEK: + return ( + + {getOrdinalNumber(monthDay!, true)} + + ); + default: + return null; + } +}; diff --git a/src/shared/ui/CronSelect/components/DynamicSelect/types.ts b/src/shared/ui/CronSelect/components/DynamicSelect/types.ts new file mode 100644 index 00000000..f659553c --- /dev/null +++ b/src/shared/ui/CronSelect/components/DynamicSelect/types.ts @@ -0,0 +1,15 @@ +import { CronSegmentValue, Period } from '@shared/services'; + +/** Interface as Props for component "DynamicSelect" */ +export interface DynamicSelectProps { + /** Value of period Select */ + period: Period; + /** Value of week day Select */ + weekDay: CronSegmentValue; + /** Value of month day Select */ + monthDay: CronSegmentValue; + /** Callback for changing value of week day Select */ + onChangeWeekDay: (value: CronSegmentValue) => void; + /** Callback for changing value of month day Select */ + onChangeMonthDay: (value: CronSegmentValue) => void; +} diff --git a/src/shared/ui/CronSelect/components/index.ts b/src/shared/ui/CronSelect/components/index.ts new file mode 100644 index 00000000..a7b7feee --- /dev/null +++ b/src/shared/ui/CronSelect/components/index.ts @@ -0,0 +1 @@ +export * from './DynamicSelect'; diff --git a/src/shared/ui/CronSelect/constants.ts b/src/shared/ui/CronSelect/constants.ts new file mode 100644 index 00000000..d2fbf481 --- /dev/null +++ b/src/shared/ui/CronSelect/constants.ts @@ -0,0 +1,28 @@ +import { DayOfWeek, DayOfWeekName, Period } from '@shared/services'; +import { prepareOptionsForSelect } from '@shared/ui'; + +export const PERIOD_SELECT_OPTIONS = prepareOptionsForSelect({ + data: Object.values(Period), + renderLabel: (data) => data, + renderValue: (data) => data, +}); + +export const DAYS_OF_WEEK_SELECT_OPTIONS = prepareOptionsForSelect({ + data: [ + { value: DayOfWeek.MONDAY, label: DayOfWeekName.MONDAY }, + { value: DayOfWeek.TUESDAY, label: DayOfWeekName.TUESDAY }, + { value: DayOfWeek.WEDNESDAY, label: DayOfWeekName.WEDNESDAY }, + { value: DayOfWeek.THURSDAY, label: DayOfWeekName.THURSDAY }, + { value: DayOfWeek.FRIDAY, label: DayOfWeekName.FRIDAY }, + { value: DayOfWeek.SATURDAY, label: DayOfWeekName.SATURDAY }, + { value: DayOfWeek.SUNDAY, label: DayOfWeekName.SUNDAY }, + ], + renderLabel: (data) => data.label, + renderValue: (data) => data.value, +}); + +export const DAYS_OF_MONTH_SELECT_OPTIONS = prepareOptionsForSelect({ + data: Array.from({ length: 31 }, (_, index) => index + 1), + renderLabel: (data) => data, + renderValue: (data) => data, +}); diff --git a/src/shared/ui/CronSelect/hooks/index.ts b/src/shared/ui/CronSelect/hooks/index.ts new file mode 100644 index 00000000..39b816b3 --- /dev/null +++ b/src/shared/ui/CronSelect/hooks/index.ts @@ -0,0 +1 @@ +export * from './useCron'; diff --git a/src/shared/ui/CronSelect/hooks/useCron/index.ts b/src/shared/ui/CronSelect/hooks/useCron/index.ts new file mode 100644 index 00000000..ba279942 --- /dev/null +++ b/src/shared/ui/CronSelect/hooks/useCron/index.ts @@ -0,0 +1,2 @@ +export * from './useCron'; +export * from './types'; diff --git a/src/shared/ui/CronSelect/hooks/useCron/types.ts b/src/shared/ui/CronSelect/hooks/useCron/types.ts new file mode 100644 index 00000000..af0c5c55 --- /dev/null +++ b/src/shared/ui/CronSelect/hooks/useCron/types.ts @@ -0,0 +1,7 @@ +/** Interface as Props for hook "useCron" */ +export interface UseCronProps { + /** Value of cron expression like "* * * * *" */ + value?: string; + /** Callback for changing value of cron expression */ + onChange?: (value: string) => void; +} diff --git a/src/shared/ui/CronSelect/hooks/useCron/useCron.ts b/src/shared/ui/CronSelect/hooks/useCron/useCron.ts new file mode 100644 index 00000000..144eabd7 --- /dev/null +++ b/src/shared/ui/CronSelect/hooks/useCron/useCron.ts @@ -0,0 +1,42 @@ +import { CronSegmentValue, CronService, Period } from '@shared/services'; +import dayjs, { Dayjs } from 'dayjs'; + +import { UseCronProps } from './types'; + +/** Hook for handling value of CronSelect component */ +export const useCron = ({ value, onChange = () => undefined }: UseCronProps) => { + const cronService = new CronService(value); + + const handleChange = () => onChange(cronService.toString()); + + const handleChangePeriod = (period: Period) => { + cronService.setPeriod(period); + handleChange(); + }; + + const handleChangeWeekDay = (weekDay: CronSegmentValue) => { + cronService.setWeekDay(weekDay); + handleChange(); + }; + + const handleChangeMonthDay = (monthDay: CronSegmentValue) => { + cronService.setMonthDay(monthDay); + handleChange(); + }; + + const handleChangeTime = (time: Dayjs | null) => { + cronService.setTime(time?.hour(), time?.minute()); + handleChange(); + }; + + return { + period: cronService.getPeriod(), + weekDay: cronService.getWeekDay(), + monthDay: cronService.getMonthDay(), + time: dayjs(cronService.getTime(), 'HH:mm'), + handleChangePeriod, + handleChangeMonthDay, + handleChangeWeekDay, + handleChangeTime, + }; +}; diff --git a/src/shared/ui/CronSelect/index.tsx b/src/shared/ui/CronSelect/index.tsx new file mode 100644 index 00000000..7523777a --- /dev/null +++ b/src/shared/ui/CronSelect/index.tsx @@ -0,0 +1,63 @@ +import React, { memo } from 'react'; +import { Select } from 'antd'; +import { TimePicker } from '@shared/ui'; +import { Period } from '@shared/services'; + +import { PERIOD_SELECT_OPTIONS } from './constants'; +import { CronSelectProps } from './types'; +import classes from './styles.module.less'; +import { useCron } from './hooks'; +import { DynamicSelect } from './components'; + +export const CronSelect = memo(({ value, onChange }: CronSelectProps) => { + const { + period, + weekDay, + monthDay, + time, + handleChangePeriod, + handleChangeWeekDay, + handleChangeMonthDay, + handleChangeTime, + } = useCron({ value, onChange }); + + return ( +
+
+ Every: +