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

View File

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

View File

@ -19,6 +19,5 @@ export enum FeatureKeys {
OSS = 'OSS',
ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT',
PLANNED_MAINTENANCE = 'PLANNED_MAINTENANCE',
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);
}
}
.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 {
@ -169,7 +181,7 @@
}
}
.view-created-at {
.schedule-created-at {
border: 1px solid var(--bg-slate-500);
background-color: var(--bg-ink-400);
border-top: 0px;
@ -316,7 +328,7 @@
}
}
.delete-view-modal {
.delete-schedule-modal {
width: calc(100% - 30px) !important; /* Adjust the 20px as needed */
max-width: 384px;
.ant-modal-content {
@ -429,7 +441,7 @@
color: var(--bg-ink-500);
}
.view-created-at {
.schedule-created-at {
border: 1px solid var(--bg-vanilla-300);
background-color: var(--bg-vanilla-100);
.ant-typography {
@ -459,7 +471,7 @@
}
}
.delete-view-modal {
.delete-schedule-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);

View File

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

View File

@ -22,20 +22,20 @@ export function PlannedDowntimeDeleteModal(
onDeleteHandler,
downtimeSchedule,
} = props;
const hideDeleteViewModal = (): void => {
const hideDeleteScheduleModal = (): void => {
setIsDeleteModalOpen(false);
};
return (
<Modal
className="delete-view-modal"
title={<span className="title">Delete view</span>}
className="delete-schedule-modal"
title={<span className="title">Delete Schedule</span>}
open={isDeleteModalOpen}
closable={false}
onCancel={hideDeleteViewModal}
onCancel={hideDeleteScheduleModal}
footer={[
<Button
key="cancel"
onClick={hideDeleteViewModal}
onClick={hideDeleteScheduleModal}
className="cancel-btn"
icon={<X size={16} />}
>
@ -48,12 +48,12 @@ export function PlannedDowntimeDeleteModal(
className="delete-btn"
disabled={isDeleteLoading}
>
Delete view
Delete Schedule
</Button>,
]}
>
<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>
</Modal>
);

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import updateDowntimeSchedule, {
} from 'api/plannedDowntime/updateDowntimeSchedule';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import dayjs from 'dayjs';
import { isEmpty, isEqual } from 'lodash-es';
import { UseMutateAsyncFunction } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
@ -42,7 +43,8 @@ export const formatDateTime = (dateTimeString?: string | null): string => {
if (!dateTimeString) {
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 = (
@ -149,6 +151,15 @@ export const recurrenceOptions = {
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 {
value: number;
unit: string;
@ -160,17 +171,108 @@ export function getDurationInfo(
if (!durationString) {
return null;
}
// Regular expression to extract value and unit from the duration string
const durationRegex = /(\d+)([hms])/;
// Match the value and unit parts in the duration string
const match = durationString.match(durationRegex);
if (match && match.length >= 3) {
// Extract value and unit from the match
const value = parseInt(match[1], 10);
const unit = match[2];
// Return duration info object
return { value, unit };
// Regular expressions to extract hours, minutes
const hoursRegex = /(\d+)h/;
const minutesRegex = /(\d+)m/;
// Extract hours, minutes from the duration string
const hoursMatch = durationString.match(hoursRegex);
const minutesMatch = durationString.match(minutesRegex);
// 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 { TabsProps } from 'antd/lib';
import { FeatureKeys } from 'constants/features';
import AllAlertRules from 'container/ListAlertRules';
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
import TriggeredAlerts from 'container/TriggeredAlerts';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { useLocation } from 'react-router-dom';
@ -13,10 +11,6 @@ function AllAlertList(): JSX.Element {
const urlQuery = useUrlQuery();
const location = useLocation();
const isPlannedDowntimeEnabled = useFeatureFlags(
FeatureKeys.PLANNED_MAINTENANCE,
)?.active;
const tab = urlQuery.get('tab');
const items: TabsProps['items'] = [
{ label: 'Alert Rules', key: 'AlertRules', children: <AllAlertRules /> },
@ -26,7 +20,7 @@ function AllAlertList(): JSX.Element {
children: <TriggeredAlerts />,
},
{
label: isPlannedDowntimeEnabled ? 'Configuration' : '',
label: 'Configuration',
key: 'Configuration',
children: <PlannedDowntime />,
},