Merge branch 'master' into search-params-for-teams

This commit is contained in:
Dhruv Joshi
2024-02-07 11:57:45 +05:30
committed by GitHub
28 changed files with 1670 additions and 1789 deletions

View File

@@ -32,7 +32,7 @@
},
"dependencies": {
"@navi/dark-knight": "^1.0.13",
"@navi/web-ui": "^1.59.4",
"@navi/web-ui": "^1.59.6",
"@reduxjs/toolkit": "^1.9.7",
"@stoddabr/react-tableau-embed-live": "^0.3.26",
"antd": "^5.9.4",

View File

@@ -1,20 +1,19 @@
import { FC } from 'react';
import { useSelector } from 'react-redux';
import Typography from '@navi/web-ui/lib/primitives/Typography';
import { IncidentPageState } from '@src/types';
import { selectIsLastChangeIndex } from '@src/slices/IncidentSlice';
import IncidentCreated from '../StepperAssets/IncidentCreated';
import IncidentChange from '../StepperAssets/IncidentChange';
import styles from './ActivityLog.module.scss';
interface ActivityLogProps {
incidentLog: {
data: {
logs: Array<any>;
has_creation: boolean;
};
};
totalLog: number;
}
const ActivityLog: FC = () => {
const incidentLog = useSelector(
(state: IncidentPageState) => state.incidentLog.incidentLogData,
);
const lastChangeIndex = useSelector(selectIsLastChangeIndex);
const totalLog = incidentLog?.data?.logs?.length - lastChangeIndex;
const ActivityLog: FC<ActivityLogProps> = ({ incidentLog, totalLog }) => {
return (
<div>
{incidentLog?.data?.logs && incidentLog.data.logs.length > 0 && (

View File

@@ -1,41 +0,0 @@
.content-wrapper {
background: #fff;
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 2px 1px rgba(0, 0, 0, 0.06),
0px 1px 1px rgba(0, 0, 0, 0.08);
border-radius: 8px;
padding: 12px;
margin-top: 16px;
width: 35%;
}
.description-details {
margin: 12px 0 16px 0;
}
.participant-detail {
display: flex;
align-items: center;
gap: 4px;
}
.team-details-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
}
.loader {
margin-top: 12px;
text-align: center;
}
.content-info {
display: flex;
align-items: center;
gap: 2px;
}
.key-value-pair {
margin-top: 6px;
}

View File

@@ -1,118 +0,0 @@
import { useEffect, useState } from 'react';
import Avatar from '@navi/web-ui/lib/primitives/Avatar';
import KeyValueLabel from '@navi/web-ui/lib/primitives/KeyValueLabel';
import Typography from '@navi/web-ui/lib/primitives/Typography';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import LoadingIcon from '@navi/web-ui/lib/icons/LoadingIcon';
import { ApiService } from '@src/services/api';
import { returnFormattedDate } from '@src/services/globalUtils';
import {
ContentProps,
FETCH_PARTICIPANTS_DATA,
IncidentConstants,
} from '../constants';
import styles from './Content.module.scss';
import commonStyles from '../Incidents.module.scss';
const Content = (props: ContentProps) => {
const { incidentData } = props;
const [data, setData] = useState<any>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const startParticipantsSearch = (): void => {
if (incidentData?.slackChannel) {
const endPoint = FETCH_PARTICIPANTS_DATA(incidentData?.slackChannel);
setIsLoading(true);
ApiService.get(endPoint)
.then(response => {
setIsLoading(false);
setData(response?.data?.data);
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message},`
: ''
}`;
setIsLoading(false);
toast.error(toastMessage);
setData([]);
});
}
};
useEffect(() => {
startParticipantsSearch();
}, [incidentData]);
return (
<div className={styles['content-wrapper']}>
<Typography variant="h5">{IncidentConstants.incidentSummary}</Typography>
<hr className={commonStyles['divider']} />
<div>
<Typography variant="h4" className={styles['content-info']}>
{IncidentConstants.title}:{' '}
<Typography variant="p3">{incidentData?.title}</Typography>
</Typography>
</div>
<div className={styles['description-details']}>
<Typography variant="h4">{IncidentConstants.description}:</Typography>
<Typography variant="p3">{incidentData?.description}</Typography>
</div>
<Typography variant="h4">{IncidentConstants.status}:</Typography>
<div>
<KeyValueLabel
className={styles['key-value-pair']}
dataArray={[
{
key: 'Created At',
value: returnFormattedDate(incidentData?.createdAt),
},
{
key: 'Created By',
value: incidentData?.createdBy,
},
{
key: 'Updated At',
value: returnFormattedDate(incidentData?.updatedAt),
},
{
key: 'Updated By',
value: incidentData?.updatedBy,
},
]}
/>
</div>
<div className={styles['description-details']}>
<Typography variant="h4">{IncidentConstants.team}:</Typography>
{isLoading ? (
<div className={styles['loader']}>
<LoadingIcon size="lg" />
</div>
) : (
data?.map(participant => (
<div
key={participant?.id}
className={styles['team-details-wrapper']}
>
<div className={styles['participant-detail']}>
<Avatar
size={20}
alt={participant?.real_name?.[0]?.toUpperCase()}
/>{' '}
<Typography variant="p3">{participant?.real_name}</Typography>
</div>
</div>
))
)}
</div>
</div>
);
};
export default Content;

View File

@@ -1,211 +1,28 @@
import React, { FC, useState, useReducer, useEffect } from 'react';
import {
Typography,
Avatar,
BorderLessInput,
} from '@navi/web-ui/lib/primitives';
import {
CloseIcon,
DeleteIconOutlined,
ArrowUpSolidIcon,
ArrowDownSolidIcon,
} from '@navi/web-ui/lib/icons';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import React from 'react';
import { useSelector } from 'react-redux';
import { Typography, Avatar } from '@navi/web-ui/lib/primitives';
import SlackIcon from '@src/assets/SlackIcon';
import LinkIcon from '@src/assets/LinkIcon';
import JiraLogo from '@src/assets/JiraLogo';
import CopyIcon from '@src/assets/CopyIcon';
import LoadingIcon from '@src/assets/LoadingIcon';
import ConfluenceIcon from '@src/assets/ConfluenceIcon';
import GoToLinkIcon from '@src/assets/GoToLinkIcon';
import { ApiService } from '@src/services/api';
import { handleCopyClick } from '@src/services/globalUtils';
import DescriptionContentProps from './DescriptionContentProps';
import {
ActionType,
initialState,
reducer,
JIRA_VALIDATION,
} from './DescriptionContentProps';
import { IncidentPageState } from '@src/types';
import JiraLinks from '../JiraLinks';
import styles from './DescriptionContent.module.scss';
import {
LINK_JIRA_INCIDENT,
UNLINK_JIRA_INCIDENT,
FETCH_INCIDENT_DATA,
} from '../constants';
const DescriptionContent: React.FC<DescriptionContentProps> = ({
id,
description,
slackChannel,
incidentName,
incidentParticipants,
jiraIds,
rcaLink,
}) => {
const [state, dispatch] = useReducer(reducer, initialState);
const DescriptionContent: React.FC = () => {
const { description, slackChannel, incidentName, rcaLink } = useSelector(
(state: IncidentPageState) => state.incidentLog.incidentData || {},
);
const incidentParticipants = useSelector(
(state: IncidentPageState) => state.incidentLog.participantsData,
);
const storedEmail = localStorage.getItem('email-id');
const goToLinkBlueColor = '#0276FE';
useEffect(() => {
dispatch({ type: ActionType.SET_JIRA_LINKS, payload: jiraIds || [] });
}, [jiraIds]);
const handleApiError = (error: any): void => {
const errorMessage =
error?.response?.data?.error?.message || 'An error occurred.';
toast.error(errorMessage);
};
const startIncidentSearch = (): void => {
const endPoint = FETCH_INCIDENT_DATA(id);
ApiService.get(endPoint)
.then(response => {
dispatch({
type: ActionType.SET_JIRA_LINKS,
payload: response?.data?.data?.jiraLinks || [],
});
})
.catch(handleApiError);
};
const addJiraLink = (payload: any): void => {
handleCloseIconClick();
dispatch({ type: ActionType.SET_SHOW_LINKED_TICKETS, payload: true });
const endPoint = LINK_JIRA_INCIDENT;
ApiService.post(endPoint, payload)
.then(response => {
toast.success(`${response?.data?.data}`);
startIncidentSearch();
})
.catch(handleApiError);
};
const removeJiraLink = (payload: any): void => {
const endPoint = UNLINK_JIRA_INCIDENT;
ApiService.post(endPoint, payload)
.then(response => {
toast.info(`${response?.data?.data}`);
startIncidentSearch();
})
.catch(handleApiError);
};
const validateLink = (value: string): void => {
const urlPattern = /^https:\/\/navihq\.atlassian\.net\/browse\/.*/;
const invalidChars = /,|\s/;
if (value === '') {
dispatch({ type: ActionType.SET_ERROR_TEXT, payload: '' });
dispatch({ type: ActionType.SET_HELPER_TEXT, payload: '' });
} else {
if (urlPattern.test(value) && !invalidChars.test(value)) {
dispatch({ type: ActionType.SET_ERROR_TEXT, payload: '' });
dispatch({
type: ActionType.SET_HELPER_TEXT,
payload: 'Press enter to add link',
});
} else {
dispatch({ type: ActionType.SET_HELPER_TEXT, payload: '' });
dispatch({
type: ActionType.SET_ERROR_TEXT,
payload: ' Invalid entry. Try entering a different URL',
});
}
}
};
const handleLinkChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const inputValue = e.target.value;
dispatch({ type: ActionType.SET_INPUT_VALUE, payload: inputValue });
validateLink(inputValue);
};
const linkSanitization = link => {
const sanitizedLinkMatch = link.match(
/(https:\/\/navihq.atlassian.net\/browse\/[^/]+)/,
);
if (sanitizedLinkMatch && sanitizedLinkMatch[1]) {
return sanitizedLinkMatch[1];
}
return link;
};
const handleInputKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>,
): void => {
if (event.key === 'Enter') {
if (!state.errorText && state.inputValue.startsWith(JIRA_VALIDATION)) {
const sanitizedLink = linkSanitization(state.inputValue);
const payload = {
incident_id: id,
jira_link: sanitizedLink,
user: storedEmail,
};
toast('Adding link. Please wait a moment.', {
icon: <LoadingIcon />,
});
addJiraLink(payload);
}
}
};
const handleLinkJiraClick = (): void => {
dispatch({ type: ActionType.SET_SHOW_INPUT, payload: true });
};
const handleCloseIconClick = (): void => {
dispatch({ type: ActionType.SET_INPUT_VALUE, payload: '' });
dispatch({ type: ActionType.SET_ERROR_TEXT, payload: '' });
dispatch({ type: ActionType.SET_HELPER_TEXT, payload: '' });
dispatch({ type: ActionType.SET_SHOW_INPUT, payload: false });
};
const handleLinkedTicketsClick = (): void => {
dispatch({
type: ActionType.SET_SHOW_LINKED_TICKETS,
payload: !state.showLinkedTickets,
});
};
const handleDeleteIconClick = (jiraLinkToDelete: string): void => {
const payload = {
incident_id: id,
jira_link: jiraLinkToDelete,
user: storedEmail,
};
toast('Removing link. Please wait a moment.', {
icon: <LoadingIcon />,
});
removeJiraLink(payload);
};
const truncateText = (text): string => {
const jiraTicketMatch = text.match(/\/browse\/([^/]+)/);
if (jiraTicketMatch && jiraTicketMatch[1]) {
return jiraTicketMatch[1];
}
return text;
};
const returnParticipants = (): JSX.Element => {
return incidentParticipants?.participants?.length ? (
incidentParticipants?.participants?.map(participant => (
<div key={participant?.id} className={styles['team-details-wrapper']}>
<div className={styles['participant-detail']}>
<Avatar size={20} isImage src={participant?.image} /> &nbsp;&nbsp;
<Typography variant="h5">{participant?.name}</Typography>
</div>
</div>
))
) : (
<div className={styles['team-details-wrapper']}>-</div>
);
};
const returnOthers = (): JSX.Element => {
return incidentParticipants?.others?.length ? (
incidentParticipants?.others?.map(participant => (
const renderParticipantList = (type: string): JSX.Element | JSX.Element[] => {
const list =
type === 'participants'
? incidentParticipants?.participants
: incidentParticipants?.others;
return list?.length ? (
list.map(participant => (
<div key={participant?.id} className={styles['team-details-wrapper']}>
<div className={styles['participant-detail']}>
<Avatar size={20} isImage src={participant?.image} /> &nbsp;&nbsp;
@@ -266,123 +83,15 @@ const DescriptionContent: React.FC<DescriptionContentProps> = ({
rel="noreferrer"
className={styles['rca-text']}
>
<GoToLinkIcon color={goToLinkBlueColor} />
<GoToLinkIcon color="var(--navi-color-blue-base)" />
<Typography variant="h4" color="var(--navi-color-blue-base)">
Go to document
</Typography>
</a>
</div>
)}
<div className={styles['description-content-jira']}>
<div className={styles['flex-row']}>
<JiraLogo />
&nbsp;
<Typography variant="h4" color="var(--navi-color-gray-c2)">
JIRA ticket(s) &nbsp;&nbsp;&nbsp;&nbsp;
</Typography>
</div>
<div>
<div className={styles['flex-row']}>
<div
className={styles['first-row-link']}
onClick={handleLinkedTicketsClick}
>
<Typography
variant="h4"
color="var(--navi-color-gray-c2)"
className={styles['typo-style']}
>
{state.jiraLinks?.filter(link => link !== '').length || 0}{' '}
linked tickets&nbsp;&nbsp;
</Typography>
{state.jiraLinks?.filter(link => link !== '').length > 0 && (
<>
{state.showLinkedTickets && (
<ArrowUpSolidIcon width={8} height={8} />
)}
{!state.showLinkedTickets && (
<ArrowDownSolidIcon width={8} height={8} />
)}
</>
)}
</div>
<div className={styles['vertical-line']} />
<div
className={`${styles['link-ticket-style']} ${
state.showInput ? styles['link-ticket-opacity'] : ''
}`}
onClick={handleLinkJiraClick}
>
<LinkIcon />
&nbsp;
<Typography variant="h4" color="var(--navi-color-blue-base)">
Link JIRA ticket
</Typography>
</div>
</div>
<div>
{state.showLinkedTickets && (
<div>
{state.jiraLinks
.filter(jiraLink => jiraLink !== '')
?.map((jiraLink, index) => (
<div key={index} className={styles['link-row-style']}>
<div>
<div className={styles['jira-link-row']}>
<a
href={jiraLink}
target="_blank"
rel="noreferrer"
className={styles['jira-link']}
>
<Typography
variant="h4"
color="var(--navi-color-blue-base)"
>
{truncateText(jiraLink)}
</Typography>
</a>
</div>
</div>
&nbsp;&nbsp;
<div className={styles['copyicon-style']}>
<CopyIcon onClick={() => handleCopyClick(jiraLink)} />
</div>
<div className={styles['deleteicon-style']}>
<DeleteIconOutlined
onClick={() => handleDeleteIconClick(jiraLink)}
width={20}
height={20}
color="var(--navi-color-gray-c3)"
/>
</div>
</div>
))}
</div>
)}
</div>
{state.showInput && (
<div className={styles['jira-input-row']}>
<div className={styles['row-input-field']}>
<BorderLessInput
inputLabel="Enter ticket link"
onChange={handleLinkChange}
onKeyDown={handleInputKeyDown}
hideClearIcon
error={state.errorText}
hintMsg={state.helperText}
fullWidth={true}
maxLength={50}
/>
</div>
<div className={styles['closeicon-style']}>
<CloseIcon onClick={handleCloseIconClick} />
</div>
</div>
)}
</div>
</div>
</div>
<JiraLinks />
<div className={styles['horizontal-line']} />
<div className={styles['description-participants']}>
<Typography variant="h6" color="var(--navi-color-gray-c3)">
@@ -392,14 +101,14 @@ const DescriptionContent: React.FC<DescriptionContentProps> = ({
<Typography variant="h6" color="var(--navi-color-gray-c3)">
Team
</Typography>
{returnParticipants()}
{renderParticipantList('participants')}
</div>
<div className={styles['description-participants']}>
<div className={styles['description-team']}>
<Typography variant="h6" color="var(--navi-color-gray-c3)">
Others
</Typography>
{returnOthers()}
{renderParticipantList('others')}
</div>
</div>
</div>

View File

@@ -1,262 +0,0 @@
import { FC, useEffect, useState } from 'react';
import cx from 'classnames';
import { Typography, Avatar } from '@navi/web-ui/lib/primitives';
import Filter from '@navi/web-ui/lib/components/Filter';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import LoadingIcon from '@navi/web-ui/lib/icons/LoadingIcon';
import { ApiService } from '@src/services/api';
import SlackIcon from '@src/assets/SlackIcon';
import {
FETCH_HEADER_DETAILS,
FETCH_PARTICIPANTS_DATA,
FETCH_INCIDENT_DATA,
UPDATE_INCIDENT,
IncidentConstants,
} from '../constants';
import styles from './DrawerMode.module.scss';
import commonStyles from '../Incidents.module.scss';
interface DrawerModeProps {
incidentId: number;
slackChannel: string;
}
const DrawerMode: FC<DrawerModeProps> = ({ incidentId, slackChannel }) => {
const [severity, setSeverity] = useState<any>();
const [status, setStatus] = useState<any>();
const [team, setTeam] = useState<any>();
const [headerData, setHeaderData] = useState<any>({});
const [incidentDetails, setIncidentDetails] = useState<any>({});
const [incidentParticipants, setIncidentParticipants] = useState<any>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const fetchHeaderDetails = (): void => {
const endPoint = FETCH_HEADER_DETAILS;
ApiService.get(endPoint)
.then(response => {
setHeaderData(response?.data?.data);
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message},`
: ''
}`;
toast.error(toastMessage);
setHeaderData({});
});
};
const initFilters = (incidentDetails): void => {
const severity = headerData?.severities?.find(
item => item.value === incidentDetails?.severityId,
);
const status = headerData?.incidentStatuses?.find(
item => item.value === incidentDetails?.status,
);
const team = headerData?.teams?.find(
item => item.value === incidentDetails?.teamId,
);
setSeverity(severity);
setStatus(status);
setTeam(team);
setIsLoading(false);
};
const fetchIncidentDetails = (): void => {
const endPoint = FETCH_INCIDENT_DATA(incidentId);
ApiService.get(endPoint)
.then(response => {
setIncidentDetails(response?.data?.data);
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message},`
: ''
}`;
toast.error(toastMessage);
setIncidentDetails({});
});
};
const fetchParticipants = (): void => {
const endPoint = FETCH_PARTICIPANTS_DATA(slackChannel);
ApiService.get(endPoint)
.then(response => {
setIncidentParticipants(response?.data?.data);
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message},`
: ''
}`;
toast.error(toastMessage);
setIncidentParticipants([]);
});
};
useEffect(() => {
setIsLoading(true);
if (incidentId && slackChannel) {
fetchIncidentDetails();
fetchHeaderDetails();
fetchParticipants();
}
}, [incidentId, slackChannel]);
useEffect(() => {
if (Object.keys(headerData).length && Object.keys(incidentDetails).length) {
initFilters(incidentDetails);
}
}, [headerData, incidentDetails, incidentParticipants]);
const updateIncident = payload => {
const endPoint = UPDATE_INCIDENT;
ApiService.post(endPoint, payload)
.then(response => {
toast.success('Incident Updated Successfully');
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message},`
: ''
}`;
toast.error(toastMessage);
});
};
const returnHeaderDetails = () => {
return (
<div>
<Typography variant="h4">Update Tags: </Typography>
<div className={styles['severity-details-wrapper']}>
<Typography variant="h5">Severity: </Typography>
<Filter
title={severity?.label ? severity?.label : 'Severity'}
options={headerData?.severities}
onSelectionChange={val => {
setSeverity(val);
updateIncident({
id: incidentId,
severityId: !Array.isArray(val)
? `${val?.value}`
: val?.[0]?.value,
});
}}
isSingleSelect
filterClass={styles['filter-wrapper']}
/>
</div>
<div className={styles['severity-details-wrapper']}>
<Typography variant="h5">Status: </Typography>
<Filter
title={status?.label ? status?.label : 'Status'}
options={headerData?.incidentStatuses}
onSelectionChange={val => {
setStatus(val);
updateIncident({
id: incidentId,
status: !Array.isArray(val) ? `${val?.value}` : val?.[0]?.value,
});
}}
isSingleSelect
filterClass={styles['filter-wrapper']}
/>
</div>
<div className={styles['severity-details-wrapper']}>
<Typography variant="h5">Team: </Typography>
<Filter
title={team?.label ? team?.label : 'Team'}
options={headerData?.teams}
onSelectionChange={val => {
setTeam(val);
updateIncident({
id: incidentId,
teamId: !Array.isArray(val) ? `${val?.value}` : val?.[0]?.value,
});
}}
isSingleSelect
filterClass={styles['filter-wrapper']}
/>
</div>
</div>
);
};
const returnContent = () => {
return (
<div>
<div className={styles['content-info']}>
<Typography variant="h4">{IncidentConstants.title}:</Typography>
<Typography variant="p4">{incidentDetails?.title}</Typography>
</div>
<div className={styles['description-details']}>
<Typography variant="h4">{IncidentConstants.description}:</Typography>
<Typography variant="p4" className={styles['description']}>
{incidentDetails?.description || '-'}
</Typography>
</div>
<div className={styles['description-details']}>
<Typography variant="h4">{IncidentConstants.channel}:</Typography>
<Typography variant="p4" className={styles['slack-channel']}>
<SlackIcon />
<a
href={`https://go-navi.slack.com/archives/${incidentDetails?.slackChannel}`}
target="_blank"
rel="noreferrer"
>
{incidentDetails?.incidentName || '-'}
</a>
</Typography>
</div>
<div className={styles['description-details']}>
<Typography variant="h4">{IncidentConstants.incidentId}:</Typography>
<Typography variant="p4" className={styles['description']}>
{incidentDetails?.id}
</Typography>
</div>
</div>
);
};
const returnParticipants = () => {
return incidentParticipants?.length ? (
incidentParticipants?.map(participant => (
<div key={participant?.id} className={styles['team-details-wrapper']}>
<div className={styles['participant-detail']}>
<Avatar size={20} isImage src={participant?.image} />{' '}
<Typography variant="p3">{participant?.name}</Typography>
</div>
</div>
))
) : (
<div className={styles['team-details-wrapper']}>-</div>
);
};
if (isLoading) {
return (
<div className={styles['loader-container']}>
<LoadingIcon size="md" />
</div>
);
}
return (
<div>
{returnContent()}
<hr className={cx(commonStyles['divider'], styles['content-divider'])} />
{returnHeaderDetails()}
<hr className={cx(commonStyles['divider'], styles['content-divider'])} />
<Typography variant="h4">Participants:</Typography>
{returnParticipants()}
</div>
);
};
export default DrawerMode;

View File

@@ -0,0 +1,383 @@
import { FC, useReducer } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { AlertOutlineIcon } from '@navi/web-ui/lib/icons';
import { SelectPickerOptionProps } from '@navi/web-ui/lib/components/SelectPicker/types';
import {
BorderedInput,
ModalDialog,
Typography,
Button,
} from '@navi/web-ui/lib/primitives';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import GoToLinkIcon from '@src/assets/GoToLinkIcon';
import LoadingIcon from '@src/assets/LoadingIcon';
import {
setOpenDialognotParticipants,
setOpenDialogDuplicate,
setOpenDialogResolve,
setOpenDialogUpdate,
} from '@src/slices/IncidentSlice';
import { IncidentPageState } from '@src/types';
import {
actionTypes,
SLACK_BASE_URL,
reducer,
initialState,
incidentRegrex,
RESOLVE_STATUS,
} from '../constants';
import useIncidentApis from '../useIncidentApis';
import styles from '../Incidents.module.scss';
const AllDailogBox: FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const openNotParticipants = useSelector(
(state: IncidentPageState) => state.incidentLog.openDialognotParticipants,
);
const openDuplicate = useSelector(
(state: IncidentPageState) => state.incidentLog.openDialogDuplicate,
);
const openResolve = useSelector(
(state: IncidentPageState) => state.incidentLog.openDialogResolve,
);
const openUpdate = useSelector(
(state: IncidentPageState) => state.incidentLog.openDialogUpdate,
);
const UpdateData = useSelector(
(state: IncidentPageState) => state.incidentLog.updateDetails,
);
const selectedOption = useSelector(
(state: IncidentPageState) => state.incidentLog.selectedOptions,
);
const incidentData = useSelector(
(state: IncidentPageState) => state.incidentLog.incidentData,
);
const { updateIncident, markDuplicateIncident } = useIncidentApis();
const incidentId = incidentData?.id?.toString() || '';
const reduxDispatch = useDispatch();
const handleopenNotParticipants = (): void => {
reduxDispatch(setOpenDialognotParticipants(false));
};
const handleGoToSlackChannel = (): void => {
const slackChannelURL = `${SLACK_BASE_URL}/archives/${incidentData?.slackChannel}`;
window.open(slackChannelURL, '_blank');
handleopenNotParticipants();
};
const handleResetDialog = (): void => {
dispatch({
type: actionTypes.RESET_DUPLICATE_DIALOG,
});
reduxDispatch(setOpenDialogDuplicate(false));
};
const disable = (): boolean => {
return !state.incidentName || !validate(state.incidentName);
};
const extractIncidentId = (incidentName: string | null): number | null => {
if (!incidentName) {
return null;
}
return (match => (match ? parseInt(match[1], 10) : null))(
incidentName.match(/_houston-(\d+)/),
);
};
const duplicateOfId = extractIncidentId(state.incidentName);
const handleIncidentChange = (
e: React.ChangeEvent<HTMLInputElement>,
): void => {
const inputValue = e.target.value;
validateIncidentID(inputValue);
dispatch({
type: actionTypes.SET_INCIDENT_NAME,
payload: inputValue,
});
};
const validateIncidentID = (value: string): void => {
dispatch({
type: actionTypes.SET_ERROR_MSG,
payload: !incidentRegrex.test(value)
? 'Please enter a valid incident I.D.'
: '',
});
};
const validate = (value: string): boolean => incidentRegrex.test(value);
const isDisabled = (): boolean => {
const incidentId = extractIncidentId(state.incidentName);
return !incidentId;
};
const goToIncident = (): void => {
if (state.incidentName) {
const incidentId = extractIncidentId(state.incidentName);
window.open(`/incident/${incidentId}`, '_blank');
}
};
const handleSeveritySelectionChange = (
selectedOption: SelectPickerOptionProps | SelectPickerOptionProps[] | null,
): void => {
if (selectedOption) {
toast('Updating ticket. Please wait a moment.', {
icon: <LoadingIcon />,
});
dispatch({
type: actionTypes.SET_IS_SEVERITY_PICKER_OPEN,
payload: false,
});
const value = Array.isArray(selectedOption)
? selectedOption[0].value
: selectedOption.value;
updateIncident({
id: parseInt(incidentId, 10),
severityId: value.toString(),
});
}
};
const handleStatusSelectionChange = (
selectedOption: SelectPickerOptionProps | SelectPickerOptionProps[] | null,
): void => {
if (selectedOption) {
dispatch({ type: actionTypes.SET_IS_STATUS_PICKER_OPEN, payload: false });
const value = Array.isArray(selectedOption)
? selectedOption[0].value
: selectedOption.value;
if (value === RESOLVE_STATUS) {
dispatch({
type: actionTypes.SET_DIALOG_TEXT,
payload: 'Resolve incident',
});
dispatch({
type: actionTypes.SET_DIALOG_BODY_TEXT,
payload: 'resolved',
});
reduxDispatch(setOpenDialogResolve(true));
} else {
toast('Updating ticket. Please wait a moment.', {
icon: <LoadingIcon />,
});
updateIncident({
id: parseInt(incidentId, 10),
status: value.toString(),
});
}
}
};
const handleTeamSelectionChange = (
selectedOption: SelectPickerOptionProps | SelectPickerOptionProps[] | null,
): void => {
if (selectedOption) {
dispatch({ type: actionTypes.SET_IS_TEAM_PICKER_OPEN, payload: false });
toast('Updating ticket. Please wait a moment.', {
icon: <LoadingIcon />,
});
const value = Array.isArray(selectedOption)
? selectedOption[0].value
: selectedOption.value;
updateIncident({
id: parseInt(incidentId, 10),
teamId: value.toString(),
});
}
};
const handleCloseConfirmationDialog = () => {
if (UpdateData?.type == 'severity') {
handleSeveritySelectionChange(selectedOption);
} else {
if (UpdateData?.type == 'status') {
handleStatusSelectionChange(selectedOption);
} else {
handleTeamSelectionChange(selectedOption);
}
}
};
const getLabelFromOption = (
option: SelectPickerOptionProps | SelectPickerOptionProps[] | null,
): string | undefined => {
if (Array.isArray(option)) {
return option[0]?.label;
} else {
return option?.label;
}
};
const renderNotAuthorizedDialog = (): JSX.Element => (
<div>
{openNotParticipants && (
<ModalDialog
open={openNotParticipants}
footerButtons={[
{
label: 'Cancel',
onClick: handleopenNotParticipants,
},
{
label: 'Open Channel',
startAdornment: (
<GoToLinkIcon color="var(--navi-color-gray-bg-primary)" />
),
onClick: handleGoToSlackChannel,
},
]}
header="You are not authorised to update this incident"
onClose={handleopenNotParticipants}
>
<Typography variant="p4">
You must be a participant of this incident to update it. Please join
the Slack channel to be added a participant
</Typography>
</ModalDialog>
)}
</div>
);
const renderDuplicateDialog = (): JSX.Element => (
<div>
{openDuplicate ? (
<ModalDialog
open={openDuplicate}
footerButtons={[
{
label: 'Cancel',
onClick: handleResetDialog,
},
{
label: 'Mark as duplicate',
disabled: disable(),
onClick: () => {
markDuplicateIncident(incidentId, duplicateOfId);
},
},
]}
header={`Duplicate incident`}
onClose={handleResetDialog}
>
<Typography variant="p3" color="var(--navi-color-gray-c1)">
Once marked as duplicate, this incident will be archived after 24
hours.
</Typography>
<div className={styles['incident-input']}>
<BorderedInput
inputLabel="Attach incident to"
labelClassName={styles['incident-label']}
fullWidth
value={state.incidentName ? state.incidentName : '_houston-'}
onChange={handleIncidentChange}
error={state.errorMsg}
Icon={<AlertOutlineIcon color="var(--navi-color-red-base)" />}
/>
</div>
<Button
className={styles['hint-text']}
variant="text"
startAdornment={
<GoToLinkIcon color="var(--navi-color-blue-base)" />
}
disabled={isDisabled()}
onClick={goToIncident}
>
Go to incident
</Button>
</ModalDialog>
) : null}
</div>
);
const renderResolveDialog = (): JSX.Element => (
<div>
{openResolve ? (
<ModalDialog
open={openResolve}
footerButtons={[
{
label: 'Cancel',
onClick: () => {
reduxDispatch(setOpenDialogResolve(false));
},
},
{
label: 'Go to slack channel',
onClick: handleGoToSlackChannel,
startAdornment: (
<GoToLinkIcon color="var(--navi-color-gray-bg-primary)" />
),
},
]}
header={`Resolve incident`}
onClose={() => reduxDispatch(setOpenDialogResolve(false))}
>
<Typography variant="p3" color="var(--navi-color-gray-c2)">
Were working on improving this feature. For the time being please
mark this incident as resolved on Slack.
</Typography>
</ModalDialog>
) : null}
</div>
);
const renderUpdateDialog = (): JSX.Element => (
<div>
{openUpdate ? (
<ModalDialog
open={openUpdate}
footerButtons={[
{
label: 'Cancel',
onClick: () => {
reduxDispatch(setOpenDialogUpdate(false));
},
},
{
label: 'Update incident',
onClick: () => {
reduxDispatch(setOpenDialogUpdate(false));
handleCloseConfirmationDialog();
},
},
]}
header={`Are you sure you want to update the incident?`}
onClose={() => reduxDispatch(setOpenDialogUpdate(false))}
>
<div>
<Typography variant="p3" color="var(--navi-color-gray-c2)">
You are updating this incident&lsquo;s &nbsp;
{UpdateData?.type || '..'}
&nbsp; from
<Typography
variant="h4"
color="var(--navi-color-gray-c2)"
className={styles['popup-style']}
>
{UpdateData?.from || '..'}
</Typography>
to&nbsp;
<Typography
variant="h4"
color="var(--navi-color-gray-c2)"
className={styles['popup-style']}
>
{getLabelFromOption(UpdateData?.to) || '..'}
</Typography>
</Typography>
</div>
</ModalDialog>
) : null}
</div>
);
return (
<div>
{renderNotAuthorizedDialog()}
{renderDuplicateDialog()}
{renderResolveDialog()}
{renderUpdateDialog()}
</div>
);
};
export default AllDailogBox;

View File

@@ -0,0 +1,370 @@
import { FC, useReducer, MutableRefObject } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import classnames from 'classnames';
import { Typography } from '@navi/web-ui/lib/primitives';
import { ArrowDownIcon } from '@navi/web-ui/lib/icons';
import { SelectPicker } from '@navi/web-ui/lib/components';
import { SelectPickerOptionProps } from '@navi/web-ui/lib/components/SelectPicker/types';
import useOutsideClick from '@src/services/hooks/useOustideClick';
import { Participant } from '@src/types';
import {
setOpenDialogDuplicate,
setOpenDialogResolve,
setOpenDialogUpdate,
setOpenDialognotParticipants,
setUpdateDetails,
setSelectedOptions,
} from '@src/slices/IncidentSlice';
import { IncidentPageState } from '@src/types';
import AllDailogBox from './AllDailogBox';
import {
generateOptions,
getCurrentData,
getUpdateTypeText,
generateMap,
getUpdateValueText,
} from '../utils';
import {
reducer,
actionTypes,
initialState,
DUPLICATE_STATUS,
RESOLVE_STATUS,
SeverityType,
StatusType,
TeamType,
} from '../constants';
import styles from '../Incidents.module.scss';
const Dropdowns: FC = () => {
const reduxDispatch = useDispatch();
const incidentData = useSelector(
(state: IncidentPageState) => state.incidentLog.incidentData,
);
const incidentParticipants = useSelector(
(state: IncidentPageState) => state.incidentLog.participantsData,
);
const headerData = useSelector(
(state: IncidentPageState) => state.incidentLog.headerData,
);
const [state, dispatch] = useReducer(reducer, initialState);
const updatedSeverities = generateOptions(headerData?.severities);
const updatedStatuses = generateOptions(headerData?.incidentStatuses);
const updatedTeams = generateOptions(headerData?.teams);
const initialSeverity = {
label: incidentData?.severityName,
value: incidentData?.severityId,
};
const initialStatus = {
label: incidentData?.statusName,
value: incidentData?.status,
};
const initialTeam = {
label: incidentData?.teamName,
value: incidentData?.teamId,
};
const { emailId: userEmail } =
JSON.parse(localStorage.getItem('user-data') || '{}') || {};
const participantsList = [
...(incidentParticipants?.participants || []),
...(incidentParticipants?.others || []),
];
const isUserParticipantList = participantsList?.some(
(participant: Participant) => participant.email === userEmail,
);
const severityMap = generateMap(headerData?.severities || []);
const statusMap = generateMap(headerData?.incidentStatuses || []);
const teamsMap = generateMap(headerData?.teams || []);
const currentSev = getCurrentData(incidentData, severityMap, 'severityId');
const currentStatus = getCurrentData(incidentData, statusMap, 'status');
const currentTeam = getCurrentData(incidentData, teamsMap, 'teamId');
const combinedSevClassNames = classnames(
styles[isUserParticipantList ? 'dropdown-box' : 'dropdown-disabled'],
{
[styles['open-box']]: state.isSeverityPickerOpen,
[styles['open-box-disabled']]:
state.isSeverityPickerOpen && !isUserParticipantList,
},
);
const combinedStatusClassNames = classnames(
styles[isUserParticipantList ? 'dropdown-box' : 'dropdown-disabled'],
{
[styles['open-box']]: state.isStatusPickerOpen,
[styles['open-box-disabled']]:
state.isStatusPickerOpen && !isUserParticipantList,
},
);
const combinedTeamClassNames = classnames(
styles[isUserParticipantList ? 'dropdown-box' : 'dropdown-disabled'],
{
[styles['open-box']]: state.isTeamPickerOpen,
[styles['open-box-disabled']]:
state.isTeamPickerOpen && !isUserParticipantList,
},
);
const handleDropdownClick = (changeType: string): (() => void) => {
if (isUserParticipantList) {
switch (changeType) {
case 'severity':
return () => handleSeverityDropdownClick();
case 'status':
return () => handleStatusDropdownClick();
case 'team':
return () => handleTeamDropdownClick();
default:
throw new Error('Invalid change type');
}
} else {
return handleDisabledDropdownClick;
}
};
const handleSeverityDropdownClick = (): void => {
dispatch({
type: actionTypes.SET_IS_SEVERITY_PICKER_OPEN,
payload: !state.isSeverityPickerOpen,
});
};
const handleStatusDropdownClick = (): void => {
dispatch({
type: actionTypes.SET_IS_STATUS_PICKER_OPEN,
payload: !state.isStatusPickerOpen,
});
};
const handleTeamDropdownClick = (): void => {
dispatch({
type: actionTypes.SET_IS_TEAM_PICKER_OPEN,
payload: !state.isTeamPickerOpen,
});
};
const handleDisabledDropdownClick = (): void => {
reduxDispatch(setOpenDialognotParticipants(true));
};
const handleDispatch = (
updateType: number,
selectedOption: SelectPickerOptionProps | SelectPickerOptionProps[],
): void => {
reduxDispatch(
setUpdateDetails({
type: getUpdateTypeText(updateType),
to: selectedOption,
from: getUpdateValueText({
updateType: updateType,
maps: { severityMap, statusMap, teamsMap },
initialValues: { initialSeverity, initialStatus, initialTeam },
}),
}),
);
reduxDispatch(setSelectedOptions(selectedOption));
reduxDispatch(setOpenDialogUpdate(true));
};
const handleOpenConfirmationDailog = (
selectedOption: SelectPickerOptionProps | SelectPickerOptionProps[],
updateType: number,
): void => {
const currentValue =
updateType === SeverityType
? initialSeverity?.value
: updateType === StatusType
? initialStatus?.value
: initialTeam?.value;
const currentState = currentValue?.toString();
const selectedvalue = Array.isArray(selectedOption)
? selectedOption[0].value
: selectedOption.value;
if (currentState !== selectedvalue) {
if (updateType === StatusType && selectedvalue === RESOLVE_STATUS) {
reduxDispatch(setOpenDialogResolve(true));
} else if (
updateType === StatusType &&
selectedvalue === DUPLICATE_STATUS
) {
dispatch({
type: actionTypes.SET_DUPLICATE_DIALOG,
payload: true,
});
reduxDispatch(setOpenDialogDuplicate(true));
} else {
handleDispatch(updateType, selectedOption);
}
}
};
const handleSevClickOutside = (): void => {
dispatch({
type: actionTypes.SET_IS_SEVERITY_PICKER_OPEN,
payload: false,
});
};
const handleStatusClickOutside = (): void => {
dispatch({
type: actionTypes.SET_IS_STATUS_PICKER_OPEN,
payload: false,
});
};
const handleTeamClickOutside = (): void => {
dispatch({
type: actionTypes.SET_IS_TEAM_PICKER_OPEN,
payload: false,
});
};
const refStatus = useOutsideClick({
callback: handleStatusClickOutside,
}) as MutableRefObject<HTMLDivElement>;
const refSeverity = useOutsideClick({
callback: handleSevClickOutside,
}) as MutableRefObject<HTMLDivElement>;
const refTeam = useOutsideClick({
callback: handleTeamClickOutside,
}) as MutableRefObject<HTMLDivElement>;
return (
<div>
<div className={styles['log-update-dropdown']}>
<div className={styles['dropdown-severity']}>
<div>
<Typography variant="p5" color="var(--navi-color-gray-c2">
Severity
</Typography>
</div>
<div ref={refSeverity}>
<div
className={combinedSevClassNames}
onClick={handleDropdownClick('severity')}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{currentSev}
</Typography>
</div>
<div className={styles['arrowdown-style']}>
<ArrowDownIcon />
</div>
</div>
<div className={styles['severity-selectpicker']}>
{state.isSeverityPickerOpen && (
<div className={styles['severity-selectpicker-style']}>
<SelectPicker
multiSelect={false}
onSelectionChange={selectedOption =>
handleOpenConfirmationDailog(selectedOption, SeverityType)
}
options={updatedSeverities}
selectedValue={incidentData?.severityId?.toString()}
/>
</div>
)}
</div>
</div>
</div>
<div className={styles['dropdown-status']}>
<div>
<Typography variant="p5" color="var(--navi-color-gray-c2)">
Status
</Typography>
</div>
<div ref={refStatus}>
<div
className={combinedStatusClassNames}
onClick={handleDropdownClick('status')}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{currentStatus}
</Typography>
</div>
<div>
<ArrowDownIcon className={styles['arrowdown-style']} />
</div>
</div>
<div className={styles['status-selectpicker']}>
{state.isStatusPickerOpen && (
<div className={styles['status-selectpicker-style']}>
<SelectPicker
multiSelect={false}
onSelectionChange={selectedOption =>
handleOpenConfirmationDailog(selectedOption, StatusType)
}
options={updatedStatuses}
selectedValue={incidentData?.status?.toString()}
/>
</div>
)}
</div>
</div>
</div>
<div className={styles['dropdown-team']}>
<div>
<Typography variant="p5" color="var(--navi-color-gray-c2)">
Team
</Typography>
</div>
<div ref={refTeam}>
<div
className={combinedTeamClassNames}
onClick={handleDropdownClick('team')}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{currentTeam}
</Typography>
</div>
<div>
<ArrowDownIcon className={styles['arrowdown-style']} />
</div>
</div>
<div className={styles['team-selectpicker']}>
{state.isTeamPickerOpen && (
<div className={styles['team-selectpicker-style']}>
<SelectPicker
multiSelect={false}
onSelectionChange={selectedOption =>
handleOpenConfirmationDailog(selectedOption, TeamType)
}
options={updatedTeams}
selectedValue={incidentData?.teamId?.toString()}
/>
</div>
)}
</div>
</div>
</div>
</div>
<AllDailogBox />
</div>
);
};
export default Dropdowns;

View File

@@ -1,30 +1,35 @@
import { FC } from 'react';
import { useNavigate } from 'react-router';
import { useSelector } from 'react-redux';
import Typography from '@navi/web-ui/lib/primitives/Typography';
import Tooltip from '@navi/web-ui/lib/primitives/Tooltip';
import Button from '@navi/web-ui/lib/primitives/Button';
import ArrowBackIcon from '@navi/web-ui/lib/icons/ArrowBackIcon';
import CopyIcon from '@src/assets/CopyIcon';
import { handleCopyUrlToClipboard } from '@src/services/globalUtils';
import { IncidentPageState } from '@src/types';
import styles from './Header.module.scss';
interface HeaderProps {
incidentName: string;
title: string;
}
const Header: FC<HeaderProps> = ({ incidentName, title }) => {
const Header: FC = () => {
const navigate = useNavigate();
const handleBacktoDashboard = (): void => {
navigate('/');
navigate(-1);
};
const incidentData = useSelector(
(state: IncidentPageState) => state.incidentLog.incidentData,
);
return (
<div>
<Button variant="text" onClick={handleBacktoDashboard}>
<div className={styles['back-btn']}>
<div className={styles['back-btn-wrapper']}>
<div className={styles['back-btn-icon']}>
<ArrowBackIcon color="#0276FE" width={20} height={20} />
<ArrowBackIcon
color="var(--navi-color-blue-base)"
width={20}
height={20}
/>
</div>
<div className={styles['back-btn-text']}>
<Typography variant="h4" color="var(--navi-color-blue-base)">
@@ -38,7 +43,7 @@ const Header: FC<HeaderProps> = ({ incidentName, title }) => {
<div className={styles['incident-info']}>
<div className={styles['incident-info-text']}>
<Typography variant="h3">
{incidentName} : {title}
{incidentData?.incidentName || '-'} : {incidentData?.title || '-'}
</Typography>
</div>
<div className={styles['incident-info-icon']}>

View File

@@ -2,7 +2,7 @@ import { FC } from 'react';
import { Typography } from '@navi/web-ui/lib/primitives';
import { returnFormattedDate } from '@src/services/globalUtils';
import StepperLogTag from '../StepperAssets/StepperLogTag';
import { CreatedInfoProps } from '../constants';
import { CreatedInfoProps } from '../types';
import styles from '../Incidents.module.scss';
const renderCreatedInfo = (byPerson, TeamAssigned, updatedAt): JSX.Element => (

View File

@@ -1,7 +1,7 @@
import { FC } from 'react';
import Typography from '@navi/web-ui/lib/primitives/Typography';
import { returnFormattedDate } from '@src/services/globalUtils';
import { UpdateInfoProps } from '../constants';
import { UpdateInfoProps } from '../types';
import styles from '../Incidents.module.scss';
const renderUpdateInfo = (fromState, byPerson, updatedAt): JSX.Element => (

View File

@@ -0,0 +1,235 @@
import { FC, useReducer, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Typography, BorderLessInput } from '@navi/web-ui/lib/primitives';
import {
CloseIcon,
DeleteIconOutlined,
ArrowUpSolidIcon,
ArrowDownSolidIcon,
} from '@navi/web-ui/lib/icons';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import LinkIcon from '@src/assets/LinkIcon';
import JiraLogo from '@src/assets/JiraLogo';
import CopyIcon from '@src/assets/CopyIcon';
import LoadingIcon from '@src/assets/LoadingIcon';
import { handleCopyClick } from '@src/services/globalUtils';
import { IncidentPageState } from '@src/types';
import { truncateText, linkSanitization } from '../utils';
import useIncidentApis from '../useIncidentApis';
import {
ActionType,
initialState,
reducer,
JIRA_VALIDATION,
} from '../DescriptionContent/DescriptionContentProps';
import styles from '../DescriptionContent/DescriptionContent.module.scss';
const JiraLinks: FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { id, jiraLinks } = useSelector(
(state: IncidentPageState) => state.incidentLog.incidentData,
);
const storedEmail = localStorage.getItem('email-id') || '';
useEffect(() => {
dispatch({ type: ActionType.SET_JIRA_LINKS, payload: jiraLinks || [] });
}, [jiraLinks]);
const { addJiraLink, removeJiraLink } = useIncidentApis();
const validateLink = (value: string): void => {
const urlPattern = /^https:\/\/navihq\.atlassian\.net\/browse\/.*/;
const invalidChars = /,|\s/;
if (value === '') {
dispatch({ type: ActionType.SET_ERROR_TEXT, payload: '' });
dispatch({ type: ActionType.SET_HELPER_TEXT, payload: '' });
} else {
if (urlPattern.test(value) && !invalidChars.test(value)) {
dispatch({ type: ActionType.SET_ERROR_TEXT, payload: '' });
dispatch({
type: ActionType.SET_HELPER_TEXT,
payload: 'Press enter to add link',
});
} else {
dispatch({ type: ActionType.SET_HELPER_TEXT, payload: '' });
dispatch({
type: ActionType.SET_ERROR_TEXT,
payload: ' Invalid entry. Try entering a different URL',
});
}
}
};
const handleLinkChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const inputValue = e.target.value;
dispatch({ type: ActionType.SET_INPUT_VALUE, payload: inputValue });
validateLink(inputValue);
};
const handleInputKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>,
): void => {
if (event.key === 'Enter') {
if (!state.errorText && state.inputValue.startsWith(JIRA_VALIDATION)) {
const sanitizedLink = linkSanitization(state.inputValue);
const payload = {
incident_id: id,
jira_link: sanitizedLink,
user: storedEmail,
};
toast('Adding link. Please wait a moment.', {
icon: <LoadingIcon />,
});
handleCloseIconClick();
dispatch({ type: ActionType.SET_SHOW_LINKED_TICKETS, payload: true });
addJiraLink(payload);
}
}
};
const handleLinkJiraClick = (): void => {
dispatch({ type: ActionType.SET_SHOW_INPUT, payload: true });
};
const handleCloseIconClick = (): void => {
dispatch({ type: ActionType.SET_INPUT_VALUE, payload: '' });
dispatch({ type: ActionType.SET_ERROR_TEXT, payload: '' });
dispatch({ type: ActionType.SET_HELPER_TEXT, payload: '' });
dispatch({ type: ActionType.SET_SHOW_INPUT, payload: false });
};
const handleLinkedTicketsClick = (): void => {
dispatch({
type: ActionType.SET_SHOW_LINKED_TICKETS,
payload: !state.showLinkedTickets,
});
};
const handleDeleteIconClick = (jiraLinkToDelete: string): void => {
const payload = {
incident_id: id,
jira_link: jiraLinkToDelete,
user: storedEmail,
};
toast('Removing link. Please wait a moment.', {
icon: <LoadingIcon />,
});
removeJiraLink(payload);
};
return (
<div>
<section className={styles['description-content-jira']}>
<div className={styles['flex-row']}>
<JiraLogo />
&nbsp;
<Typography variant="h4" color="var(--navi-color-gray-c2)">
JIRA ticket(s) &nbsp;&nbsp;&nbsp;&nbsp;
</Typography>
</div>
<div>
<div className={styles['flex-row']}>
<div
className={styles['first-row-link']}
onClick={handleLinkedTicketsClick}
>
<Typography
variant="h4"
color="var(--navi-color-gray-c2)"
className={styles['typo-style']}
>
{state.jiraLinks?.filter(link => link !== '').length || 0}{' '}
linked tickets&nbsp;&nbsp;
</Typography>
{state.jiraLinks?.filter(link => link !== '').length > 0 && (
<>
{state.showLinkedTickets && (
<ArrowUpSolidIcon width={8} height={8} />
)}
{!state.showLinkedTickets && (
<ArrowDownSolidIcon width={8} height={8} />
)}
</>
)}
</div>
<div className={styles['vertical-line']} />
<div
className={`${styles['link-ticket-style']} ${
state.showInput ? styles['link-ticket-opacity'] : ''
}`}
onClick={handleLinkJiraClick}
>
<LinkIcon />
&nbsp;
<Typography variant="h4" color="var(--navi-color-blue-base)">
Link JIRA ticket
</Typography>
</div>
</div>
<div>
{state.showLinkedTickets && (
<div>
{state.jiraLinks
.filter(jiraLink => jiraLink !== '')
?.map((jiraLink, index) => (
<div key={index} className={styles['link-row-style']}>
<div>
<div className={styles['jira-link-row']}>
<a
href={jiraLink}
target="_blank"
rel="noreferrer"
className={styles['jira-link']}
>
<Typography
variant="h4"
color="var(--navi-color-blue-base)"
>
{truncateText(jiraLink)}
</Typography>
</a>
</div>
</div>
&nbsp;&nbsp;
<div className={styles['copyicon-style']}>
<CopyIcon onClick={() => handleCopyClick(jiraLink)} />
</div>
<div className={styles['deleteicon-style']}>
<DeleteIconOutlined
onClick={() => handleDeleteIconClick(jiraLink)}
width={20}
height={20}
color="var(--navi-color-gray-c3)"
/>
</div>
</div>
))}
</div>
)}
</div>
{state.showInput && (
<div className={styles['jira-input-row']}>
<div className={styles['row-input-field']}>
<BorderLessInput
inputLabel="Enter ticket link"
onChange={handleLinkChange}
onKeyDown={handleInputKeyDown}
hideClearIcon
error={state.errorText}
hintMsg={state.helperText}
fullWidth={true}
maxLength={50}
/>
</div>
<div className={styles['closeicon-style']}>
<CloseIcon onClick={handleCloseIconClick} />
</div>
</div>
)}
</div>
</section>
</div>
);
};
export default JiraLinks;

View File

@@ -1,16 +0,0 @@
.meta-info-wrapper {
background: #fff;
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 2px 1px rgba(0, 0, 0, 0.06),
0px 1px 1px rgba(0, 0, 0, 0.08);
border-radius: 8px;
padding: 12px;
margin-top: 16px;
width: 65%;
// Remove once UI is available
padding-top: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}

View File

@@ -1,17 +0,0 @@
import { FC } from 'react';
import Typography from '@navi/web-ui/lib/primitives/Typography';
import UnderConstructionIllustration from '@src/assets/UnderConstructionIllustration';
import styles from './MetaInfo.module.scss';
const MetaInfo: FC = () => {
return (
<div className={styles['meta-info-wrapper']}>
<Typography variant="h3">More Info will be updated soon!!</Typography>
<UnderConstructionIllustration width={500} height={500} />
</div>
);
};
export default MetaInfo;

View File

@@ -1,5 +1,3 @@
import exp from 'constants';
export const mapSeverityToTagColor = severity => {
switch (severity) {
case 'Sev-0':

View File

@@ -0,0 +1,49 @@
import { FC } from 'react';
import { useSelector } from 'react-redux';
import { Tooltip, Typography } from '@navi/web-ui/lib/primitives';
import { AlertOutlineIcon } from '@navi/web-ui/lib/icons';
import { IncidentPageState } from '@src/types';
import Dropdowns from '../Dropdowns';
import styles from '../Incidents.module.scss';
const UpdateIncidentBox: FC = () => {
const incidentParticipants = useSelector(
(state: IncidentPageState) => state.incidentLog.participantsData,
);
const { emailId: userEmail } =
JSON.parse(localStorage.getItem('user-data') || '{}') || {};
const participantsList = [
...(incidentParticipants?.participants || []),
...(incidentParticipants?.others || []),
];
const isUserParticipantList = participantsList?.some(
(participant: any) => participant.email === userEmail,
);
return (
<div>
<div className={styles['log-update-text']}>
<Typography variant="h6" color="var(--navi-color-gray-c3)">
UPDATE INCIDENT
</Typography>
{!isUserParticipantList && (
<div className={styles['info-icon']}>
<Tooltip
text="Only Slack channel participants can update this incident."
withPointer
position={'right'}
>
<div className={styles['alert-icon']}>
<AlertOutlineIcon />
</div>
</Tooltip>
</div>
)}
</div>
<Dropdowns />
</div>
);
};
export default UpdateIncidentBox;

View File

@@ -1,5 +1,3 @@
import exp from 'constants';
import { TeamsData } from '../Team/constants';
import { createURL } from '@src/services/globalUtils';
export const IncidentConstants = {
@@ -39,36 +37,7 @@ export const FETCH_AUDIT_LOG = (incidentId: string): string => {
};
export const incidentRegrex = /^_houston-0*[1-9][0-9]*$/;
export interface ContentProps {
incidentData: any;
}
export interface TeamResultsTableProps {
teamsData: Array<TeamsData>;
}
export interface HeaderProps {
incidentId: string;
incidentData: any;
}
export interface UpdateInfoProps {
fromState: string;
byPerson: string;
updatedAt: string;
isLastItem: boolean;
}
export interface CreatedInfoProps {
byPerson: string;
TeamAssigned: string;
updatedAt: string;
isLastItem: boolean;
}
export interface ResponseType {
data: '';
status: number;
}
export const actionTypes = {
SET_INCIDENT_DATA: 'SET_INCIDENT_DATA',
SET_HEADER_DATA: 'SET_HEADER_DATA',

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
import { TeamsData } from '@src/Pages/Team/constants';
import { UpdateIncidentType } from '@src/types';
export interface ContentProps {
incidentData: any;
}
export interface TeamResultsTableProps {
teamsData: Array<TeamsData>;
}
export interface HeaderProps {
incidentId: string;
incidentData: any;
}
export interface UpdateInfoProps {
fromState: string;
byPerson: string;
updatedAt: string;
isLastItem: boolean;
}
export interface CreatedInfoProps {
byPerson: string;
TeamAssigned: string;
updatedAt: string;
isLastItem: boolean;
}
export interface ResponseType {
data: '';
status: number;
}
export interface JiraLinkPayload {
incident_id: number | null;
jira_link: string;
user: string;
}
export interface useIncidentApiProps {
fetchIncidentLog: (incidentId: string) => void;
startIncidentSearch: (incidentId: string) => void;
fetchHeaderDetails: () => void;
fetchParticipants: (slackChannel: string) => void;
updateIncident: (payload: UpdateIncidentType) => void;
markDuplicateIncident: (
incidentId: string,
duplicateOfId: number | null,
) => void;
addJiraLink: (payload: JiraLinkPayload) => void;
removeJiraLink: (payload: JiraLinkPayload) => void;
}

View File

@@ -0,0 +1,144 @@
import { useDispatch } from 'react-redux';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import { ApiService } from '@src/services/api';
import {
setIncidentData,
setIncidentLogData,
setHeaderData,
setParticipantsData,
} from '@src/slices/IncidentSlice';
import LoadingIcon from '@src/assets/LoadingIcon';
import { UpdateIncidentType } from '@src/types';
import { JiraLinkPayload, ResponseType } from '@src/Pages/Incidents/types';
import {
DUPLICATE_STATUS,
LINK_JIRA_INCIDENT,
UNLINK_JIRA_INCIDENT,
} from '@src/Pages/Incidents/constants';
import { setOpenDialogDuplicate } from '@src/slices/IncidentSlice';
import { useIncidentApiProps } from './types';
import {
FETCH_INCIDENT_DATA,
UPDATE_INCIDENT,
FETCH_HEADER_DETAILS,
FETCH_AUDIT_LOG,
FETCH_PARTICIPANTS_DATA,
} from './constants';
const useIncidentApis = (): useIncidentApiProps => {
const dispatch = useDispatch();
const handleApiError = error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message},`
: ''
}`;
toast.error(toastMessage);
};
const fetchIncidentLog = (incidentId: string): void => {
const endPoint = FETCH_AUDIT_LOG(incidentId);
ApiService.get(endPoint)
.then(response => {
dispatch(setIncidentLogData(response?.data));
})
.catch(handleApiError);
};
const startIncidentSearch = (incidentId): void => {
const endPoint = FETCH_INCIDENT_DATA(incidentId);
ApiService.get(endPoint)
.then(response => {
dispatch(setIncidentData(response?.data?.data));
fetchIncidentLog(incidentId);
fetchParticipants(response?.data?.data?.slackChannel);
})
.catch(handleApiError);
};
const fetchHeaderDetails = (): void => {
const endPoint = FETCH_HEADER_DETAILS;
ApiService.get(endPoint)
.then(response => {
dispatch(setHeaderData(response?.data?.data));
})
.catch(handleApiError);
};
const fetchParticipants = (slackChannel: string): void => {
const endPoint = FETCH_PARTICIPANTS_DATA(slackChannel);
ApiService.get(endPoint)
.then(response => {
dispatch(setParticipantsData(response?.data?.data));
})
.catch(handleApiError);
};
const updateIncident = (payload: UpdateIncidentType): void => {
const endPoint = UPDATE_INCIDENT;
ApiService.post(endPoint, payload)
.then(response => {
toast.success('Incident updated successfully');
startIncidentSearch(payload?.id);
})
.catch(handleApiError);
};
const markDuplicateIncident = (
incidentId: string,
duplicateOfId: number | null,
): void => {
const endPoint = UPDATE_INCIDENT;
toast('Updating ticket. Please wait a moment.', {
icon: <LoadingIcon />,
});
ApiService.post(endPoint, {
id: parseInt(incidentId, 10),
status: DUPLICATE_STATUS,
duplicateOfId: duplicateOfId,
})
.then((response: ResponseType) => {
if (response?.status === 200) {
const toastMessage = `This incident is marked as duplicate of _houston-${duplicateOfId}`;
toast.success(toastMessage);
startIncidentSearch(incidentId);
dispatch(setOpenDialogDuplicate(false));
}
})
.catch(handleApiError);
};
const addJiraLink = (payload: JiraLinkPayload): void => {
const endPoint = LINK_JIRA_INCIDENT;
ApiService.post(endPoint, payload)
.then(response => {
toast.success(`${response?.data?.data}`);
startIncidentSearch(payload?.incident_id);
})
.catch(handleApiError);
};
const removeJiraLink = (payload: JiraLinkPayload): void => {
const endPoint = UNLINK_JIRA_INCIDENT;
ApiService.post(endPoint, payload)
.then(response => {
toast.info(`${response?.data?.data}`);
startIncidentSearch(payload?.incident_id);
})
.catch(handleApiError);
};
return {
fetchIncidentLog,
startIncidentSearch,
fetchHeaderDetails,
fetchParticipants,
updateIncident,
markDuplicateIncident,
addJiraLink,
removeJiraLink,
};
};
export default useIncidentApis;

View File

@@ -1,15 +1,14 @@
import { SeverityType, StatusType, TeamType } from './constants';
export const getUpdateTypeText = (updateType: number): string => {
switch (updateType) {
case SeverityType:
return ' severity ';
return 'severity';
case StatusType:
return ' status ';
return 'status';
case TeamType:
return ' team ';
return 'team';
default:
return ' severity ';
return 'severity';
}
};
@@ -23,9 +22,91 @@ type DataItemModified = {
label: string;
};
interface Maps {
severityMap: { [key: number]: string };
statusMap: { [key: number]: string };
teamsMap: { [key: number]: string };
}
interface InitialValues {
initialSeverity?: { value: number | null };
initialStatus?: { value: number | null };
initialTeam?: { value: number | null };
}
export const generateOptions = (data: DataItem[]): DataItemModified[] => {
return (data || []).map(item => ({
value: item.value.toString(),
label: item.label,
}));
};
export const truncateText = (text: string): string => {
const jiraTicketMatch = text.match(/\/browse\/([^/]+)/);
if (jiraTicketMatch && jiraTicketMatch[1]) {
return jiraTicketMatch[1];
}
return text;
};
export const linkSanitization = (link: string): string => {
const sanitizedLinkMatch = link.match(
/(https:\/\/navihq.atlassian.net\/browse\/[^/]+)/,
);
if (sanitizedLinkMatch && sanitizedLinkMatch[1]) {
return sanitizedLinkMatch[1];
}
return link;
};
export const getCurrentData = (
data: { [key: string]: any },
dataMap: { [key: string]: string },
key: string,
): string => {
return dataMap && data[key] ? dataMap[data[key]] : '-';
};
export const generateMap = (data: DataItem[]): { [key: number]: string } => {
return data.reduce((map, item) => {
map[item.value] = item.label;
return map;
}, {});
};
export const getUpdateValueText = ({
updateType,
maps,
initialValues,
}: {
updateType: number | null;
maps: Maps;
initialValues: InitialValues;
}): string => {
switch (updateType) {
case SeverityType:
return ` ${
maps.severityMap && initialValues.initialSeverity?.value
? maps.severityMap[initialValues.initialSeverity.value]
: '-'
} `;
case StatusType:
return ` ${
maps.statusMap && initialValues.initialStatus?.value
? maps.statusMap[initialValues.initialStatus.value]
: '-'
} `;
case TeamType:
return ` ${
maps.teamsMap && initialValues.initialTeam?.value
? maps.teamsMap[initialValues.initialTeam.value]
: '-'
} `;
default:
return ` ${
maps.severityMap && initialValues.initialSeverity?.value
? maps.severityMap[initialValues.initialSeverity.value]
: '-'
} `;
}
};

View File

@@ -13,7 +13,7 @@ import useClickStream from '@src/services/clickStream';
import { CLICK_STREAM_EVENT_FACTORY } from '@src/services/clickStream/constants/values';
import { ApiService } from '@src/services/api';
import MembersDetails from '@src/components/MembersDetails';
import DrawerStyles from '@src/Pages/Incidents/DrawerMode/DrawerMode.module.scss';
import DrawerStyles from './TeamForm.module.scss';
import styles from '../Team.module.scss';
import { ConfigProvider, Select, ThemeConfig } from 'antd';
import { getBots } from '../bots';

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import Typography from '@navi/web-ui/lib/primitives/Typography';
import { Accordion, AccordionGroup } from '@navi/web-ui/lib/primitives';
import { TeamResultsTableProps } from '@src/Pages/Incidents/constants';
import { TeamResultsTableProps } from '@src/Pages/Incidents/types';
import useClickStream from '@src/services/clickStream';
import { CLICK_STREAM_EVENT_FACTORY } from '@src/services/clickStream/constants/values';
import TeamForm from './TeamForm';

View File

@@ -24,7 +24,6 @@ const MultiSelectPickerWithSearch: FC<MultiSelectPickerWithSearchProps> = ({
multiSelect,
placeholder,
debounceDelay,
inputSize = 'full-width',
pickerWrapperClassName,
clearAll = false,
optionGroupKey = '',
@@ -172,7 +171,7 @@ const MultiSelectPickerWithSearch: FC<MultiSelectPickerWithSearchProps> = ({
return options.length > searchEnableThreshold ? (
<div className={styles['search-input-wrapper']}>
<SearchBarInput
inputSize={inputSize}
inputSize={'full-width'}
placeholder={placeholder}
value={searchValue}
onSearchChange={handleOnChange}

View File

@@ -0,0 +1,159 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { SelectPickerOptionProps } from '@navi/web-ui/lib/components/SelectPicker/types';
import {
IncidentDatatype,
HeaderDatatype,
ParticipantsDatatype,
UpdateDetailsType,
IncidentLogDataType,
IncidentLogPageState,
IncidentPageState,
} from '@src/types';
const initialState: IncidentLogPageState = {
incidentLogData: {
relation_name: '',
record_id: null,
logs: [],
has_creation: false,
},
incidentData: {
id: null,
title: '',
description: '',
status: null,
statusName: '',
severityId: null,
severityName: '',
incidentName: null,
slackChannel: '',
detectionTime: null,
startTime: null,
endTime: null,
teamId: null,
teamName: '',
jiraLinks: [],
confluenceId: null,
severityTat: null,
remindMeAt: null,
enableReminder: false,
createdBy: '',
updatedBy: '',
createdAt: null,
updatedAt: null,
rcaLink: '',
},
headerData: {
severities: [],
incidentStatuses: [],
teams: [],
},
participantsData: {
participants: [],
others: [],
},
updateDetails: {
type: null,
from: null,
to: null,
},
openDialogUpdate: false,
openDialogDuplicate: false,
openDialogResolve: false,
openDialognotParticipants: false,
selectedOptions: null,
};
const incidentLogSlice = createSlice({
name: 'incidentLog',
initialState,
reducers: {
setIncidentLogData: (
state,
action: PayloadAction<IncidentLogDataType>,
): void => {
state.incidentLogData = action.payload;
},
setIncidentData: (state, action: PayloadAction<IncidentDatatype>): void => {
state.incidentData = action.payload;
},
setHeaderData: (state, action: PayloadAction<HeaderDatatype>): void => {
state.headerData = action.payload;
},
setParticipantsData: (
state,
action: PayloadAction<ParticipantsDatatype>,
): void => {
state.participantsData = action.payload;
},
setOpenDialogUpdate: (state, action: PayloadAction<boolean>): void => {
state.openDialogUpdate = action.payload;
},
setOpenDialogDuplicate: (state, action: PayloadAction<boolean>): void => {
state.openDialogDuplicate = action.payload;
},
setOpenDialogResolve: (state, action: PayloadAction<boolean>): void => {
state.openDialogResolve = action.payload;
},
setOpenDialognotParticipants: (
state,
action: PayloadAction<boolean>,
): void => {
state.openDialognotParticipants = action.payload;
},
setUpdateDetails: (
state,
action: PayloadAction<UpdateDetailsType>,
): void => {
state.updateDetails = { ...state.updateDetails, ...action.payload };
},
setSelectedOptions: (
state,
action: PayloadAction<
SelectPickerOptionProps | SelectPickerOptionProps[]
>,
): void => {
state.selectedOptions = action.payload;
},
resetIncidentLogState: (state): void => {
state.incidentLogData = {} as IncidentLogDataType;
state.incidentData = {} as IncidentDatatype;
state.headerData = {} as HeaderDatatype;
state.participantsData = {} as ParticipantsDatatype;
state.updateDetails = {} as UpdateDetailsType;
state.openDialogUpdate = false;
state.openDialogDuplicate = false;
state.openDialogResolve = false;
state.openDialognotParticipants = false;
state.selectedOptions = {} as
| SelectPickerOptionProps
| SelectPickerOptionProps[];
},
},
});
export const selectIsLastChangeIndex = (state: IncidentPageState): number => {
return (state.incidentLog.incidentLogData?.data?.logs || [])
.slice()
.reverse()
.findIndex(log =>
log.changes.some(change =>
['Status', 'SeverityId', 'TeamId'].includes(change.attribute),
),
);
};
export const {
setIncidentData,
setIncidentLogData,
setHeaderData,
setParticipantsData,
setOpenDialogUpdate,
setOpenDialogDuplicate,
setOpenDialogResolve,
setOpenDialognotParticipants,
setUpdateDetails,
setSelectedOptions,
} = incidentLogSlice.actions;
export default incidentLogSlice.reducer;

View File

@@ -6,6 +6,7 @@ import dashboardReducer from '../slices/dashboardSlice';
import teamReducer1 from '../slices/team1Slice';
import jiraDashboardReducer from '../slices/jiraDashboardSlice';
import incidentLogReducer from '@src/slices/IncidentSlice';
const store = configureStore({
reducer: {
@@ -14,6 +15,7 @@ const store = configureStore({
dashboard: dashboardReducer,
team1: teamReducer1,
jiraDashboard: jiraDashboardReducer,
incidentLog: incidentLogReducer,
},
});

124
src/types/index.d.ts vendored
View File

@@ -1,3 +1,5 @@
import { SelectPickerOptionProps } from '@navi/web-ui/lib/components/SelectPicker/types';
import Severity from '@src/Pages/Severity';
export {};
interface AppConfig {
@@ -53,3 +55,125 @@ export interface PageDetails {
pageSize: number;
totalElements: number;
}
export interface IncidentDatatype {
id: number | null;
title: string;
description: string;
status: number | null;
statusName: string;
severityId: number | null;
severityName: string;
incidentName: string | null;
slackChannel: string;
detectionTime: Date | null;
startTime: Date | null;
endTime: Date | null;
teamId: number | null;
teamName: string;
jiraLinks: string[];
confluenceId: number | null;
severityTat: number | null;
remindMeAt: Date | null;
enableReminder: boolean;
createdBy: string;
updatedBy: string;
createdAt: Date | null;
updatedAt: Date | null;
rcaLink: string;
}
interface Severity {
value: number;
label: string;
}
interface IncidentStatus {
value: number;
label: string;
}
interface Team {
value: number;
label: string;
}
export interface HeaderDatatype {
severities: Severity[];
incidentStatuses: IncidentStatus[];
teams: Team[];
}
export interface Participant {
id: string;
name: string;
email?: string;
image: string;
}
interface Other {
id: string;
name: string;
email?: string;
image: string;
}
export interface ParticipantsDatatype {
participants: Participant[];
others: Other[];
}
export interface UpdateDetailsType {
type: string | null;
from: string | null;
to: SelectPickerOptionProps | SelectPickerOptionProps[] | null;
}
export interface IncidentLogDataType {
relation_name: string;
record_id: number | null;
logs: LogType[];
has_creation: boolean;
}
interface LogType {
created_at: string;
user_info: UserInfoType;
changes: ChangeType[];
}
interface UserInfoType {
id: string;
name: string;
email: string;
}
interface ChangeType {
to: string;
from: string;
attribute: string;
}
export interface IncidentLogPageState {
incidentLogData: IncidentLogDatatype;
incidentData: IncidentDatatype;
headerData: HeaderDatatype;
participantsData: ParticipantsDatatype;
updateDetails: UpdateDetailsType;
openDialogUpdate: boolean;
openDialogDuplicate: boolean;
openDialogResolve: boolean;
openDialognotParticipants: boolean;
selectedOptions: SelectPickerOptionProps | SelectPickerOptionProps[] | null;
}
export interface IncidentPageState {
incidentLog: IncidentLogPageState;
}
export interface UpdateIncidentType {
id: number;
severityId?: string;
teamId?: string;
status?: string;
}