mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 23:29:01 +08:00
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:
parent
220edd139a
commit
fff9954da2
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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[] };
|
||||
|
||||
|
@ -19,6 +19,5 @@ export enum FeatureKeys {
|
||||
OSS = 'OSS',
|
||||
ONBOARDING = 'ONBOARDING',
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
PLANNED_MAINTENANCE = 'PLANNED_MAINTENANCE',
|
||||
GATEWAY = 'GATEWAY',
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
];
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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()) ||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 />,
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user