Merge branch 'master' into search-params-for-teams
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
<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} />
|
||||
@@ -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 />
|
||||
|
||||
<Typography variant="h4" color="var(--navi-color-gray-c2)">
|
||||
JIRA ticket(s)
|
||||
</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
|
||||
</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 />
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
383
src/Pages/Incidents/Dropdowns/AllDailogBox.tsx
Normal file
383
src/Pages/Incidents/Dropdowns/AllDailogBox.tsx
Normal 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)">
|
||||
We’re 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‘s
|
||||
{UpdateData?.type || '..'}
|
||||
from
|
||||
<Typography
|
||||
variant="h4"
|
||||
color="var(--navi-color-gray-c2)"
|
||||
className={styles['popup-style']}
|
||||
>
|
||||
{UpdateData?.from || '..'}
|
||||
</Typography>
|
||||
to
|
||||
<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;
|
||||
370
src/Pages/Incidents/Dropdowns/index.tsx
Normal file
370
src/Pages/Incidents/Dropdowns/index.tsx
Normal 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;
|
||||
@@ -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']}>
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
235
src/Pages/Incidents/JiraLinks/index.tsx
Normal file
235
src/Pages/Incidents/JiraLinks/index.tsx
Normal 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 />
|
||||
|
||||
<Typography variant="h4" color="var(--navi-color-gray-c2)">
|
||||
JIRA ticket(s)
|
||||
</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
|
||||
</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 />
|
||||
|
||||
<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>
|
||||
|
||||
<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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,5 +1,3 @@
|
||||
import exp from 'constants';
|
||||
|
||||
export const mapSeverityToTagColor = severity => {
|
||||
switch (severity) {
|
||||
case 'Sev-0':
|
||||
|
||||
49
src/Pages/Incidents/UpdateIncidentBox/index.tsx
Normal file
49
src/Pages/Incidents/UpdateIncidentBox/index.tsx
Normal 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;
|
||||
@@ -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
52
src/Pages/Incidents/types.ts
Normal file
52
src/Pages/Incidents/types.ts
Normal 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;
|
||||
}
|
||||
144
src/Pages/Incidents/useIncidentApis.tsx
Normal file
144
src/Pages/Incidents/useIncidentApis.tsx
Normal 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;
|
||||
@@ -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]
|
||||
: '-'
|
||||
} `;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
159
src/slices/IncidentSlice.tsx
Normal file
159
src/slices/IncidentSlice.tsx
Normal 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;
|
||||
@@ -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
124
src/types/index.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user