Schedule maintainence release changes (#5585)

* feat: schedule maintenance feedback fixes

* feat: schedule maintenance feedback fixes

* feat: code refactor

* feat: code refactor

* feat: fixed incorrect payload values from start and endTime

* feat: sorted list by updatedAt

* feat: removed dependency on BE response prop - kind

* feat: fixed timezone switching and adding different timezones
This commit is contained in:
SagarRajput-7 2024-07-31 22:30:42 +05:30 committed by GitHub
parent 220edd139a
commit fff9954da2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 323 additions and 497 deletions

View File

@ -1,6 +1,7 @@
import axios from 'api'; import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { Dayjs } from 'dayjs';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { Recurrence } from './getAllDowntimeSchedules'; import { Recurrence } from './getAllDowntimeSchedules';
@ -11,8 +12,8 @@ export interface DowntimeSchedulePayload {
alertIds: string[]; alertIds: string[];
schedule: { schedule: {
timezone?: string; timezone?: string;
startTime?: string; startTime?: string | Dayjs;
endTime?: string; endTime?: string | Dayjs;
recurrence?: Recurrence; recurrence?: Recurrence;
}; };
} }

View File

@ -1,6 +1,6 @@
import axios from 'api'; import axios from 'api';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { Option } from 'container/PlannedDowntime/DropdownWithSubMenu/DropdownWithSubMenu'; import { Option } from 'container/PlannedDowntime/PlannedDowntimeutils';
import { useQuery, UseQueryResult } from 'react-query'; import { useQuery, UseQueryResult } from 'react-query';
export type Recurrence = { export type Recurrence = {
@ -28,6 +28,7 @@ export interface DowntimeSchedules {
createdBy: string | null; createdBy: string | null;
updatedAt: string | null; updatedAt: string | null;
updatedBy: string | null; updatedBy: string | null;
kind: string | null;
} }
export type PayloadProps = { data: DowntimeSchedules[] }; export type PayloadProps = { data: DowntimeSchedules[] };

View File

@ -19,6 +19,5 @@ export enum FeatureKeys {
OSS = 'OSS', OSS = 'OSS',
ONBOARDING = 'ONBOARDING', ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT', CHAT_SUPPORT = 'CHAT_SUPPORT',
PLANNED_MAINTENANCE = 'PLANNED_MAINTENANCE',
GATEWAY = 'GATEWAY', GATEWAY = 'GATEWAY',
} }

View File

@ -1,136 +0,0 @@
.options {
width: 100%;
.option {
padding: 8px 10px;
cursor: pointer;
overflow: auto;
}
.option:hover {
background-color: var(--bg-slate-200);
}
}
.submenu-container {
position: absolute;
top: 0;
right: 50%;
z-index: 1;
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-500);
max-height: 300px;
overflow-y: auto;
width: 160px;
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px;
border-radius: 4px;
.submenu-checkbox {
padding: 0px;
}
}
.dropdown-submenu {
.dropdown-input {
box-sizing: border-box;
margin: 0;
padding: 4.5px 11px;
color: rgba(255, 255, 255, 0.85);
font-size: 13px;
line-height: 1.6153846153846154;
list-style: none;
font-family: Inter;
position: relative;
display: inline-block;
min-width: 0;
transition: all 0.2s;
}
.ant-popover-inner {
padding: 0px;
}
}
.options-container {
position: relative;
--arrow-x: 175px;
--arrow-y: 266px;
width: 350px;
box-sizing: border-box;
margin: 0;
padding: 4px;
color: var(--bg-vanilla-400);
font-size: 12px;
line-height: 1.6153846153846154;
list-style: none;
font-family: Inter;
z-index: 1050;
overflow: hidden;
font-variant: initial;
background-color: var(--bg-ink-400);
border-radius: 2px;
outline: none;
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.3);
}
.submenu-popover {
.ant-popover-inner {
padding: 0px;
}
.option {
padding: 8px 10px;
cursor: pointer;
overflow: auto;
}
.option:hover {
background-color: var(--bg-slate-200);
}
}
.save-option-btn {
height: 24px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
align-self: flex-end;
width: 60px;
}
.submenu-header {
color: var(--bg-vanilla-200);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
padding-bottom: 8px;
text-transform: uppercase;
}
.lightMode {
.option:hover {
background-color: var(--bg-vanilla-200);
}
.submenu-container {
background-color: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
}
.dropdown-submenu {
.dropdown-input {
color: var(--bg-slate-100);
}
}
.options-container {
color: var(--bg-slate-100);
background-color: var(--bg-vanilla-100);
}
.submenu-header {
color: var(--bg-slate-200);
}
}

View File

