diff --git a/package.json b/package.json index f528df1..8af7131 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Pages/Incidents/Incidents.module.scss b/src/Pages/Incidents/Incidents.module.scss index e55fe1a..309e996 100644 --- a/src/Pages/Incidents/Incidents.module.scss +++ b/src/Pages/Incidents/Incidents.module.scss @@ -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; +} diff --git a/src/Pages/Incidents/constants.ts b/src/Pages/Incidents/constants.ts index 91a6ce5..d030070 100644 --- a/src/Pages/Incidents/constants.ts +++ b/src/Pages/Incidents/constants.ts @@ -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'; diff --git a/src/Pages/Incidents/index.tsx b/src/Pages/Incidents/index.tsx index b543816..9cc61d1 100644 --- a/src/Pages/Incidents/index.tsx +++ b/src/Pages/Incidents/index.tsx @@ -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: , @@ -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, + ): 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: , + }); + 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 ( @@ -521,222 +600,232 @@ const Incident: FC = () => {
- -
-
-
-
- - UPDATE INCIDENT - - {!isUserParticipantList && ( -
- -
- -
-
-
- )} -
- -
-
-
+ defaultActiveKey="1" + items={[ + { + key: '1', + label: 'Details', + children: ( +
+
+
+
- Severity + UPDATE INCIDENT -
-
-
-
- + - {severityMap && state.severity?.value - ? severityMap[state.severity.value] - : '-'} - +
+ +
+
-
- -
-
-
- {state.isSeverityPickerOpen && ( -
- - handleOpenConfirmationDailog( - selectedOption, - SeverityType, - ) - } - options={updatedSeverities} - selectedValue={state.severity?.value.toString()} - /> -
- )} -
+ )}
-
-
-
- - Status - -
-
-
-
- - {incidentStatusMap && state.status?.value - ? incidentStatusMap[state.status.value] - : '-'} - -
-
- -
-
-
- {state.isStatusPickerOpen && ( -
- - handleOpenConfirmationDailog( - selectedOption, - StatusType, - ) - } - options={updatedStatuses} - selectedValue={state.status?.value.toString()} - /> -
- )} -
-
-
-
-
- - Team - -
-
-
-
- - {teamsMap && state.team?.value - ? teamsMap[state.team.value] - : '-'} - -
+
+
- + + Severity + +
+
+
+
+ + {severityMap && state.severity?.value + ? severityMap[state.severity.value] + : '-'} + +
+
+ +
+
+
+ {state.isSeverityPickerOpen && ( +
+ + handleOpenConfirmationDailog( + selectedOption, + SeverityType, + ) + } + options={updatedSeverities} + selectedValue={state.severity?.value.toString()} + /> +
+ )} +
- -
- {state.isTeamPickerOpen && ( -
+
+ - - handleOpenConfirmationDailog( - selectedOption, - TeamType, - ) - } - options={updatedTeams} - selectedValue={state.team?.value.toString()} - /> + Status + +
+
+
+
+ + {incidentStatusMap && state.status?.value + ? incidentStatusMap[state.status.value] + : '-'} + +
+
+ +
- )} +
+ {state.isStatusPickerOpen && ( +
+ + handleOpenConfirmationDailog( + selectedOption, + StatusType, + ) + } + options={updatedStatuses} + selectedValue={state.status?.value.toString()} + /> +
+ )} +
+
+
+
+
+ + Team + +
+
+
+
+ + {teamsMap && state.team?.value + ? teamsMap[state.team.value] + : '-'} + +
+ +
+ +
+
+ +
+ {state.isTeamPickerOpen && ( +
+ + handleOpenConfirmationDailog( + selectedOption, + TeamType, + ) + } + options={updatedTeams} + selectedValue={state.team?.value.toString()} + /> +
+ )} +
+
+ +
-
- { - - } +
- - -
-
- - + ), + }, + ]} + />
{open && ( { }, { label: 'Open Channel', - startAdornment: , + startAdornment: ( + + ), onClick: handleGoToSlackChannel, }, ]} @@ -761,6 +852,54 @@ const Incident: FC = () => { )} + {state.openDuplicateDialog ? ( + { + markDuplicateIncident(); + }, + }, + ]} + header={`Duplicate incident`} + onClose={handleResetDialog} + > + + Once marked as duplicate, this incident will be archived after 24 + hours. + +
+ } + /> +
+ + +
+ ) : null} {state.openDialog ? ( { { label: 'Go to slack channel', onClick: handleGoToSlackChannel, - startAdornment: , + startAdornment: ( + + ), }, ]} header={`${state.dialogText}`}