feat: [SIG-582]: added planned maintenance create dialog (#4863)

* feat: [SIG-582]: added planned maintenance create dialog

* feat: [SIG-582]: added planned maintenance - listing

* feat: [SIG-582]: added alert rule select

* feat: [SIG-582]: added - planned maintenance list and createform ui fixes

* feat: [SIG-582]: added - alert-rule tag styles

* feat: [SIG-582]: added - style changes

* feat: [SIG-582]: added - crud API integration and delete modal

* feat: [SIG-582]: added - reccurrence form details

* feat: [SIG-582]: added - duration and timezone

* feat: [SIG-582]: removed console logs

* feat: [SIG-582]: added - form validation for duration and endTime

* feat: [SIG-582]: code refactor

* feat: [SIG-582]: code refactor

* feat: [SIG-582]: code refactor

* feat: [SIG-582]: code refactor

* feat: [SIG-582]: light mode styles

* feat: [SIG-582]: code refactor

* feat: [SIG-582]: code refactor and comment resolve

* feat: [SIG-582]: code refactor and removed filters

* feat: [SIG-582]: changed coming up on to start time

* feat: [SIG-582]: added planned downtime behind FF - PLANNED_MAINTENANCE
This commit is contained in:
SagarRajput-7 2024-05-24 14:02:37 +05:30 committed by GitHub
parent f818a86720
commit 7e79900973
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 2838 additions and 15 deletions

View File

@ -0,0 +1,44 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Recurrence } from './getAllDowntimeSchedules';
export interface DowntimeSchedulePayload {
name: string;
description?: string;
alertIds: string[];
schedule: {
timezone?: string;
startTime?: string;
endTime?: string;
recurrence?: Recurrence;
};
}
export interface PayloadProps {
status: string;
data: string;
}
const createDowntimeSchedule = async (
props: DowntimeSchedulePayload,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/downtime_schedules', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default createDowntimeSchedule;

View File

@ -0,0 +1,19 @@
import axios from 'api';
import { useMutation, UseMutationResult } from 'react-query';
export interface DeleteDowntimeScheduleProps {
id?: number;
}
export interface DeleteSchedulePayloadProps {
status: string;
data: string;
}
export const useDeleteDowntimeSchedule = (
props: DeleteDowntimeScheduleProps,
): UseMutationResult<DeleteSchedulePayloadProps, Error, number> =>
useMutation({
mutationKey: [props.id],
mutationFn: () => axios.delete(`/downtime_schedules/${props.id}`),
});

View File

@ -0,0 +1,50 @@
import axios from 'api';
import { AxiosError, AxiosResponse } from 'axios';
import { Option } from 'container/PlannedDowntime/DropdownWithSubMenu/DropdownWithSubMenu';
import { useQuery, UseQueryResult } from 'react-query';
export type Recurrence = {
startTime?: string | null;
endTime?: string | null;
duration?: number | string | null;
repeatType?: string | Option | null;
repeatOn?: string[] | null;
};
type Schedule = {
timezone: string | null;
startTime: string | null;
endTime: string | null;
recurrence: Recurrence | null;
};
export interface DowntimeSchedules {
id: number;
name: string | null;
description: string | null;
schedule: Schedule | null;
alertIds: string[] | null;
createdAt: string | null;
createdBy: string | null;
updatedAt: string | null;
updatedBy: string | null;
}
export type PayloadProps = { data: DowntimeSchedules[] };
export const getAllDowntimeSchedules = async (
props?: GetAllDowntimeSchedulesPayloadProps,
): Promise<AxiosResponse<PayloadProps>> =>
axios.get('/downtime_schedules', { params: props });
export interface GetAllDowntimeSchedulesPayloadProps {
active?: boolean;
recurrence?: boolean;
}
export const useGetAllDowntimeSchedules = (
props?: GetAllDowntimeSchedulesPayloadProps,
): UseQueryResult<AxiosResponse<PayloadProps>, AxiosError> =>
useQuery<AxiosResponse<PayloadProps>, AxiosError>({
queryKey: ['getAllDowntimeSchedules', props],
queryFn: () => getAllDowntimeSchedules(props),
});

View File

@ -0,0 +1,37 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { DowntimeSchedulePayload } from './createDowntimeSchedule';
export interface DowntimeScheduleUpdatePayload {
data: DowntimeSchedulePayload;
id?: number;
}
export interface PayloadProps {
status: string;
data: string;
}
const updateDowntimeSchedule = async (
props: DowntimeScheduleUpdatePayload,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.put(`/downtime_schedules/${props.id}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default updateDowntimeSchedule;

View File

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

View File

@ -0,0 +1,127 @@
.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;
}
}
.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

@ -0,0 +1,230 @@
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

@ -0,0 +1,535 @@
.createDowntimeModal {
.ant-modal-content {
padding: 16px;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
background-color: var(--bg-ink-400);
.ant-typography {
margin: 0;
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
.ant-modal-body {
.ant-divider {
margin: 16px 0;
border: 0.5px solid var(--bg-slate-500);
}
}
}
}
.createForm {
label {
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
padding-bottom: 6px;
}
.ant-picker,
input {
width: 100%;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
.alert-rule-tags {
margin-bottom: 8px;
overflow: auto;
max-height: 100px;
.ant-tag {
user-select: none;
height: 28px;
display: inline-flex;
align-items: center;
}
}
.ant-select {
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
}
.alert-rule-tags {
.ant-tag {
display: flex;
align-items: center;
border: 1px solid rgba(113, 144, 249, 0.2);
background: rgba(113, 144, 249, 0.1);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
padding-right: 0px;
border-right: 0;
color: var(--Robin-400, #7190f9);
.ant-tag-close-icon {
height: 28px;
width: 20px !important;
justify-content: center;
border-left: 1px solid rgba(113, 144, 249, 0.2);
border-radius: 0px 2px 2px 0px;
background: rgba(113, 144, 249, 0.3);
border-right: 1px solid rgba(113, 144, 249, 0.2);
margin-right: 0px;
svg {
fill: var(--bg-robin-500);
}
}
}
.non-closable-tag {
padding-right: 7px;
border-right: 1px solid rgba(113, 144, 249, 0.2);
}
.red-tag.non-closable-tag {
border-right: 1px solid rgba(242, 71, 105, 0.2) !important;
}
.red-tag {
border: 1px solid rgba(242, 71, 105, 0.2);
background: rgba(242, 71, 105, 0.1);
border-right: 0;
color: var(--Sakura-400, #f56c87);
.ant-tag-close-icon {
background: rgba(242, 71, 105, 0.3);
border-left: 1px solid rgba(242, 71, 105, 0.2);
border-right: 1px solid rgba(242, 71, 105, 0.2);
svg {
fill: var(--bg-sakura-500);
}
}
}
}
.datePicker {
.ant-picker-panel-container {
background: #262930 !important;
}
}
.planned-downtime-container {
margin-top: 70px;
display: flex;
justify-content: center;
width: 100%;
.planned-downtime-content {
width: calc(100% - 30px);
max-width: 736px;
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-lg);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 28px;
letter-spacing: -0.09px;
}
.subtitle {
color: var(---bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: -0.07px;
}
.ant-input-affix-wrapper {
margin-top: 16px;
margin-bottom: 8px;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
.ant-btn {
margin-top: 8px;
}
}
.view-created-at {
border: 1px solid var(--bg-slate-500);
background-color: var(--bg-ink-400);
border-top: 0px;
width: 100%;
padding: 8px 16px;
gap: 6px;
height: 40px;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
display: flex;
align-items: center;
.ant-typography {
color: var(--bg-vanilla-400);
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
}
}
.ant-collapse-header {
height: 56px;
align-items: center;
}
.header-content {
display: flex;
gap: 8px;
.ant-typography {
color: var(--bg-vanilla-400);
font-variant-numeric: lining-nums tabular-nums stacked-fractions
slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on, 'cpsp' on, 'case' on;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
}
.ant-tag {
border-radius: 20px;
}
.action-btn {
display: flex;
align-items: center;
gap: 20px;
cursor: pointer;
.hidden {
display: none;
}
}
}
}
}
.new-downtime-menu {
.create-downtime-menu-item {
display: flex;
align-items: center;
gap: 8px;
}
}
.planned-downtime-table {
.ant-table {
background: none !important;
}
.ant-table-cell {
padding: 0 !important;
border: 0 !important;
width: 736px;
}
.ant-table-tbody {
display: flex;
flex-direction: column;
gap: 16px;
.collapse-list {
border: 1px solid var(--bg-slate-500);
background-color: var(--bg-ink-400);
border-top-left-radius: 6px;
border-top-right-radius: 6px;
.render-item-collapse-list {
margin-bottom: 13px;
display: grid;
grid-template-columns: 128px 1fr;
}
.alert-rule-collapse-list {
width: 540px;
max-height: 100px;
overflow: auto;
.ant-tag {
height: 28px;
display: flex;
align-items: center;
}
}
.ant-collapse-content-active {
border-top: 0;
}
.ant-collapse-item {
border: none;
}
.ant-collapse-content-box {
padding: 8px 20px 12px 38px;
.render-item-value {
.ant-typography {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
}
}
}
.ant-typography {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}
.delete-view-modal {
width: calc(100% - 30px) !important; /* Adjust the 20px as needed */
max-width: 384px;
.ant-modal-content {
padding: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
padding: 16px;
background: var(--bg-ink-400);
}
.ant-modal-body {
padding: 0px 16px 28px 16px;
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: -0.07px;
}
.save-view-input {
margin-top: 8px;
display: flex;
gap: 8px;
}
.ant-color-picker-trigger {
padding: 6px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
width: 32px;
height: 32px;
.ant-color-picker-color-block {
border-radius: 50px;
width: 16px;
height: 16px;
flex-shrink: 0;
.ant-color-picker-color-block-inner {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
.ant-modal-footer {
display: flex;
justify-content: flex-end;
padding: 16px 16px;
margin: 0;
.cancel-btn {
display: flex;
align-items: center;
border: none;
border-radius: 2px;
background: var(--bg-slate-500);
}
.delete-btn {
display: flex;
align-items: center;
border: none;
border-radius: 2px;
background: var(--bg-cherry-500);
margin-left: 12px;
}
.delete-btn:hover {
color: var(--bg-vanilla-100);
background: var(--bg-cherry-600);
}
}
}
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px; /* 142.857% */
}
}
.duration-input {
.ant-select {
width: 100px;
}
}
.lightMode {
.datePicker {
.ant-picker-panel-container {
background: #ffffff !important;
}
}
.planned-downtime-container {
.planned-downtime-content {
.title {
color: var(--bg-ink-500);
}
.view-created-at {
border: 1px solid var(--bg-vanilla-300);
background-color: var(--bg-vanilla-100);
.ant-typography {
color: var(--bg-slate-100);
}
border-top: 0px !important;
}
}
}
.planned-downtime-table {
.ant-table-tbody {
.collapse-list {
border: 1px solid var(--bg-vanilla-300);
background-color: var(--bg-vanilla-100);
.ant-collapse-content-box {
.ant-typography {
color: var(--bg-slate-100);
}
}
.ant-typography {
color: var(--bg-slate-400);
}
}
}
}
.delete-view-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-200);
background: var(--bg-vanilla-100);
.ant-modal-header {
background: var(--bg-vanilla-100);
.title {
color: var(--bg-ink-500);
}
}
.ant-modal-body {
.ant-typography {
color: var(--bg-ink-500);
}
.save-view-input {
.ant-input {
background: var(--bg-vanilla-200);
color: var(--bg-ink-500);
}
}
}
.ant-modal-footer {
.cancel-btn {
background: var(--bg-vanilla-300);
color: var(--bg-ink-400);
}
}
}
}
.createDowntimeModal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-modal-header {
background-color: var(--bg-vanilla-100);
.ant-typography {
color: var(--bg-slate-100);
}
}
.ant-modal-body {
.ant-divider {
border: 0.5px solid var(--bg-vanilla-300);
}
}
}
}
.createForm {
.ant-picker,
input {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
.ant-picker .ant-picker-input input {
border: 0px !important;
}
.ant-select {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
}

View File

@ -0,0 +1,152 @@
import './PlannedDowntime.styles.scss';
import 'dayjs/locale/en';
import { PlusOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Flex, Form, Input, Typography } from 'antd';
import getAll from 'api/alerts/getAll';
import { useDeleteDowntimeSchedule } from 'api/plannedDowntime/deleteDowntimeSchedule';
import {
DowntimeSchedules,
useGetAllDowntimeSchedules,
} from 'api/plannedDowntime/getAllDowntimeSchedules';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
import { Search } from 'lucide-react';
import React, { ChangeEvent, useState } from 'react';
import { useQuery } from 'react-query';
import { PlannedDowntimeDeleteModal } from './PlannedDowntimeDeleteModal';
import { PlannedDowntimeForm } from './PlannedDowntimeForm';
import { PlannedDowntimeList } from './PlannedDowntimeList';
import {
defautlInitialValues,
deleteDowntimeHandler,
} from './PlannedDowntimeutils';
dayjs.locale('en');
export function PlannedDowntime(): JSX.Element {
const { data, isError, isLoading } = useQuery('allAlerts', {
queryFn: getAll,
cacheTime: 0,
});
const [isOpen, setIsOpen] = React.useState(false);
const [form] = Form.useForm();
const [initialValues, setInitialValues] = useState<
Partial<DowntimeSchedules & { editMode: boolean }>
>(defautlInitialValues);
const downtimeSchedules = useGetAllDowntimeSchedules();
const alertOptions = React.useMemo(
() =>
data?.payload?.map((i) => ({
label: i.alert,
value: i.id,
})),
[data],
);
const [searchValue, setSearchValue] = React.useState<string | number>('');
const [deleteData, setDeleteData] = useState<{ id: number; name: string }>();
const [isEditMode, setEditMode] = useState<boolean>(false);
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
setSearchValue(e.target.value);
};
const clearSearch = (): void => {
setSearchValue('');
};
// Delete Downtime Schedule
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { notifications } = useNotifications();
const hideDeleteDowntimeScheduleModal = (): void => {
setIsDeleteModalOpen(false);
};
const refetchAllSchedules = (): void => {
downtimeSchedules.refetch();
};
const {
mutateAsync: deleteDowntimeScheduleAsync,
isLoading: isDeleteLoading,
} = useDeleteDowntimeSchedule({ id: deleteData?.id });
const onDeleteHandler = (): void => {
deleteDowntimeHandler({
deleteDowntimeScheduleAsync,
notifications,
refetchAllSchedules,
deleteId: deleteData?.id,
hideDeleteDowntimeScheduleModal,
clearSearch,
});
};
return (
<div className="planned-downtime-container">
<div className="planned-downtime-content">
<Typography.Title className="title">Planned Downtime</Typography.Title>
<Typography.Text className="subtitle">
Create and manage planned downtimes.
</Typography.Text>
<Flex className="toolbar">
<Input
placeholder="Search for a planned downtime..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue}
onChange={handleSearch}
/>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={(): void => {
setInitialValues({ ...defautlInitialValues, editMode: false });
setIsOpen(true);
setEditMode(false);
form.resetFields();
}}
>
New downtime
</Button>
</Flex>
<br />
<PlannedDowntimeList
downtimeSchedules={downtimeSchedules}
alertOptions={alertOptions || []}
setInitialValues={setInitialValues}
setModalOpen={setIsOpen}
handleDeleteDowntime={(id, name): void => {
setDeleteData({ id, name });
setIsDeleteModalOpen(true);
}}
setEditMode={setEditMode}
searchValue={searchValue}
/>
<PlannedDowntimeForm
alertOptions={alertOptions || []}
initialValues={initialValues}
isError={isError}
isLoading={isLoading}
isOpen={isOpen}
setIsOpen={setIsOpen}
refetchAllSchedules={refetchAllSchedules}
isEditMode={isEditMode}
form={form}
/>
<PlannedDowntimeDeleteModal
isDeleteLoading={isDeleteLoading}
isDeleteModalOpen={isDeleteModalOpen}
onDeleteHandler={onDeleteHandler}
setIsDeleteModalOpen={setIsDeleteModalOpen}
downtimeSchedule={deleteData?.name || ''}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,60 @@
import './PlannedDowntime.styles.scss';
import { Button, Modal, Typography } from 'antd';
import { Trash2, X } from 'lucide-react';
import { SetStateAction } from 'react';
interface PlannedDowntimeDeleteModalProps {
isDeleteModalOpen: boolean;
setIsDeleteModalOpen: (value: SetStateAction<boolean>) => void;
onDeleteHandler: () => void;
isDeleteLoading: boolean;
downtimeSchedule: string;
}
export function PlannedDowntimeDeleteModal(
props: PlannedDowntimeDeleteModalProps,
): JSX.Element {
const {
isDeleteModalOpen,
setIsDeleteModalOpen,
isDeleteLoading,
onDeleteHandler,
downtimeSchedule,
} = props;
const hideDeleteViewModal = (): void => {
setIsDeleteModalOpen(false);
};
return (
<Modal
className="delete-view-modal"
title={<span className="title">Delete view</span>}
open={isDeleteModalOpen}
closable={false}
onCancel={hideDeleteViewModal}
footer={[
<Button
key="cancel"
onClick={hideDeleteViewModal}
className="cancel-btn"
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
icon={<Trash2 size={16} />}
onClick={onDeleteHandler}
className="delete-btn"
disabled={isDeleteLoading}
>
Delete view
</Button>,
]}
>
<Typography.Text className="delete-text">
{`Are you sure you want to delete - ${downtimeSchedule} view? Deleting a view is irreversible and cannot be undone.`}
</Typography.Text>
</Modal>
);
}

View File

@ -0,0 +1,432 @@
import './PlannedDowntime.styles.scss';
import 'dayjs/locale/en';
import { CheckOutlined } from '@ant-design/icons';
import {
Button,
DatePicker,
Divider,
Form,
FormInstance,
Input,
Modal,
Select,
Spin,
Typography,
} from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { SelectProps } from 'antd/lib';
import {
DowntimeSchedules,
Recurrence,
} from 'api/plannedDowntime/getAllDowntimeSchedules';
import { DowntimeScheduleUpdatePayload } from 'api/plannedDowntime/updateDowntimeSchedule';
import {
ModalButtonWrapper,
ModalTitle,
} from 'container/PipelinePage/PipelineListsView/styles';
import dayjs from 'dayjs';
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,
recurrenceOptions,
} from './PlannedDowntimeutils';
dayjs.locale('en');
interface PlannedDowntimeFormData {
name: string;
startTime: dayjs.Dayjs | string;
endTime: dayjs.Dayjs | string;
recurrence?: Recurrence | null;
alertRules: DefaultOptionType[];
recurrenceSelect?: Recurrence;
timezone?: string;
}
const customFormat = 'Do MMMM, YYYY ⎯ HH:mm:ss';
interface PlannedDowntimeFormProps {
initialValues: Partial<
DowntimeSchedules & {
editMode: boolean;
}
>;
alertOptions: DefaultOptionType[];
isError: boolean;
isLoading: boolean;
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
refetchAllSchedules: () => void;
isEditMode: boolean;
form: FormInstance<any>;
}
export function PlannedDowntimeForm(
props: PlannedDowntimeFormProps,
): JSX.Element {
const {
initialValues,
alertOptions,
isError,
isLoading,
isOpen,
setIsOpen,
refetchAllSchedules,
isEditMode,
form,
} = props;
const [selectedTags, setSelectedTags] = React.useState<
DefaultOptionType | DefaultOptionType[]
>([]);
const alertRuleFormName = 'alertRules';
const [saveLoading, setSaveLoading] = useState(false);
const [durationUnit, setDurationUnit] = useState<string>('m');
const [selectedRecurrenceOption, setSelectedRecurrenceOption] = useState<
string | null
>();
const { notifications } = useNotifications();
const datePickerFooter = (mode: any): any =>
mode === 'time' ? (
<span style={{ color: 'gray' }}>Please select the time</span>
) : null;
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 createEditProps: DowntimeScheduleUpdatePayload = {
data: {
alertIds: values.alertRules
.map((alert) => alert.value)
.filter((alert) => alert !== undefined) as string[],
name: values.name,
schedule: {
startTime: formatDate(values.startTime),
timezone: values.timezone,
endTime: formatDate(values.endTime),
recurrence: values.recurrence as Recurrence,
},
},
id: isEditMode ? initialValues.id : undefined,
};
setSaveLoading(true);
try {
const response = await createEditDowntimeSchedule({ ...createEditProps });
if (response.message === 'success') {
setIsOpen(false);
notifications.success({
message: 'Success',
description: isEditMode
? 'Schedule updated successfully'
: 'Schedule created successfully',
});
refetchAllSchedules();
} else {
notifications.error({
message: 'Error',
description: response.error || 'unexpected_error',
});
}
} catch (e) {
notifications.error({
message: 'Error',
description: 'unexpected_error',
});
}
setSaveLoading(false);
},
[initialValues.id, isEditMode, notifications, refetchAllSchedules, setIsOpen],
);
const onFinish = async (values: PlannedDowntimeFormData): Promise<void> => {
const recurrenceData: Recurrence | undefined =
(values?.recurrenceSelect?.repeatType as Option)?.value ===
recurrenceOptions.doesNotRepeat.value
? undefined
: {
duration: values.recurrence?.duration
? `${values.recurrence?.duration}${durationUnit}`
: undefined,
endTime: !isEmpty(values.endTime)
? (values.endTime as string)
: undefined,
startTime: values.startTime as string,
repeatOn: !values?.recurrenceSelect?.repeatOn?.length
? undefined
: values?.recurrenceSelect?.repeatOn,
repeatType: (values?.recurrenceSelect?.repeatType as Option)?.value,
};
const payloadValues = { ...values, recurrence: recurrenceData };
await saveHanlder(payloadValues);
};
const formValidationRules = [
{
required: true,
},
];
const handleOk = async (): Promise<void> => {
try {
await form.validateFields();
} catch (error) {
console.error(error);
}
};
const handleCancel = (): void => {
setIsOpen(false);
};
const handleChange = (
_value: string,
options: DefaultOptionType | DefaultOptionType[],
): void => {
form.setFieldValue(alertRuleFormName, options);
setSelectedTags(options);
};
const noTagRenderer: SelectProps['tagRender'] = () => (
// eslint-disable-next-line react/jsx-no-useless-fragment
<></>
);
const handleClose = (removedTag: DefaultOptionType['value']): void => {
if (!removedTag) {
return;
}
const newTags = selectedTags.filter(
(tag: DefaultOptionType) => tag.value !== removedTag,
);
form.setFieldValue(alertRuleFormName, newTags);
setSelectedTags(newTags);
};
const formatedInitialValues = useMemo(() => {
const formData: PlannedDowntimeFormData = {
name: defaultTo(initialValues.name, ''),
alertRules: getAlertOptionsFromIds(
initialValues.alertIds || [],
alertOptions,
),
endTime: initialValues.schedule?.endTime
? dayjs(initialValues.schedule?.endTime)
: '',
startTime: initialValues.schedule?.startTime
? dayjs(initialValues.schedule?.startTime)
: '',
recurrenceSelect: initialValues.schedule?.recurrence
? initialValues.schedule?.recurrence
: {
repeatType: recurrenceOptions.doesNotRepeat,
},
recurrence: {
...initialValues.schedule?.recurrence,
duration: getDurationInfo(
initialValues.schedule?.recurrence?.duration as string,
)?.value,
},
timezone: initialValues.schedule?.timezone as string,
};
return formData;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValues]);
useEffect(() => {
setSelectedTags(formatedInitialValues.alertRules);
form.setFieldsValue({ ...formatedInitialValues });
}, [form, formatedInitialValues, initialValues]);
const timeZoneItems: DefaultOptionType[] = ALL_TIME_ZONES.map(
(timezone: string) => ({
label: timezone,
value: timezone,
key: timezone,
}),
);
return (
<Modal
title={
<ModalTitle level={4}>
{isEditMode ? 'Edit planned downtime' : 'New planned downtime'}
</ModalTitle>
}
centered
open={isOpen}
className="createDowntimeModal"
width={384}
onCancel={handleCancel}
footer={null}
>
<Divider plain />
<Form<PlannedDowntimeFormData>
name={initialValues.editMode ? 'edit-form' : 'create-form'}
form={form}
layout="vertical"
className="createForm"
onFinish={onFinish}
autoComplete="off"
>
<Form.Item
label="Name"
name="name"
required={false}
rules={formValidationRules}
>
<Input placeholder="e.g. Upgrade downtime" />
</Form.Item>
<Form.Item
label="Starts from"
name="startTime"
required={false}
rules={formValidationRules}
className="formItemWithBullet"
>
<DatePicker
format={customFormat}
showTime
renderExtraFooter={datePickerFooter}
popupClassName="datePicker"
/>
</Form.Item>
<Form.Item
label="Repeats every"
name="recurrenceSelect"
required={false}
rules={formValidationRules}
>
<DropdownWithSubMenu
options={recurrenceOption}
form={form}
setRecurrenceOption={setSelectedRecurrenceOption}
/>
</Form.Item>
{selectedRecurrenceOption !== recurrenceOptions.doesNotRepeat.value && (
<Form.Item
label="Duration"
name={['recurrence', 'duration']}
required={false}
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()}
/>
</Form.Item>
)}{' '}
<Form.Item
label="Timezone"
name="timezone"
required={false}
rules={formValidationRules}
>
<Select options={timeZoneItems} placeholder="Select timezone" showSearch />
</Form.Item>
<Form.Item
label="Ends on"
name="endTime"
required={false}
rules={[
{
required:
selectedRecurrenceOption === recurrenceOptions.doesNotRepeat.value,
},
]}
className="formItemWithBullet"
>
<DatePicker
format={customFormat}
showTime
renderExtraFooter={datePickerFooter}
popupClassName="datePicker"
/>
</Form.Item>
<div>
<Typography style={{ marginBottom: 8 }}>Silence Alerts</Typography>
<Form.Item noStyle shouldUpdate>
<AlertRuleTags
closable
selectedTags={selectedTags}
handleClose={handleClose}
/>
</Form.Item>
<Form.Item name={alertRuleFormName} rules={formValidationRules}>
<Select
placeholder="Search for alerts rules or groups..."
mode="multiple"
status={isError ? 'error' : undefined}
loading={isLoading}
tagRender={noTagRenderer}
onChange={handleChange}
options={alertOptions}
notFoundContent={
isLoading ? (
<span>
<Spin size="small" /> Loading...
</span>
) : (
<span>No alert available.</span>
)
}
>
{alertOptions?.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</Form.Item>
</div>
<Form.Item style={{ marginBottom: 0 }}>
<ModalButtonWrapper>
<Button
key="submit"
type="primary"
htmlType="submit"
icon={<CheckOutlined />}
onClick={handleOk}
loading={saveLoading || isLoading}
>
{isEditMode ? 'Update downtime schedule' : 'Add downtime schedule'}
</Button>
</ModalButtonWrapper>
</Form.Item>
</Form>
</Modal>
);
}

View File

@ -0,0 +1,348 @@
/* eslint-disable react/require-default-props */
import './PlannedDowntime.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Collapse, Flex, Space, Table, Tag, Tooltip, Typography } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { TableProps } from 'antd/lib';
import {
DowntimeSchedules,
PayloadProps,
Recurrence,
} from 'api/plannedDowntime/getAllDowntimeSchedules';
import { AxiosError, AxiosResponse } from 'axios';
import cx from 'classnames';
import { useNotifications } from 'hooks/useNotifications';
import { defaultTo } from 'lodash-es';
import { CalendarClock, PenLine, Trash2 } from 'lucide-react';
import { ReactNode, useEffect } from 'react';
import { UseQueryResult } from 'react-query';
import {
formatDateTime,
getAlertOptionsFromIds,
getDuration,
recurrenceInfo,
} from './PlannedDowntimeutils';
const { Panel } = Collapse;
interface AlertRuleTagsProps {
selectedTags: DefaultOptionType | DefaultOptionType[];
closable: boolean;
handleClose?: (removedTag: DefaultOptionType['value']) => void;
classname?: string;
}
export function AlertRuleTags(props: AlertRuleTagsProps): JSX.Element {
const { closable, selectedTags, handleClose, classname } = props;
return (
<Space
wrap
style={{ marginBottom: 8 }}
className={cx('alert-rule-tags', classname)}
>
{selectedTags?.map((tag: DefaultOptionType, index: number) => {
const isLongTag = (tag?.label as string)?.length > 20;
const tagElem = (
<Tag
key={tag.value}
onClose={(): void => handleClose?.(tag?.value)}
closable={closable}
className={cx(
{ 'red-tag': index % 2 },
{ 'non-closable-tag': !closable },
)}
>
<span>
{isLongTag
? `${(tag?.label as string | null)?.slice(0, 20)}...`
: tag?.label}
</span>
</Tag>
);
return isLongTag ? (
<Tooltip title={tag?.label} key={tag?.value}>
{tagElem}
</Tooltip>
) : (
tagElem
);
})}
</Space>
);
}
function HeaderComponent({
name,
duration,
handleEdit,
handleDelete,
}: {
name: string;
duration: string;
handleEdit: () => void;
handleDelete: () => void;
}): JSX.Element {
return (
<Flex className="header-content" justify="space-between">
<Flex gap={8}>
<Typography>{name}</Typography>
<Tag>{duration}</Tag>
</Flex>
<div className="action-btn">
<PenLine
size={14}
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
handleEdit();
}}
/>
<Trash2
size={14}
color={Color.BG_CHERRY_500}
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
handleDelete();
}}
/>
</div>
</Flex>
);
}
export function CollapseListContent({
created_at,
created_by_name,
created_by_email,
timeframe,
repeats,
updated_at,
updated_by_name,
alertOptions,
}: {
created_at?: string;
created_by_name?: string;
created_by_email?: string;
timeframe: [string | undefined | null, string | undefined | null];
repeats?: Recurrence | null;
updated_at?: string;
updated_by_name?: string;
alertOptions?: DefaultOptionType[];
}): JSX.Element {
const renderItems = (title: string, value: ReactNode): JSX.Element => (
<div className="render-item-collapse-list">
<Typography>{title}</Typography>
<div className="render-item-value">{value}</div>
</div>
);
return (
<Flex vertical>
{renderItems(
'Created by',
created_by_name ? (
<Flex gap={8}>
<Typography>{created_by_name}</Typography>
{created_by_email && (
<Tag style={{ borderRadius: 20 }}>{created_by_email}</Tag>
)}
</Flex>
) : (
'-'
),
)}
{renderItems(
'Created on',
created_at ? (
<Typography>{`${formatDateTime(created_at)}`}</Typography>
) : (
'-'
),
)}
{updated_at &&
renderItems(
'Updated on',
<Typography>{`${formatDateTime(updated_at)}`}</Typography>,
)}
{updated_by_name &&
renderItems('Updated by', <Typography>{updated_by_name}</Typography>)}
{renderItems(
'Timeframe',
timeframe[0] || timeframe[1] ? (
<Typography>{`${formatDateTime(timeframe[0])}${formatDateTime(
timeframe[1],
)}`}</Typography>
) : (
'-'
),
)}
{renderItems('Repeats', <Typography>{recurrenceInfo(repeats)}</Typography>)}
{renderItems(
'Alerts silenced',
alertOptions?.length ? (
<AlertRuleTags
closable={false}
classname="alert-rule-collapse-list"
selectedTags={alertOptions}
/>
) : (
'-'
),
)}
</Flex>
);
}
export function CustomCollapseList(
props: DowntimeSchedulesTableData & {
setInitialValues: React.Dispatch<
React.SetStateAction<Partial<DowntimeSchedules>>
>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteDowntime: (id: number, name: string) => void;
setEditMode: React.Dispatch<React.SetStateAction<boolean>>;
},
): JSX.Element {
const {
createdAt,
createdBy,
schedule,
updatedAt,
updatedBy,
name,
id,
alertOptions,
setInitialValues,
setModalOpen,
handleDeleteDowntime,
setEditMode,
} = props;
const scheduleTime = schedule?.startTime ? schedule.startTime : createdAt;
// Combine time and date
const formattedDateAndTime = `Start time ⎯ ${formatDateTime(
defaultTo(scheduleTime, ''),
)}`;
return (
<>
<Collapse accordion className="collapse-list">
<Panel
header={
<HeaderComponent
duration={
schedule?.recurrence?.duration
? (schedule?.recurrence?.duration as string)
: getDuration(schedule?.startTime, schedule?.endTime)
}
name={defaultTo(name, '')}
handleEdit={(): void => {
setInitialValues({ ...props });
setModalOpen(true);
setEditMode(true);
}}
handleDelete={(): void => {
handleDeleteDowntime(id, name || '');
}}
/>
}
key={id}
>
<CollapseListContent
created_at={defaultTo(createdAt, '')}
created_by_name={defaultTo(createdBy, '')}
timeframe={[schedule?.startTime, schedule?.endTime]}
repeats={schedule?.recurrence}
updated_at={defaultTo(updatedAt, '')}
updated_by_name={defaultTo(updatedBy, '')}
alertOptions={alertOptions}
/>
</Panel>
</Collapse>
<div className="view-created-at">
<CalendarClock size={14} />
<Typography.Text>{formattedDateAndTime}</Typography.Text>
</div>
</>
);
}
export type DowntimeSchedulesTableData = DowntimeSchedules & {
alertOptions: DefaultOptionType[];
};
export function PlannedDowntimeList({
downtimeSchedules,
alertOptions,
setInitialValues,
setModalOpen,
handleDeleteDowntime,
setEditMode,
searchValue,
}: {
downtimeSchedules: UseQueryResult<
AxiosResponse<PayloadProps, any>,
AxiosError<unknown, any>
>;
alertOptions: DefaultOptionType[];
setInitialValues: React.Dispatch<
React.SetStateAction<Partial<DowntimeSchedules>>
>;
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
handleDeleteDowntime: (id: number, name: string) => void;
setEditMode: React.Dispatch<React.SetStateAction<boolean>>;
searchValue: string | number;
}): JSX.Element {
const columns: TableProps<DowntimeSchedulesTableData>['columns'] = [
{
title: 'Downtime',
key: 'downtime',
render: (data: DowntimeSchedulesTableData): JSX.Element =>
CustomCollapseList({
...data,
setInitialValues,
setModalOpen,
handleDeleteDowntime,
setEditMode,
}),
},
];
const { notifications } = useNotifications();
const tableData = (downtimeSchedules.data?.data?.data || [])
?.filter(
(data) =>
data?.name?.includes(searchValue.toLocaleString()) ||
data?.id.toLocaleString() === searchValue.toLocaleString(),
)
.map?.((data) => {
const specificAlertOptions = getAlertOptionsFromIds(
data.alertIds || [],
alertOptions,
);
return { ...data, alertOptions: specificAlertOptions };
});
useEffect(() => {
if (downtimeSchedules.isError) {
notifications.error(downtimeSchedules.error);
}
}, [downtimeSchedules.error, downtimeSchedules.isError, notifications]);
return (
<Table<DowntimeSchedulesTableData>
columns={columns}
className="planned-downtime-table"
bordered={false}
dataSource={tableData || []}
loading={downtimeSchedules.isLoading || downtimeSchedules.isFetching}
showHeader={false}
pagination={{ pageSize: 5, showSizeChanger: false }}
/>
);
}

View File

@ -0,0 +1,176 @@
import { NotificationInstance } from 'antd/es/notification/interface';
import { DefaultOptionType } from 'antd/es/select';
import createDowntimeSchedule from 'api/plannedDowntime/createDowntimeSchedule';
import { DeleteSchedulePayloadProps } from 'api/plannedDowntime/deleteDowntimeSchedule';
import {
DowntimeSchedules,
Recurrence,
} from 'api/plannedDowntime/getAllDowntimeSchedules';
import updateDowntimeSchedule, {
DowntimeScheduleUpdatePayload,
PayloadProps,
} from 'api/plannedDowntime/updateDowntimeSchedule';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import dayjs from 'dayjs';
import { UseMutateAsyncFunction } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
type DateTimeString = string | null | undefined;
export const getDuration = (
startTime: DateTimeString,
endTime: DateTimeString,
): string => {
if (!startTime || !endTime) {
return 'N/A';
}
const start = dayjs(startTime);
const end = dayjs(endTime);
const durationMs = end.diff(start);
const minutes = Math.floor(durationMs / (1000 * 60));
const hours = Math.floor(durationMs / (1000 * 60 * 60));
if (minutes < 60) {
return `${minutes} min`;
}
return `${hours} hours`;
};
export const formatDateTime = (dateTimeString?: string | null): string => {
if (!dateTimeString) {
return 'N/A';
}
return dayjs(dateTimeString).format('MMM DD, YYYY h:mm A');
};
export const getAlertOptionsFromIds = (
alertIds: string[],
alertOptions: DefaultOptionType[],
): DefaultOptionType[] =>
alertOptions.filter(
(alert) =>
alert !== undefined &&
alert.value &&
alertIds?.includes(alert.value as string),
);
export const recurrenceInfo = (recurrence?: Recurrence | null): string => {
if (!recurrence) {
return 'No';
}
const { startTime, duration, repeatOn, repeatType, endTime } = recurrence;
const formattedStartTime = startTime ? formatDateTime(startTime) : '';
const formattedEndTime = endTime ? `to ${formatDateTime(endTime)}` : '';
const weeklyRepeatString = repeatOn ? `on ${repeatOn.join(', ')}` : '';
const durationString = duration ? `- Duration: ${duration}` : '';
return `Repeats - ${repeatType} ${weeklyRepeatString} from ${formattedStartTime} ${formattedEndTime} ${durationString}`;
};
export const defautlInitialValues: Partial<
DowntimeSchedules & { editMode: boolean }
> = {
name: '',
description: '',
schedule: {
timezone: '',
endTime: '',
recurrence: null,
startTime: '',
},
alertIds: [],
createdAt: '',
createdBy: '',
editMode: false,
};
type DeleteDowntimeScheduleProps = {
deleteDowntimeScheduleAsync: UseMutateAsyncFunction<
DeleteSchedulePayloadProps,
Error,
number
>;
notifications: NotificationInstance;
refetchAllSchedules: VoidFunction;
deleteId?: number;
hideDeleteDowntimeScheduleModal: () => void;
clearSearch: () => void;
};
export const deleteDowntimeHandler = ({
deleteDowntimeScheduleAsync,
refetchAllSchedules,
deleteId,
hideDeleteDowntimeScheduleModal,
clearSearch,
notifications,
}: DeleteDowntimeScheduleProps): void => {
if (!deleteId) {
const errorMsg = new Error('Something went wrong');
console.error('Unable to delete, please provide correct deleteId');
showErrorNotification(notifications, errorMsg);
} else {
deleteDowntimeScheduleAsync(deleteId, {
onSuccess: () => {
hideDeleteDowntimeScheduleModal();
clearSearch();
notifications.success({
message: 'Downtime schedule Deleted Successfully',
});
refetchAllSchedules();
},
onError: (err) => {
showErrorNotification(notifications, err);
},
});
}
};
export const createEditDowntimeSchedule = async (
props: DowntimeScheduleUpdatePayload,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
if (props.id && props.id > 0) {
return updateDowntimeSchedule({ ...props });
}
return createDowntimeSchedule({ ...props.data });
};
export const recurrenceOptions = {
doesNotRepeat: {
label: 'Does not repeat',
value: 'does-not-repeat',
},
daily: { label: 'Daily', value: 'daily' },
weekly: { label: 'Weekly', value: 'weekly' },
monthly: { label: 'Monthly', value: 'monthly' },
};
interface DurationInfo {
value: number;
unit: string;
}
export function getDurationInfo(
durationString: string | undefined | null,
): DurationInfo | null {
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 };
}
// If no value or unit part found, return null
return null;
}

View File

@ -1,30 +1,47 @@
import { Tabs } from 'antd';
import { TabsProps } from 'antd/lib';
import { FeatureKeys } from 'constants/features';
import AllAlertRules from 'container/ListAlertRules';
// import MapAlertChannels from 'container/MapAlertChannels';
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';
function AllAlertList(): JSX.Element {
const items = [
{ label: 'Alert Rules', key: 'Alert Rules', children: <AllAlertRules /> },
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 /> },
{
label: 'Triggered Alerts',
key: 'Triggered Alerts',
key: 'TriggeredAlerts',
children: <TriggeredAlerts />,
},
// {
// label: 'Planned Downtime',
// key: 'Planned Downtime',
// // children: <PlannedDowntime />,
// },
// {
// label: 'Map Alert Channels',
// key = 'Map Alert Channels',
// children: <MapAlertChannels />,
// },
{
label: isPlannedDowntimeEnabled ? 'Configuration' : '',
key: 'Configuration',
children: <PlannedDowntime />,
},
];
return (
<Tabs destroyInactiveTabPane defaultActiveKey="Alert Rules" items={items} />
<Tabs
destroyInactiveTabPane
items={items}
activeKey={tab || 'AlertRules'}
onChange={(tab): void => {
urlQuery.set('tab', tab);
history.replace(`${location.pathname}?${urlQuery.toString()}`);
}}
/>
);
}

View File

@ -0,0 +1,595 @@
export const ALL_TIME_ZONES = [
'Africa/Abidjan',
'Africa/Accra',
'Africa/Addis_Ababa',
'Africa/Algiers',
'Africa/Asmara',
'Africa/Asmera',
'Africa/Bamako',
'Africa/Bangui',
'Africa/Banjul',
'Africa/Bissau',
'Africa/Blantyre',
'Africa/Brazzaville',
'Africa/Bujumbura',
'Africa/Cairo',
'Africa/Casablanca',
'Africa/Ceuta',
'Africa/Conakry',
'Africa/Dakar',
'Africa/Dar_es_Salaam',
'Africa/Djibouti',
'Africa/Douala',
'Africa/El_Aaiun',
'Africa/Freetown',
'Africa/Gaborone',
'Africa/Harare',
'Africa/Johannesburg',
'Africa/Juba',
'Africa/Kampala',
'Africa/Khartoum',
'Africa/Kigali',
'Africa/Kinshasa',
'Africa/Lagos',
'Africa/Libreville',
'Africa/Lome',
'Africa/Luanda',
'Africa/Lubumbashi',
'Africa/Lusaka',
'Africa/Malabo',
'Africa/Maputo',
'Africa/Maseru',
'Africa/Mbabane',
'Africa/Mogadishu',
'Africa/Monrovia',
'Africa/Nairobi',
'Africa/Ndjamena',
'Africa/Niamey',
'Africa/Nouakchott',
'Africa/Ouagadougou',
'Africa/Porto-Novo',
'Africa/Sao_Tome',
'Africa/Timbuktu',
'Africa/Tripoli',
'Africa/Tunis',
'Africa/Windhoek',
'America/Adak',
'America/Anchorage',
'America/Anguilla',
'America/Antigua',
'America/Araguaina',
'America/Argentina/Buenos_Aires',
'America/Argentina/Catamarca',
'America/Argentina/ComodRivadavia',
'America/Argentina/Cordoba',
'America/Argentina/Jujuy',
'America/Argentina/La_Rioja',
'America/Argentina/Mendoza',
'America/Argentina/Rio_Gallegos',
'America/Argentina/Salta',
'America/Argentina/San_Juan',
'America/Argentina/San_Luis',
'America/Argentina/Tucuman',
'America/Argentina/Ushuaia',
'America/Aruba',
'America/Asuncion',
'America/Atikokan',
'America/Atka',
'America/Bahia',
'America/Bahia_Banderas',
'America/Barbados',
'America/Belem',
'America/Belize',
'America/Blanc-Sablon',
'America/Boa_Vista',
'America/Bogota',
'America/Boise',
'America/Buenos_Aires',
'America/Cambridge_Bay',
'America/Campo_Grande',
'America/Cancun',
'America/Caracas',
'America/Catamarca',
'America/Cayenne',
'America/Cayman',
'America/Chicago',
'America/Chihuahua',
'America/Coral_Harbour',
'America/Cordoba',
'America/Costa_Rica',
'America/Creston',
'America/Cuiaba',
'America/Curacao',
'America/Danmarkshavn',
'America/Dawson',
'America/Dawson_Creek',
'America/Denver',
'America/Detroit',
'America/Dominica',
'America/Edmonton',
'America/Eirunepe',
'America/El_Salvador',
'America/Ensenada',
'America/Fort_Nelson',
'America/Fort_Wayne',
'America/Fortaleza',
'America/Glace_Bay',
'America/Godthab',
'America/Goose_Bay',
'America/Grand_Turk',
'America/Grenada',
'America/Guadeloupe',
'America/Guatemala',
'America/Guayaquil',
'America/Guyana',
'America/Halifax',
'America/Havana',
'America/Hermosillo',
'America/Indiana/Indianapolis',
'America/Indiana/Knox',
'America/Indiana/Marengo',
'America/Indiana/Petersburg',
'America/Indiana/Tell_City',
'America/Indiana/Vevay',
'America/Indiana/Vincennes',
'America/Indiana/Winamac',
'America/Indianapolis',
'America/Inuvik',
'America/Iqaluit',
'America/Jamaica',
'America/Jujuy',
'America/Juneau',
'America/Kentucky/Louisville',
'America/Kentucky/Monticello',
'America/Knox_IN',
'America/Kralendijk',
'America/La_Paz',
'America/Lima',
'America/Los_Angeles',
'America/Louisville',
'America/Lower_Princes',
'America/Maceio',
'America/Managua',
'America/Manaus',
'America/Marigot',
'America/Martinique',
'America/Matamoros',
'America/Mazatlan',
'America/Mendoza',
'America/Menominee',
'America/Merida',
'America/Metlakatla',
'America/Mexico_City',
'America/Miquelon',
'America/Moncton',
'America/Monterrey',
'America/Montevideo',
'America/Montreal',
'America/Montserrat',
'America/Nassau',
'America/New_York',
'America/Nipigon',
'America/Nome',
'America/Noronha',
'America/North_Dakota/Beulah',
'America/North_Dakota/Center',
'America/North_Dakota/New_Salem',
'America/Nuuk',
'America/Ojinaga',
'America/Panama',
'America/Pangnirtung',
'America/Paramaribo',
'America/Phoenix',
'America/Port-au-Prince',
'America/Port_of_Spain',
'America/Porto_Acre',
'America/Porto_Velho',
'America/Puerto_Rico',
'America/Punta_Arenas',
'America/Rainy_River',
'America/Rankin_Inlet',
'America/Recife',
'America/Regina',
'America/Resolute',
'America/Rio_Branco',
'America/Rosario',
'America/Santa_Isabel',
'America/Santarem',
'America/Santiago',
'America/Santo_Domingo',
'America/Sao_Paulo',
'America/Scoresbysund',
'America/Shiprock',
'America/Sitka',
'America/St_Barthelemy',
'America/St_Johns',
'America/St_Kitts',
'America/St_Lucia',
'America/St_Thomas',
'America/St_Vincent',
'America/Swift_Current',
'America/Tegucigalpa',
'America/Thule',
'America/Thunder_Bay',
'America/Tijuana',
'America/Toronto',
'America/Tortola',
'America/Vancouver',
'America/Virgin',
'America/Whitehorse',
'America/Winnipeg',
'America/Yakutat',
'America/Yellowknife',
'Antarctica/Casey',
'Antarctica/Davis',
'Antarctica/DumontDUrville',
'Antarctica/Macquarie',
'Antarctica/Mawson',
'Antarctica/McMurdo',
'Antarctica/Palmer',
'Antarctica/Rothera',
'Antarctica/South_Pole',
'Antarctica/Syowa',
'Antarctica/Troll',
'Antarctica/Vostok',
'Arctic/Longyearbyen',
'Asia/Aden',
'Asia/Almaty',
'Asia/Amman',
'Asia/Anadyr',
'Asia/Aqtau',
'Asia/Aqtobe',
'Asia/Ashgabat',
'Asia/Ashkhabad',
'Asia/Atyrau',
'Asia/Baghdad',
'Asia/Bahrain',
'Asia/Baku',
'Asia/Bangkok',
'Asia/Barnaul',
'Asia/Beirut',
'Asia/Bishkek',
'Asia/Brunei',
'Asia/Calcutta',
'Asia/Chita',
'Asia/Choibalsan',
'Asia/Chongqing',
'Asia/Chungking',
'Asia/Colombo',
'Asia/Dacca',
'Asia/Damascus',
'Asia/Dhaka',
'Asia/Dili',
'Asia/Dubai',
'Asia/Dushanbe',
'Asia/Famagusta',
'Asia/Gaza',
'Asia/Harbin',
'Asia/Hebron',
'Asia/Ho_Chi_Minh',
'Asia/Hong_Kong',
'Asia/Hovd',
'Asia/Irkutsk',
'Asia/Istanbul',
'Asia/Jakarta',
'Asia/Jayapura',
'Asia/Jerusalem',
'Asia/Kabul',
'Asia/Kamchatka',
'Asia/Karachi',
'Asia/Kashgar',
'Asia/Kathmandu',
'Asia/Katmandu',
'Asia/Khandyga',
'Asia/Kolkata',
'Asia/Krasnoyarsk',
'Asia/Kuala_Lumpur',
'Asia/Kuching',
'Asia/Kuwait',
'Asia/Macao',
'Asia/Macau',
'Asia/Magadan',
'Asia/Makassar',
'Asia/Manila',
'Asia/Muscat',
'Asia/Nicosia',
'Asia/Novokuznetsk',
'Asia/Novosibirsk',
'Asia/Omsk',
'Asia/Oral',
'Asia/Phnom_Penh',
'Asia/Pontianak',
'Asia/Pyongyang',
'Asia/Qatar',
'Asia/Qostanay',
'Asia/Qyzylorda',
'Asia/Rangoon',
'Asia/Riyadh',
'Asia/Saigon',
'Asia/Sakhalin',
'Asia/Samarkand',
'Asia/Seoul',
'Asia/Shanghai',
'Asia/Singapore',
'Asia/Srednekolymsk',
'Asia/Taipei',
'Asia/Tashkent',
'Asia/Tbilisi',
'Asia/Tehran',
'Asia/Tel_Aviv',
'Asia/Thimbu',
'Asia/Thimphu',
'Asia/Tokyo',
'Asia/Tomsk',
'Asia/Ujung_Pandang',
'Asia/Ulaanbaatar',
'Asia/Ulan_Bator',
'Asia/Urumqi',
'Asia/Ust-Nera',
'Asia/Vientiane',
'Asia/Vladivostok',
'Asia/Yakutsk',
'Asia/Yangon',
'Asia/Yekaterinburg',
'Asia/Yerevan',
'Atlantic/Azores',
'Atlantic/Bermuda',
'Atlantic/Canary',
'Atlantic/Cape_Verde',
'Atlantic/Faeroe',
'Atlantic/Faroe',
'Atlantic/Jan_Mayen',
'Atlantic/Madeira',
'Atlantic/Reykjavik',
'Atlantic/South_Georgia',
'Atlantic/St_Helena',
'Atlantic/Stanley',
'Australia/ACT',
'Australia/Adelaide',
'Australia/Brisbane',
'Australia/Broken_Hill',
'Australia/Canberra',
'Australia/Currie',
'Australia/Darwin',
'Australia/Eucla',
'Australia/Hobart',
'Australia/LHI',
'Australia/Lindeman',
'Australia/Lord_Howe',
'Australia/Melbourne',
'Australia/NSW',
'Australia/North',
'Australia/Perth',
'Australia/Queensland',
'Australia/South',
'Australia/Sydney',
'Australia/Tasmania',
'Australia/Victoria',
'Australia/West',
'Australia/Yancowinna',
'Brazil/Acre',
'Brazil/DeNoronha',
'Brazil/East',
'Brazil/West',
'CET',
'CST6CDT',
'Canada/Atlantic',
'Canada/Central',
'Canada/Eastern',
'Canada/Mountain',
'Canada/Newfoundland',
'Canada/Pacific',
'Canada/Saskatchewan',
'Canada/Yukon',
'Chile/Continental',
'Chile/EasterIsland',
'Cuba',
'EET',
'EST',
'EST5EDT',
'Egypt',
'Eire',
'Etc/GMT',
'Etc/GMT+0',
'Etc/GMT+1',
'Etc/GMT+10',
'Etc/GMT+11',
'Etc/GMT+12',
'Etc/GMT+2',
'Etc/GMT+3',
'Etc/GMT+4',
'Etc/GMT+5',
'Etc/GMT+6',
'Etc/GMT+7',
'Etc/GMT+8',
'Etc/GMT+9',
'Etc/GMT-0',
'Etc/GMT-1',
'Etc/GMT-10',
'Etc/GMT-11',
'Etc/GMT-12',
'Etc/GMT-13',
'Etc/GMT-14',
'Etc/GMT-2',
'Etc/GMT-3',
'Etc/GMT-4',
'Etc/GMT-5',
'Etc/GMT-6',
'Etc/GMT-7',
'Etc/GMT-8',
'Etc/GMT-9',
'Etc/GMT0',
'Etc/Greenwich',
'Etc/UCT',
'Etc/UTC',
'Etc/Universal',
'Etc/Zulu',
'Europe/Amsterdam',
'Europe/Andorra',
'Europe/Astrakhan',
'Europe/Athens',
'Europe/Belfast',
'Europe/Belgrade',
'Europe/Berlin',
'Europe/Bratislava',
'Europe/Brussels',
'Europe/Bucharest',
'Europe/Budapest',
'Europe/Busingen',
'Europe/Chisinau',
'Europe/Copenhagen',
'Europe/Dublin',
'Europe/Gibraltar',
'Europe/Guernsey',
'Europe/Helsinki',
'Europe/Isle_of_Man',
'Europe/Istanbul',
'Europe/Jersey',
'Europe/Kaliningrad',
'Europe/Kiev',
'Europe/Kirov',
'Europe/Lisbon',
'Europe/Ljubljana',
'Europe/London',
'Europe/Luxembourg',
'Europe/Madrid',
'Europe/Malta',
'Europe/Mariehamn',
'Europe/Minsk',
'Europe/Monaco',
'Europe/Moscow',
'Europe/Nicosia',
'Europe/Oslo',
'Europe/Paris',
'Europe/Podgorica',
'Europe/Prague',
'Europe/Riga',
'Europe/Rome',
'Europe/Samara',
'Europe/San_Marino',
'Europe/Sarajevo',
'Europe/Saratov',
'Europe/Simferopol',
'Europe/Skopje',
'Europe/Sofia',
'Europe/Stockholm',
'Europe/Tallinn',
'Europe/Tirane',
'Europe/Tiraspol',
'Europe/Ulyanovsk',
'Europe/Uzhgorod',
'Europe/Vaduz',
'Europe/Vatican',
'Europe/Vienna',
'Europe/Vilnius',
'Europe/Volgograd',
'Europe/Warsaw',
'Europe/Zagreb',
'Europe/Zaporozhye',
'Europe/Zurich',
'GB',
'GB-Eire',
'GMT',
'GMT+0',
'GMT-0',
'GMT0',
'Greenwich',
'HST',
'Hongkong',
'Iceland',
'Indian/Antananarivo',
'Indian/Chagos',
'Indian/Christmas',
'Indian/Cocos',
'Indian/Comoro',
'Indian/Kerguelen',
'Indian/Mahe',
'Indian/Maldives',
'Indian/Mauritius',
'Indian/Mayotte',
'Indian/Reunion',
'Iran',
'Israel',
'Jamaica',
'Japan',
'Kwajalein',
'Libya',
'MET',
'MST',
'MST7MDT',
'Mexico/BajaNorte',
'Mexico/BajaSur',
'Mexico/General',
'NZ',
'NZ-CHAT',
'Navajo',
'PRC',
'PST8PDT',
'Pacific/Apia',
'Pacific/Auckland',
'Pacific/Bougainville',
'Pacific/Chatham',
'Pacific/Chuuk',
'Pacific/Easter',
'Pacific/Efate',
'Pacific/Enderbury',
'Pacific/Fakaofo',
'Pacific/Fiji',
'Pacific/Funafuti',
'Pacific/Galapagos',
'Pacific/Gambier',
'Pacific/Guadalcanal',
'Pacific/Guam',
'Pacific/Honolulu',
'Pacific/Johnston',
'Pacific/Kiritimati',
'Pacific/Kosrae',
'Pacific/Kwajalein',
'Pacific/Majuro',
'Pacific/Marquesas',
'Pacific/Midway',
'Pacific/Nauru',
'Pacific/Niue',
'Pacific/Norfolk',
'Pacific/Noumea',
'Pacific/Pago_Pago',
'Pacific/Palau',
'Pacific/Pitcairn',
'Pacific/Pohnpei',
'Pacific/Ponape',
'Pacific/Port_Moresby',
'Pacific/Rarotonga',
'Pacific/Saipan',
'Pacific/Samoa',
'Pacific/Tahiti',
'Pacific/Tarawa',
'Pacific/Tongatapu',
'Pacific/Truk',
'Pacific/Wake',
'Pacific/Wallis',
'Pacific/Yap',
'Poland',
'Portugal',
'ROC',
'ROK',
'Singapore',
'Turkey',
'UCT',
'US/Alaska',
'US/Aleutian',
'US/Arizona',
'US/Central',
'US/East-Indiana',
'US/Eastern',
'US/Hawaii',
'US/Indiana-Starke',
'US/Michigan',
'US/Mountain',
'US/Pacific',
'US/Samoa',
'UTC',
'Universal',
'W-SU',
'WET',
'Zulu',
];