@ -1,230 +0,0 @@
import './DropdownWithSubMenu.styles.scss';
import { CheckOutlined } from '@ant-design/icons';
import { Button, Checkbox, Popover, Typography } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { FormInstance } from 'antd/lib';
import { useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
import { recurrenceOptions } from '../PlannedDowntimeutils';
interface SubOption {
label: string;
value: string;
}
export interface Option {
label: string;
value: string;
submenu?: SubOption[];
}
interface DropdownProps {
options: Option[];
form: FormInstance<any>;
setRecurrenceOption: React.Dispatch<
React.SetStateAction<string | undefined | null>
>;
}
export function DropdownWithSubMenu(props: DropdownProps): JSX.Element {
const { options, form, setRecurrenceOption } = props;
const [selectedOption, setSelectedOption] = useState<Option | null>(
form.getFieldValue('recurrenceSelect')
? form.getFieldValue('recurrenceSelect').repeatType
: recurrenceOptions.doesNotRepeat,
);
const [selectedSubMenuOption, setSelectedSubMenuOption] = useState<string[]>(
[],
);
const [isOpen, setIsOpen] = useState(false);
const [isSubMenuOpen, setSubMenuIsOpen] = useState(false);
const [selectionCompleted, setSelectionCompleted] = useState(false);
useEffect(() => setRecurrenceOption?.(selectedOption?.value), [
selectedOption,
setRecurrenceOption,
]);
const handleSelectOption = (option: Option): void => {
setSelectedOption(option);
if (isSubMenuOpen && !option.submenu?.length) {
setSubMenuIsOpen(false);
setSelectionCompleted(true);
} else {
setIsOpen(!!option.submenu?.length);
setSelectionCompleted(false);
}
};
useEffect(() => {
form.setFieldValue('recurrenceSelect', {
repeatType: selectedOption,
repeatOn: selectedOption?.value === 'weekly' ? selectedSubMenuOption : [],
});
}, [form, selectedOption, selectedSubMenuOption]);
const handleInputChange = (): void => {
setIsOpen(true);
};
const handleOptionKeyDown = (
e: React.KeyboardEvent<HTMLDivElement>,
option: Option,
): void => {
if (e.key === 'Enter') {
handleSelectOption(option);
}
};
const handleCheckboxChange = (
event: CheckboxChangeEvent,
value: string,
): void => {
const { checked } = event.target;
let selectedSubMenuOptions: string[] = [...(selectedSubMenuOption || [])];
if (!checked) {
selectedSubMenuOptions = selectedSubMenuOptions.filter(
(item) => item !== value,
);
} else {
selectedSubMenuOptions.push(value);
}
setSelectedSubMenuOption(selectedSubMenuOptions);
};
const handleSaveOptionClick = (): void => {
setSubMenuIsOpen(false);
setSelectionCompleted(true);
};
useEffect(() => {
if (selectionCompleted) {
setIsOpen(false);
}
}, [selectionCompleted]);
return (
<div className="dropdown-submenu">
<Popover
getPopupContainer={popupContainer}
open={isOpen}
onOpenChange={(visible): void => {
if (!visible) {
setSubMenuIsOpen(false);
}
}}
content={
<div className="options-container">
<div className="options">
{options.map((option) => {
if (option.value === recurrenceOptions.weekly.value) {
return (
<Popover
key={option.value}
placement="right"
arrow={false}
trigger="click"
rootClassName="submenu-popover"
autoAdjustOverflow
open={isSubMenuOpen}
content={
<div className="submenu-container">
<Typography.Text className="submenu-header">
repeats weekly on
</Typography.Text>
{option.submenu?.map((subMenuOption) => (
<Checkbox
onChange={(e): void =>
handleCheckboxChange(e, subMenuOption.value)
}
className="submenu-checkbox"
key={subMenuOption.value}
>
{subMenuOption.label}
</Checkbox>
))}
<Button
icon={<CheckOutlined />}
type="primary"
className="save-option-btn"
onClick={handleSaveOptionClick}
>
Save
</Button>
</div>
}
>
<div
key={option.value}
className="option"
role="option"
aria-selected={selectedOption === option}
tabIndex={0}
onClick={(): void => {
handleSelectOption(option);
setSubMenuIsOpen(true);
}}
onKeyDown={(e): void => handleOptionKeyDown(e, option)}
>
{option.label}
</div>
</Popover>
);
}
return (
<div
key={option.value}
className="option"
role="option"
aria-selected={selectedOption === option}
tabIndex={0}
onClick={(): void => {
handleSelectOption(option);
}}
onKeyDown={(e): void => handleOptionKeyDown(e, option)}
>
{option.label}
</div>
);
})}
</div>
</div>
}
trigger="click"
arrow={false}
autoAdjustOverflow
placement="bottom"
>
<input
type="text"
placeholder="Select option..."
className="dropdown-input"
value={selectedOption?.label}
onChange={handleInputChange}
onClick={(): void => setIsOpen(true)}
/>
</Popover>
</div>
);
}
export const recurrenceOption: Option[] = [
recurrenceOptions.doesNotRepeat,
recurrenceOptions.daily,
{
...recurrenceOptions.weekly,
submenu: [
{ label: 'Monday', value: 'monday' },
{ label: 'Tuesday', value: 'tuesday' },
{ label: 'Wednesday', value: 'wednesday' },
{ label: 'Thrusday', value: 'thrusday' },
{ label: 'Friday', value: 'friday' },
{ label: 'Saturday', value: 'saturday' },
{ label: 'Sunday', value: 'sunday' },
],
},
recurrenceOptions.monthly,
];

View File

@ -65,6 +65,18 @@
background: var(--bg-ink-300); background: var(--bg-ink-300);
} }
} }
.alert-rule-form {
display: flex;
gap: 8px;
align-items: end;
.alert-rule-info {
font-size: 11px;
font-weight: 300;
color: var(--bg-vanilla-400);
}
}
} }
.alert-rule-tags { .alert-rule-tags {
@ -169,7 +181,7 @@
} }
} }
.view-created-at { .schedule-created-at {
border: 1px solid var(--bg-slate-500); border: 1px solid var(--bg-slate-500);
background-color: var(--bg-ink-400); background-color: var(--bg-ink-400);
border-top: 0px; border-top: 0px;
@ -316,7 +328,7 @@
} }
} }
.delete-view-modal { .delete-schedule-modal {
width: calc(100% - 30px) !important; /* Adjust the 20px as needed */ width: calc(100% - 30px) !important; /* Adjust the 20px as needed */
max-width: 384px; max-width: 384px;
.ant-modal-content { .ant-modal-content {
@ -429,7 +441,7 @@
color: var(--bg-ink-500); color: var(--bg-ink-500);
} }
.view-created-at { .schedule-created-at {
border: 1px solid var(--bg-vanilla-300); border: 1px solid var(--bg-vanilla-300);
background-color: var(--bg-vanilla-100); background-color: var(--bg-vanilla-100);
.ant-typography { .ant-typography {
@ -459,7 +471,7 @@
} }
} }
.delete-view-modal { .delete-schedule-modal {
.ant-modal-content { .ant-modal-content {
border: 1px solid var(--bg-vanilla-200); border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100); background: var(--bg-vanilla-100);

View File

@ -13,7 +13,7 @@ import {
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import React, { ChangeEvent, useState } from 'react'; import React, { ChangeEvent, useEffect, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { PlannedDowntimeDeleteModal } from './PlannedDowntimeDeleteModal'; import { PlannedDowntimeDeleteModal } from './PlannedDowntimeDeleteModal';
@ -48,6 +48,12 @@ export function PlannedDowntime(): JSX.Element {
[data], [data],
); );
useEffect(() => {
if (!isOpen) {
form.resetFields();
}
}, [form, isOpen]);
const [searchValue, setSearchValue] = React.useState<string | number>(''); const [searchValue, setSearchValue] = React.useState<string | number>('');
const [deleteData, setDeleteData] = useState<{ id: number; name: string }>(); const [deleteData, setDeleteData] = useState<{ id: number; name: string }>();
const [isEditMode, setEditMode] = useState<boolean>(false); const [isEditMode, setEditMode] = useState<boolean>(false);
@ -128,17 +134,19 @@ export function PlannedDowntime(): JSX.Element {
setEditMode={setEditMode} setEditMode={setEditMode}
searchValue={searchValue} searchValue={searchValue}
/> />
<PlannedDowntimeForm {isOpen && (
alertOptions={alertOptions || []} <PlannedDowntimeForm
initialValues={initialValues} alertOptions={alertOptions || []}
isError={isError} initialValues={initialValues}
isLoading={isLoading} isError={isError}
isOpen={isOpen} isLoading={isLoading}
setIsOpen={setIsOpen} isOpen={isOpen}
refetchAllSchedules={refetchAllSchedules} setIsOpen={setIsOpen}
isEditMode={isEditMode} refetchAllSchedules={refetchAllSchedules}
form={form} isEditMode={isEditMode}
/> form={form}
/>
)}
<PlannedDowntimeDeleteModal <PlannedDowntimeDeleteModal
isDeleteLoading={isDeleteLoading} isDeleteLoading={isDeleteLoading}
isDeleteModalOpen={isDeleteModalOpen} isDeleteModalOpen={isDeleteModalOpen}

View File

@ -22,20 +22,20 @@ export function PlannedDowntimeDeleteModal(
onDeleteHandler, onDeleteHandler,
downtimeSchedule, downtimeSchedule,
} = props; } = props;
const hideDeleteViewModal = (): void => { const hideDeleteScheduleModal = (): void => {
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
}; };
return ( return (
<Modal <Modal
className="delete-view-modal" className="delete-schedule-modal"
title={<span className="title">Delete view</span>} title={<span className="title">Delete Schedule</span>}
open={isDeleteModalOpen} open={isDeleteModalOpen}
closable={false} closable={false}
onCancel={hideDeleteViewModal} onCancel={hideDeleteScheduleModal}
footer={[ footer={[
<Button <Button
key="cancel" key="cancel"
onClick={hideDeleteViewModal} onClick={hideDeleteScheduleModal}
className="cancel-btn" className="cancel-btn"
icon={<X size={16} />} icon={<X size={16} />}
> >
@ -48,12 +48,12 @@ export function PlannedDowntimeDeleteModal(
className="delete-btn" className="delete-btn"
disabled={isDeleteLoading} disabled={isDeleteLoading}
> >
Delete view Delete Schedule
</Button>, </Button>,
]} ]}
> >
<Typography.Text className="delete-text"> <Typography.Text className="delete-text">
{`Are you sure you want to delete - ${downtimeSchedule} view? Deleting a view is irreversible and cannot be undone.`} {`Are you sure you want to delete - ${downtimeSchedule} schedule? Deleting a schedule is irreversible and cannot be undone.`}
</Typography.Text> </Typography.Text>
</Modal> </Modal>
); );

View File

@ -1,3 +1,5 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable sonarjs/no-identical-functions */
import './PlannedDowntime.styles.scss'; import './PlannedDowntime.styles.scss';
import 'dayjs/locale/en'; import 'dayjs/locale/en';
@ -26,25 +28,30 @@ import {
ModalTitle, ModalTitle,
} from 'container/PipelinePage/PipelineListsView/styles'; } from 'container/PipelinePage/PipelineListsView/styles';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { defaultTo, isEmpty } from 'lodash-es'; import { defaultTo, isEmpty } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ALL_TIME_ZONES } from 'utils/timeZoneUtil'; import { ALL_TIME_ZONES } from 'utils/timeZoneUtil';
import {
DropdownWithSubMenu,
Option,
recurrenceOption,
} from './DropdownWithSubMenu/DropdownWithSubMenu';
import { AlertRuleTags } from './PlannedDowntimeList'; import { AlertRuleTags } from './PlannedDowntimeList';
import { import {
createEditDowntimeSchedule, createEditDowntimeSchedule,
getAlertOptionsFromIds, getAlertOptionsFromIds,
getDurationInfo, getDurationInfo,
getEndTime,
handleTimeConvertion,
isScheduleRecurring,
recurrenceOptions, recurrenceOptions,
recurrenceOptionWithSubmenu,
recurrenceWeeklyOptions,
} from './PlannedDowntimeutils'; } from './PlannedDowntimeutils';
dayjs.locale('en'); dayjs.locale('en');
dayjs.extend(utc);
dayjs.extend(timezone);
interface PlannedDowntimeFormData { interface PlannedDowntimeFormData {
name: string; name: string;
startTime: dayjs.Dayjs | string; startTime: dayjs.Dayjs | string;
@ -93,10 +100,19 @@ export function PlannedDowntimeForm(
>([]); >([]);
const alertRuleFormName = 'alertRules'; const alertRuleFormName = 'alertRules';
const [saveLoading, setSaveLoading] = useState(false); const [saveLoading, setSaveLoading] = useState(false);
const [durationUnit, setDurationUnit] = useState<string>('m'); const [durationUnit, setDurationUnit] = useState<string>(
const [selectedRecurrenceOption, setSelectedRecurrenceOption] = useState< getDurationInfo(initialValues.schedule?.recurrence?.duration as string)
string | null ?.unit || 'm',
>(); );
const [recurrenceType, setRecurrenceType] = useState<string | null>(
(initialValues.schedule?.recurrence?.repeatType as string) ||
recurrenceOptions.doesNotRepeat.value,
);
const timezoneInitialValue = !isEmpty(initialValues.schedule?.timezone)
? (initialValues.schedule?.timezone as string)
: undefined;
const { notifications } = useNotifications(); const { notifications } = useNotifications();
@ -107,9 +123,7 @@ export function PlannedDowntimeForm(
const saveHanlder = useCallback( const saveHanlder = useCallback(
async (values: PlannedDowntimeFormData) => { async (values: PlannedDowntimeFormData) => {
const formatDate = (date: string | dayjs.Dayjs): string | undefined => const shouldKeepLocalTime = !isEditMode;
!isEmpty(date) ? dayjs(date).format('YYYY-MM-DDTHH:mm:ss[Z]') : undefined;
const createEditProps: DowntimeScheduleUpdatePayload = { const createEditProps: DowntimeScheduleUpdatePayload = {
data: { data: {
alertIds: values.alertRules alertIds: values.alertRules
@ -117,9 +131,21 @@ export function PlannedDowntimeForm(
.filter((alert) => alert !== undefined) as string[], .filter((alert) => alert !== undefined) as string[],
name: values.name, name: values.name,
schedule: { schedule: {
startTime: formatDate(values.startTime), startTime: handleTimeConvertion(
values.startTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
),
timezone: values.timezone, timezone: values.timezone,
endTime: formatDate(values.endTime), endTime: values.endTime
? handleTimeConvertion(
values.endTime,
timezoneInitialValue,
values.timezone,
shouldKeepLocalTime,
)
: undefined,
recurrence: values.recurrence as Recurrence, recurrence: values.recurrence as Recurrence,
}, },
}, },
@ -152,25 +178,41 @@ export function PlannedDowntimeForm(
} }
setSaveLoading(false); setSaveLoading(false);
}, },
[initialValues.id, isEditMode, notifications, refetchAllSchedules, setIsOpen], [
initialValues.id,
isEditMode,
notifications,
refetchAllSchedules,
setIsOpen,
timezoneInitialValue,
],
); );
const onFinish = async (values: PlannedDowntimeFormData): Promise<void> => { const onFinish = async (values: PlannedDowntimeFormData): Promise<void> => {
const recurrenceData: Recurrence | undefined = const recurrenceData: Recurrence | undefined =
(values?.recurrenceSelect?.repeatType as Option)?.value === values?.recurrence?.repeatType === recurrenceOptions.doesNotRepeat.value
recurrenceOptions.doesNotRepeat.value
? undefined ? undefined
: { : {
duration: values.recurrence?.duration duration: values.recurrence?.duration
? `${values.recurrence?.duration}${durationUnit}` ? `${values.recurrence?.duration}${durationUnit}`
: undefined, : undefined,
endTime: !isEmpty(values.endTime) endTime: !isEmpty(values.endTime)
? (values.endTime as string) ? handleTimeConvertion(
values.endTime,
timezoneInitialValue,
values.timezone,
!isEditMode,
)
: undefined, : undefined,
startTime: values.startTime as string, startTime: handleTimeConvertion(
repeatOn: !values?.recurrenceSelect?.repeatOn?.length values.startTime,
timezoneInitialValue,
values.timezone,
!isEditMode,
),
repeatOn: !values.recurrence?.repeatOn?.length
? undefined ? undefined
: values?.recurrenceSelect?.repeatOn, : values.recurrence?.repeatOn,
repeatType: (values?.recurrenceSelect?.repeatType as Option)?.value, repeatType: values.recurrence?.repeatType,
}; };
const payloadValues = { ...values, recurrence: recurrenceData }; const payloadValues = { ...values, recurrence: recurrenceData };
@ -226,19 +268,15 @@ export function PlannedDowntimeForm(
initialValues.alertIds || [], initialValues.alertIds || [],
alertOptions, alertOptions,
), ),
endTime: initialValues.schedule?.endTime endTime: getEndTime(initialValues) ? dayjs(getEndTime(initialValues)) : '',
? dayjs(initialValues.schedule?.endTime)
: '',
startTime: initialValues.schedule?.startTime startTime: initialValues.schedule?.startTime
? dayjs(initialValues.schedule?.startTime) ? dayjs(initialValues.schedule?.startTime)
: '', : '',
recurrenceSelect: initialValues.schedule?.recurrence
? initialValues.schedule?.recurrence
: {
repeatType: recurrenceOptions.doesNotRepeat,
},
recurrence: { recurrence: {
...initialValues.schedule?.recurrence, ...initialValues.schedule?.recurrence,
repeatType: !isScheduleRecurring(initialValues?.schedule)
? recurrenceOptions.doesNotRepeat.value
: (initialValues.schedule?.recurrence?.repeatType as string),
duration: getDurationInfo( duration: getDurationInfo(
initialValues.schedule?.recurrence?.duration as string, initialValues.schedule?.recurrence?.duration as string,
)?.value, )?.value,
@ -283,101 +321,118 @@ export function PlannedDowntimeForm(
layout="vertical" layout="vertical"
className="createForm" className="createForm"
onFinish={onFinish} onFinish={onFinish}
onValuesChange={(): void => {
setRecurrenceType(form.getFieldValue('recurrence')?.repeatType as string);
}}
autoComplete="off" autoComplete="off"
> >
<Form.Item <Form.Item label="Name" name="name" rules={formValidationRules}>
label="Name"
name="name"
required={false}
rules={formValidationRules}
>
<Input placeholder="e.g. Upgrade downtime" /> <Input placeholder="e.g. Upgrade downtime" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="Starts from" label="Starts from"
name="startTime" name="startTime"
required={false}
rules={formValidationRules} rules={formValidationRules}
className="formItemWithBullet" className="formItemWithBullet"
getValueProps={(value): any => ({
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
})}
> >
<DatePicker <DatePicker
format={customFormat} format={(date): string =>
dayjs(date).tz(timezoneInitialValue).format(customFormat)
}
showTime showTime
renderExtraFooter={datePickerFooter} renderExtraFooter={datePickerFooter}
showNow={false}
popupClassName="datePicker" popupClassName="datePicker"
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="Repeats every" label="Repeats every"
name="recurrenceSelect" name={['recurrence', 'repeatType']}
required={false}
rules={formValidationRules} rules={formValidationRules}
> >
<DropdownWithSubMenu <Select
options={recurrenceOption} placeholder="Select option..."
form={form} options={recurrenceOptionWithSubmenu}
setRecurrenceOption={setSelectedRecurrenceOption}
/> />
</Form.Item> </Form.Item>
{selectedRecurrenceOption !== recurrenceOptions.doesNotRepeat.value && ( {recurrenceType === recurrenceOptions.weekly.value && (
<Form.Item <Form.Item
label="Duration" label="Weekly occurernce"
name={['recurrence', 'duration']} name={['recurrence', 'repeatOn']}
required={false}
rules={formValidationRules} rules={formValidationRules}
> >
<Input <Select
addonAfter={ placeholder="Select option..."
<Select mode="multiple"
defaultValue="m" options={Object.values(recurrenceWeeklyOptions)}
value={
getDurationInfo(
initialValues.schedule?.recurrence?.duration as string,
)?.unit
}
onChange={(value): void => setDurationUnit(value)}
>
<Select.Option value="m">Mins</Select.Option>
<Select.Option value="h">Hours</Select.Option>
</Select>
}
className="duration-input"
type="number"
placeholder="Enter duration"
min={1}
onWheel={(e): void => e.currentTarget.blur()}
/> />
</Form.Item> </Form.Item>
)}{' '} )}
<Form.Item {recurrenceType &&
label="Timezone" recurrenceType !== recurrenceOptions.doesNotRepeat.value && (
name="timezone" <Form.Item
required={false} label="Duration"
rules={formValidationRules} name={['recurrence', 'duration']}
> rules={formValidationRules}
>
<Input
addonAfter={
<Select
defaultValue="m"
value={durationUnit}
onChange={(value): void => {
setDurationUnit(value);
}}
>
<Select.Option value="m">Mins</Select.Option>
<Select.Option value="h">Hours</Select.Option>
</Select>
}
className="duration-input"
type="number"
placeholder="Enter duration"
min={1}
onWheel={(e): void => e.currentTarget.blur()}
/>
</Form.Item>
)}
<Form.Item label="Timezone" name="timezone" rules={formValidationRules}>
<Select options={timeZoneItems} placeholder="Select timezone" showSearch /> <Select options={timeZoneItems} placeholder="Select timezone" showSearch />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="Ends on" label="Ends on"
name="endTime" name="endTime"
required={false} required={recurrenceType === recurrenceOptions.doesNotRepeat.value}
rules={[ rules={[
{ {
required: required: recurrenceType === recurrenceOptions.doesNotRepeat.value,
selectedRecurrenceOption === recurrenceOptions.doesNotRepeat.value,
}, },
]} ]}
className="formItemWithBullet" className="formItemWithBullet"
getValueProps={(value): any => ({
value: value ? dayjs(value).tz(timezoneInitialValue) : undefined,
})}
> >
<DatePicker <DatePicker
format={customFormat} format={(date): string =>
dayjs(date).tz(timezoneInitialValue).format(customFormat)
}
showTime showTime
showNow={false}
renderExtraFooter={datePickerFooter} renderExtraFooter={datePickerFooter}
popupClassName="datePicker" popupClassName="datePicker"
/> />
</Form.Item> </Form.Item>
<div> <div>
<Typography style={{ marginBottom: 8 }}>Silence Alerts</Typography> <div className="alert-rule-form">
<Typography style={{ marginBottom: 8 }}>Silence Alerts</Typography>
<Typography style={{ marginBottom: 8 }} className="alert-rule-info">
(Leave empty to silence all alerts)
</Typography>
</div>
<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>
<AlertRuleTags <AlertRuleTags
closable closable
@ -385,7 +440,7 @@ export function PlannedDowntimeForm(
handleClose={handleClose} handleClose={handleClose}
/> />
</Form.Item> </Form.Item>
<Form.Item name={alertRuleFormName} rules={formValidationRules}> <Form.Item name={alertRuleFormName}>
<Select <Select
placeholder="Search for alerts rules or groups..." placeholder="Search for alerts rules or groups..."
mode="multiple" mode="multiple"
@ -393,7 +448,11 @@ export function PlannedDowntimeForm(
loading={isLoading} loading={isLoading}
tagRender={noTagRenderer} tagRender={noTagRenderer}
onChange={handleChange} onChange={handleChange}
showSearch
options={alertOptions} options={alertOptions}
filterOption={(input, option): boolean =>
(option?.label as string)?.toLowerCase()?.includes(input.toLowerCase())
}
notFoundContent={ notFoundContent={
isLoading ? ( isLoading ? (
<span> <span>

View File

@ -22,6 +22,7 @@ import {
formatDateTime, formatDateTime,
getAlertOptionsFromIds, getAlertOptionsFromIds,
getDuration, getDuration,
getEndTime,
recurrenceInfo, recurrenceInfo,
} from './PlannedDowntimeutils'; } from './PlannedDowntimeutils';
@ -122,6 +123,7 @@ export function CollapseListContent({
updated_at, updated_at,
updated_by_name, updated_by_name,
alertOptions, alertOptions,
timezone,
}: { }: {
created_at?: string; created_at?: string;
created_by_name?: string; created_by_name?: string;
@ -131,6 +133,7 @@ export function CollapseListContent({
updated_at?: string; updated_at?: string;
updated_by_name?: string; updated_by_name?: string;
alertOptions?: DefaultOptionType[]; alertOptions?: DefaultOptionType[];
timezone?: string;
}): JSX.Element { }): JSX.Element {
const renderItems = (title: string, value: ReactNode): JSX.Element => ( const renderItems = (title: string, value: ReactNode): JSX.Element => (
<div className="render-item-collapse-list"> <div className="render-item-collapse-list">
@ -180,6 +183,7 @@ export function CollapseListContent({
'-' '-'
), ),
)} )}
{renderItems('Timezone', <Typography>{timezone || '-'}</Typography>)}
{renderItems('Repeats', <Typography>{recurrenceInfo(repeats)}</Typography>)} {renderItems('Repeats', <Typography>{recurrenceInfo(repeats)}</Typography>)}
{renderItems( {renderItems(
'Alerts silenced', 'Alerts silenced',
@ -220,13 +224,15 @@ export function CustomCollapseList(
setModalOpen, setModalOpen,
handleDeleteDowntime, handleDeleteDowntime,
setEditMode, setEditMode,
kind,
} = props; } = props;
const scheduleTime = schedule?.startTime ? schedule.startTime : createdAt; const scheduleTime = schedule?.startTime ? schedule.startTime : createdAt;
// Combine time and date // Combine time and date
const formattedDateAndTime = `Start time ⎯ ${formatDateTime( const formattedDateAndTime = `Start time ⎯ ${formatDateTime(
defaultTo(scheduleTime, ''), defaultTo(scheduleTime, ''),
)}`; )} ${schedule?.timezone}`;
const endTime = getEndTime({ kind, schedule });
return ( return (
<> <>
@ -255,15 +261,19 @@ export function CustomCollapseList(
<CollapseListContent <CollapseListContent
created_at={defaultTo(createdAt, '')} created_at={defaultTo(createdAt, '')}
created_by_name={defaultTo(createdBy, '')} created_by_name={defaultTo(createdBy, '')}
timeframe={[schedule?.startTime, schedule?.endTime]} timeframe={[
schedule?.startTime?.toString(),
typeof endTime === 'string' ? endTime : endTime?.toString(),
]}
repeats={schedule?.recurrence} repeats={schedule?.recurrence}
updated_at={defaultTo(updatedAt, '')} updated_at={defaultTo(updatedAt, '')}
updated_by_name={defaultTo(updatedBy, '')} updated_by_name={defaultTo(updatedBy, '')}
alertOptions={alertOptions} alertOptions={alertOptions}
timezone={defaultTo(schedule?.timezone, '')}
/> />
</Panel> </Panel>
</Collapse> </Collapse>
<div className="view-created-at"> <div className="schedule-created-at">
<CalendarClock size={14} /> <CalendarClock size={14} />
<Typography.Text>{formattedDateAndTime}</Typography.Text> <Typography.Text>{formattedDateAndTime}</Typography.Text>
</div> </div>
@ -314,6 +324,12 @@ export function PlannedDowntimeList({
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const tableData = (downtimeSchedules.data?.data?.data || []) const tableData = (downtimeSchedules.data?.data?.data || [])
.sort((a, b): number => {
if (a?.updatedAt && b?.updatedAt) {
return b.updatedAt.localeCompare(a.updatedAt);
}
return 0;
})
?.filter( ?.filter(
(data) => (data) =>
data?.name?.includes(searchValue.toLocaleString()) || data?.name?.includes(searchValue.toLocaleString()) ||

View File

@ -12,6 +12,7 @@ import updateDowntimeSchedule, {
} from 'api/plannedDowntime/updateDowntimeSchedule'; } from 'api/plannedDowntime/updateDowntimeSchedule';
import { showErrorNotification } from 'components/ExplorerCard/utils'; import { showErrorNotification } from 'components/ExplorerCard/utils';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { isEmpty, isEqual } from 'lodash-es';
import { UseMutateAsyncFunction } from 'react-query'; import { UseMutateAsyncFunction } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
@ -42,7 +43,8 @@ export const formatDateTime = (dateTimeString?: string | null): string => {
if (!dateTimeString) { if (!dateTimeString) {
return 'N/A'; return 'N/A';
} }
return dayjs(dateTimeString).format('MMM DD, YYYY h:mm A');
return dayjs(dateTimeString.slice(0, 19)).format('MMM DD, YYYY h:mm A');
}; };
export const getAlertOptionsFromIds = ( export const getAlertOptionsFromIds = (
@ -149,6 +151,15 @@ export const recurrenceOptions = {
monthly: { label: 'Monthly', value: 'monthly' }, monthly: { label: 'Monthly', value: 'monthly' },
}; };
export const recurrenceWeeklyOptions = {
monday: { label: 'Monday', value: 'monday' },
tuesday: { label: 'Tuesday', value: 'tuesday' },
wednesday: { label: 'Wednesday', value: 'wednesday' },
thursday: { label: 'Thursday', value: 'thursday' },
friday: { label: 'Friday', value: 'friday' },
saturday: { label: 'Saturday', value: 'saturday' },
sunday: { label: 'Sunday', value: 'sunday' },
};
interface DurationInfo { interface DurationInfo {
value: number; value: number;
unit: string; unit: string;
@ -160,17 +171,108 @@ export function getDurationInfo(
if (!durationString) { if (!durationString) {
return null; return null;
} }
// Regular expression to extract value and unit from the duration string
const durationRegex = /(\d+)([hms])/; // Regular expressions to extract hours, minutes
// Match the value and unit parts in the duration string const hoursRegex = /(\d+)h/;
const match = durationString.match(durationRegex); const minutesRegex = /(\d+)m/;
if (match && match.length >= 3) {
// Extract value and unit from the match // Extract hours, minutes from the duration string
const value = parseInt(match[1], 10); const hoursMatch = durationString.match(hoursRegex);
const unit = match[2]; const minutesMatch = durationString.match(minutesRegex);
// Return duration info object
return { value, unit }; // Convert extracted values to integers, defaulting to 0 if not found
const hours = hoursMatch ? parseInt(hoursMatch[1], 10) : 0;
const minutes = minutesMatch ? parseInt(minutesMatch[1], 10) : 0;
// If there are no minutes and only hours, return the hours
if (hours > 0 && minutes === 0) {
return { value: hours, unit: 'h' };
} }
// If no value or unit part found, return null
return null; // Otherwise, calculate the total duration in minutes
const totalMinutes = hours * 60 + minutes;
return { value: totalMinutes, unit: 'm' };
}
export interface Option {
label: string;
value: string;
}
export const recurrenceOptionWithSubmenu: Option[] = [
recurrenceOptions.doesNotRepeat,
recurrenceOptions.daily,
recurrenceOptions.weekly,
recurrenceOptions.monthly,
];
export const getRecurrenceOptionFromValue = (
value?: string | Option | null,
): Option | null | undefined => {
if (!value) {
return null;
}
if (typeof value === 'string') {
return Object.values(recurrenceOptions).find(
(option) => option.value === value,
);
}
return value;
};
export const getEndTime = ({
kind,
schedule,
}: Partial<
DowntimeSchedules & {
editMode: boolean;
}
>): string | dayjs.Dayjs => {
if (kind === 'fixed') {
return schedule?.endTime || '';
}
return schedule?.recurrence?.endTime || '';
};
export const isScheduleRecurring = (
schedule?: DowntimeSchedules['schedule'],
): boolean => (schedule ? !isEmpty(schedule?.recurrence) : false);
function convertUtcOffsetToTimezoneOffset(offsetMinutes: number): string {
const sign = offsetMinutes >= 0 ? '+' : '-';
const absOffset = Math.abs(offsetMinutes);
const hours = String(Math.floor(absOffset / 60)).padStart(2, '0');
const minutes = String(absOffset % 60).padStart(2, '0');
return `${sign}${hours}:${minutes}`;
}
export function formatWithTimezone(
dateValue?: string | dayjs.Dayjs,
timezone?: string,
): string {
const parsedDate =
typeof dateValue === 'string' ? dateValue : dateValue?.format();
console.log('dateValue', parsedDate, 'timezone', timezone);
// Get the target timezone offset
const targetOffset = convertUtcOffsetToTimezoneOffset(
dayjs(dateValue).tz(timezone).utcOffset(),
);
return `${parsedDate?.substring(0, 19)}${targetOffset}`;
}
export function handleTimeConvertion(
dateValue: string | dayjs.Dayjs,
timezoneInit?: string,
timezone?: string,
shouldKeepLocalTime?: boolean,
): string {
const timezoneChanged = !isEqual(timezoneInit, timezone);
const initialTime = dayjs(dateValue).tz(timezoneInit);
const formattedTime = formatWithTimezone(initialTime, timezone);
return timezoneChanged
? formattedTime
: dayjs(dateValue).tz(timezone, shouldKeepLocalTime).format();
} }

View File

@ -1,10 +1,8 @@
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import { TabsProps } from 'antd/lib'; import { TabsProps } from 'antd/lib';
import { FeatureKeys } from 'constants/features';
import AllAlertRules from 'container/ListAlertRules'; import AllAlertRules from 'container/ListAlertRules';
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime'; import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
import TriggeredAlerts from 'container/TriggeredAlerts'; import TriggeredAlerts from 'container/TriggeredAlerts';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history'; import history from 'lib/history';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@ -13,10 +11,6 @@ function AllAlertList(): JSX.Element {
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const location = useLocation(); const location = useLocation();
const isPlannedDowntimeEnabled = useFeatureFlags(
FeatureKeys.PLANNED_MAINTENANCE,
)?.active;
const tab = urlQuery.get('tab'); const tab = urlQuery.get('tab');
const items: TabsProps['items'] = [ const items: TabsProps['items'] = [
{ label: 'Alert Rules', key: 'AlertRules', children: <AllAlertRules /> }, { label: 'Alert Rules', key: 'AlertRules', children: <AllAlertRules /> },
@ -26,7 +20,7 @@ function AllAlertList(): JSX.Element {
children: <TriggeredAlerts />, children: <TriggeredAlerts />,
}, },
{ {
label: isPlannedDowntimeEnabled ? 'Configuration' : '', label: 'Configuration',
key: 'Configuration', key: 'Configuration',
children: <PlannedDowntime />, children: <PlannedDowntime />,
}, },