TP-48516 | Mark incident as duplicate from Houston UI (#105)

* TP-48516 | design implemented

* TP-48516 | updated Goto  incident and css changes

* TP-48516 | Api integrated

* TP-48516 | added input validations , api integration

* TP-48516 | Disabled duplicate button

* TP-48516 | resolving PR commits

* TP-48516 | resolved error message color bug

* TP-48516 | added return type

* TP-48516 |  resolving PR comments

* TP-48516 | padding for GoTo CTA increased
This commit is contained in:
Pooja Jaiswal
2024-01-08 14:39:32 +05:30
committed by GitHub
parent c60a56fb08
commit 4a96a09077
4 changed files with 410 additions and 223 deletions

View File

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

View File

@@ -183,3 +183,25 @@
.popup-style {
display: inline;
}
.hint-text {
margin-top: 24px;
font-size: 12px;
color: var(--navi-color-blue-base);
border: none;
:hover {
cursor: pointer;
}
}
.incident-input {
margin-top: 20px;
input {
color: var(--navi-color-gray-c2);
}
}
.incident-label {
font-size: 13px;
font-weight: 400;
}

View File

@@ -15,7 +15,7 @@ export const IncidentConstants = {
incidentId: 'Incident Id',
};
const URL_PREFIX = createURL('/houston');
export const URL_PREFIX = createURL('/houston');
export const FETCH_INCIDENT_DATA = (payload: any): string => {
return `${URL_PREFIX}/incidents/${payload}`;
@@ -38,6 +38,7 @@ export const FETCH_AUDIT_LOG = (incidentId: string): string => {
return `${URL_PREFIX}/logs/incident/${incidentId}`;
};
export const incidentRegrex = /^_houston-0*[1-9][0-9]*$/;
export interface ContentProps {
incidentData: any;
}
@@ -64,7 +65,10 @@ export interface CreatedInfoProps {
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',
@@ -84,6 +88,10 @@ export const actionTypes = {
SET_IS_SEVERITY_PICKER_OPEN: 'SET_IS_SEVERITY_PICKER_OPEN',
SET_IS_STATUS_PICKER_OPEN: 'SET_IS_STATUS_PICKER_OPEN',
SET_IS_TEAM_PICKER_OPEN: 'SET_IS_TEAM_PICKER_OPEN',
SET_DUPLICATE_DIALOG: 'SET_DUPLICATE_DIALOG',
SET_INCIDENT_NAME: 'SET_INCIDENT_NAME',
SET_ERROR_MSG: 'SET_ERROR_MSG',
RESET_DUPLICATE_DIALOG: 'RESET_DUPLICATE_DIALOG',
};
export const reducer = (state, action) => {
@@ -108,6 +116,8 @@ export const reducer = (state, action) => {
return { ...state, totalLog: action.payload };
case actionTypes.SET_OPEN_DIALOG:
return { ...state, openDialog: action.payload };
case actionTypes.SET_DUPLICATE_DIALOG:
return { ...state, openDuplicateDialog: action.payload };
case actionTypes.SET_OPEN_CONFIRMATION_DIALOG:
return { ...state, openConfirmationDialog: action.payload };
case actionTypes.SET_SELECTED_OPTION:
@@ -124,6 +134,17 @@ export const reducer = (state, action) => {
return { ...state, isStatusPickerOpen: action.payload };
case actionTypes.SET_IS_TEAM_PICKER_OPEN:
return { ...state, isTeamPickerOpen: action.payload };
case actionTypes.SET_INCIDENT_NAME:
return { ...state, incidentName: action.payload };
case actionTypes.SET_ERROR_MSG:
return { ...state, errorMsg: action.payload };
case actionTypes.RESET_DUPLICATE_DIALOG:
return {
...state,
openDuplicateDialog: false,
incidentName: '',
errorMsg: '',
};
default:
return state;
}
@@ -148,6 +169,9 @@ export const initialState = {
isSeverityPickerOpen: false,
isStatusPickerOpen: false,
isTeamPickerOpen: false,
incidentName: '',
openDuplicateDialog: false,
errorMsg: '',
};
export const RESOLVE_STATUS = '4';

View File

@@ -1,12 +1,18 @@
import { FC, useEffect, useState, useReducer, MutableRefObject } from 'react';
import { useMatch } from 'react-router-dom';
import classnames from 'classnames';
import { Tabs } from 'antd';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import { AlertOutlineIcon, ArrowDownIcon } from '@navi/web-ui/lib/icons';
import TabItem from '@navi/web-ui/lib-esm/components/Tabs/TabItem';
import Tabs from '@navi/web-ui/lib-esm/components/Tabs/Tabs';
import { SelectPicker } from '@navi/web-ui/lib/components';
import { ModalDialog, Tooltip, Typography } from '@navi/web-ui/lib/primitives';
import {
BorderedInput,
ModalDialog,
Tooltip,
Typography,
Button,
} from '@navi/web-ui/lib/primitives';
import { SelectPickerOptionProps } from '@navi/web-ui/lib/components/SelectPicker/types';
import GoToLinkIcon from '@src/assets/GoToLinkIcon';
import { ApiService } from '@src/services/api';
@@ -34,15 +40,15 @@ import {
StatusType,
TeamType,
SLACK_BASE_URL,
incidentRegrex,
ResponseType,
} from './constants';
import styles from './Incidents.module.scss';
const Incident: FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { fireEvent } = useClickStream();
const { EVENT_NAME, SCREEN_NAME } = CLICK_STREAM_EVENT_FACTORY;
const updatedSeverities = generateOptions(state.headerData?.severities);
const updatedStatuses = generateOptions(state.headerData?.incidentStatuses);
const updatedTeams = generateOptions(state.headerData?.teams);
@@ -54,6 +60,8 @@ const Incident: FC = () => {
path: '/incident/:incidentId',
});
const incidentId = IncidentMatch?.params?.incidentId || '';
const currentIncidentId = parseInt(incidentId, 10);
const incidentName = state.incidentName;
const severityMap = state.headerData?.severities?.reduce((map, severity) => {
map[severity.value] = severity.label;
@@ -70,6 +78,7 @@ const Incident: FC = () => {
map[Incidentteam.value] = Incidentteam.label;
return map;
}, {});
const handleSevClickOutside = () => {
dispatch({
type: actionTypes.SET_IS_SEVERITY_PICKER_OPEN,
@@ -376,16 +385,6 @@ const Incident: FC = () => {
payload: 'resolved',
});
dispatch({ type: actionTypes.SET_OPEN_DIALOG, payload: true });
} else if (value === DUPLICATE_STATUS) {
dispatch({
type: actionTypes.SET_DIALOG_TEXT,
payload: 'Duplicate incident',
});
dispatch({
type: actionTypes.SET_DIALOG_BODY_TEXT,
payload: 'duplicate',
});
dispatch({ type: actionTypes.SET_OPEN_DIALOG, payload: true });
} else {
toast('Updating ticket. Please wait a moment.', {
icon: <LoadingIcon />,
@@ -436,11 +435,16 @@ const Incident: FC = () => {
payload: false,
});
dispatch({ type: actionTypes.SET_IS_TEAM_PICKER_OPEN, payload: false });
if (
updateType === StatusType &&
(selectedvalue === RESOLVE_STATUS || selectedvalue === DUPLICATE_STATUS)
) {
if (updateType === StatusType && selectedvalue === RESOLVE_STATUS) {
handleStatusSelectionChange(selectedOption);
} else if (
updateType === StatusType &&
selectedvalue === DUPLICATE_STATUS
) {
dispatch({
type: actionTypes.SET_DUPLICATE_DIALOG,
payload: true,
});
} else {
dispatch({ type: actionTypes.SET_UPDATE_TYPE, payload: updateType });
dispatch({
@@ -508,6 +512,81 @@ const Incident: FC = () => {
} `;
}
};
const handleIncidentChange = (
e: React.ChangeEvent<HTMLInputElement>,
): void => {
const inputValue = e.target.value;
validateIncidentID(inputValue);
dispatch({
type: actionTypes.SET_INCIDENT_NAME,
payload: inputValue,
});
};
const handleResetDialog = (): void => {
dispatch({
type: actionTypes.RESET_DUPLICATE_DIALOG,
});
};
const validateIncidentID = (value: string): void => {
dispatch({
type: actionTypes.SET_ERROR_MSG,
payload: !incidentRegrex.test(value)
? 'Please enter a valid incident I.D.'
: '',
});
};
const extractIncidentId = (incidentName: string): number | null => {
return (match => (match ? parseInt(match[1], 10) : null))(
incidentName.match(/_houston-(\d+)/),
);
};
const duplicateOfId = extractIncidentId(incidentName);
const disable = (): boolean => {
return !incidentName || !validate(incidentName);
};
const goToIncident = (): void => {
if (state.incidentName) {
const incidentId = extractIncidentId(state.incidentName);
window.open(`/incident/${incidentId}`, '_blank');
}
};
const validate = (value: string): boolean => incidentRegrex.test(value);
const isDisabled = (): boolean => {
const incidentId = extractIncidentId(state.incidentName);
return !incidentId;
};
const markDuplicateIncident = (): void => {
const endPoint = UPDATE_INCIDENT;
toast('Updating ticket. Please wait a moment.', {
icon: <LoadingIcon />,
});
ApiService.post(endPoint, {
id: currentIncidentId,
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();
fetchIncidentLog();
handleResetDialog();
fetchParticipants();
}
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message}`
: 'Something went wrong. Please try again.'
}`;
toast.error(toastMessage);
startIncidentSearch();
});
};
return (
<ErrorBoundary>
@@ -521,222 +600,232 @@ const Incident: FC = () => {
<div className={styles['tab-content-wrapper']}>
<Tabs
onTabChange={function noRefCheck() {}}
defaultSelectedKey={'detail-key'}
>
<TabItem label="Details" key="detail-key">
<div className={styles['horizontal-line']}>
<div className={styles['content-wrapper']}>
<div className={styles['audit-log']}>
<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>
<div className={styles['log-update-dropdown']}>
<div className={styles['dropdown-severity']}>
<div>
defaultActiveKey="1"
items={[
{
key: '1',
label: 'Details',
children: (
<div>
<div className={styles['content-wrapper']}>
<div className={styles['audit-log']}>
<div className={styles['log-update-text']}>
<Typography
variant="p5"
color="var(--navi-color-gray-c2"
variant="h6"
color="var(--navi-color-gray-c3)"
>
Severity
UPDATE INCIDENT
</Typography>
</div>
<div ref={refSeverity}>
<div
className={combinedSevClassNames}
onClick={handleSevClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
{!isUserParticipantList && (
<div className={styles['info-icon']}>
<Tooltip
text="Only Slack channel participants can update this incident."
withPointer
position={'right'}
>
{severityMap && state.severity?.value
? severityMap[state.severity.value]
: '-'}
</Typography>
<div className={styles['alert-icon']}>
<AlertOutlineIcon />
</div>
</Tooltip>
</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={state.severity?.value.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={handleStatusClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{incidentStatusMap && state.status?.value
? incidentStatusMap[state.status.value]
: '-'}
</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={state.status?.value.toString()}
/>
</div>
)}
</div>
</div>
</div>
<div className={styles['dropdown-team']}>
<div>
<Typography variant="p5" color="#585757">
Team
</Typography>
</div>
<div ref={refTeam}>
<div
className={combinedTeamClassNames}
onClick={handleTeamClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{teamsMap && state.team?.value
? teamsMap[state.team.value]
: '-'}
</Typography>
</div>
<div className={styles['log-update-dropdown']}>
<div className={styles['dropdown-severity']}>
<div>
<ArrowDownIcon
className={styles['arrowdown-style']}
/>
<Typography
variant="p5"
color="var(--navi-color-gray-c2"
>
Severity
</Typography>
</div>
<div ref={refSeverity}>
<div
className={combinedSevClassNames}
onClick={handleSevClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{severityMap && state.severity?.value
? severityMap[state.severity.value]
: '-'}
</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={state.severity?.value.toString()}
/>
</div>
)}
</div>
</div>
</div>
<div className={styles['team-selectpicker']}>
{state.isTeamPickerOpen && (
<div
className={styles['team-selectpicker-style']}
<div className={styles['dropdown-status']}>
<div>
<Typography
variant="p5"
color="var(--navi-color-gray-c2)"
>
<SelectPicker
multiSelect={false}
onSelectionChange={selectedOption =>
handleOpenConfirmationDailog(
selectedOption,
TeamType,
)
}
options={updatedTeams}
selectedValue={state.team?.value.toString()}
/>
Status
</Typography>
</div>
<div ref={refStatus}>
<div
className={combinedStatusClassNames}
onClick={handleStatusClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{incidentStatusMap && state.status?.value
? incidentStatusMap[state.status.value]
: '-'}
</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={state.status?.value.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={handleTeamClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{teamsMap && state.team?.value
? teamsMap[state.team.value]
: '-'}
</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={state.team?.value.toString()}
/>
</div>
)}
</div>
</div>
</div>
</div>
<ActivityLog
incidentLog={state.incidentLog}
totalLog={state.totalLog}
/>
</div>
</div>
{
<ActivityLog
incidentLog={state.incidentLog}
totalLog={state.totalLog}
<DescriptionContent
id={state.incidentData?.id}
incidentName={state.incidentData?.incidentName}
description={state.incidentData?.description}
slackChannel={state.incidentData?.slackChannel}
incidentParticipants={state.incidentParticipants}
jiraIds={state.incidentData?.jiraLinks}
rcaLink={state.incidentData?.rcaLink}
/>
}
</div>
</div>
<DescriptionContent
id={state.incidentData?.id}
incidentName={state.incidentData?.incidentName}
description={state.incidentData?.description}
slackChannel={state.incidentData?.slackChannel}
incidentParticipants={state.incidentParticipants}
jiraIds={state.incidentData?.jiraLinks}
rcaLink={state.incidentData?.rcaLink}
/>
</div>
</div>
</TabItem>
</Tabs>
),
},
]}
/>
</div>
{open && (
<ModalDialog
@@ -748,7 +837,9 @@ const Incident: FC = () => {
},
{
label: 'Open Channel',
startAdornment: <GoToLinkIcon color="#fff" />,
startAdornment: (
<GoToLinkIcon color="var(--navi-color-gray-bg-primary)" />
),
onClick: handleGoToSlackChannel,
},
]}
@@ -761,6 +852,54 @@ const Incident: FC = () => {
</Typography>
</ModalDialog>
)}
{state.openDuplicateDialog ? (
<ModalDialog
open={state.openDuplicateDialog}
footerButtons={[
{
label: 'Cancel',
onClick: handleResetDialog,
},
{
label: 'Mark as duplicate',
disabled: disable(),
onClick: () => {
markDuplicateIncident();
},
},
]}
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}
{state.openDialog ? (
<ModalDialog
open={state.openDialog}
@@ -777,7 +916,9 @@ const Incident: FC = () => {
{
label: 'Go to slack channel',
onClick: handleGoToSlackChannel,
startAdornment: <GoToLinkIcon color="#fff" />,
startAdornment: (
<GoToLinkIcon color="var(--navi-color-gray-bg-primary)" />
),
},
]}
header={`${state.dialogText}`}