diff --git a/src/Pages/Incidents/Dropdowns/index.tsx b/src/Pages/Incidents/Dropdowns/index.tsx index d3e329a..c8b1054 100644 --- a/src/Pages/Incidents/Dropdowns/index.tsx +++ b/src/Pages/Incidents/Dropdowns/index.tsx @@ -3,20 +3,23 @@ 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 { Button, Drawer } from '@navi/web-ui/lib/primitives'; 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 useClickStream from '@src/services/clickStream'; +import { CLICK_STREAM_EVENT_FACTORY } from '@src/services/clickStream/constants/values'; import AllDailogBox from './AllDailogBox'; +import ResolveForm from '../ResolveForm'; import { generateOptions, getCurrentData, @@ -38,10 +41,13 @@ import styles from '../Incidents.module.scss'; const Dropdowns: FC = () => { const reduxDispatch = useDispatch(); - + const { fireEvent } = useClickStream(); + const { EVENT_NAME, SCREEN_NAME } = CLICK_STREAM_EVENT_FACTORY; const incidentData = useSelector( (state: IncidentPageState) => state.incidentLog.incidentData, ); + const currentIncidentId = incidentData?.id; + const slackChannel = incidentData?.slackChannel; const incidentParticipants = useSelector( (state: IncidentPageState) => state.incidentLog.participantsData, ); @@ -182,8 +188,18 @@ const Dropdowns: FC = () => { ? selectedOption[0].value : selectedOption.value; if (currentState !== selectedvalue) { + fireEvent(EVENT_NAME.Houston_Resolve_Incident_initiate, { + screen_name: SCREEN_NAME.INCIDENT_PAGE, + }); if (updateType === StatusType && selectedvalue === RESOLVE_STATUS) { - reduxDispatch(setOpenDialogResolve(true)); + dispatch({ + type: actionTypes.SET_IS_INCIDENT_RESOLVED, + payload: true, + }); + dispatch({ + type: actionTypes.SET_IS_STATUS_PICKER_OPEN, + payload: !state.isStatusPickerOpen, + }); } else if ( updateType === StatusType && selectedvalue === DUPLICATE_STATUS @@ -198,7 +214,12 @@ const Dropdowns: FC = () => { } } }; - + const handleDrawerClose = () => { + dispatch({ + type: actionTypes.SET_IS_INCIDENT_RESOLVED, + payload: false, + }); + }; const handleSevClickOutside = (): void => { dispatch({ type: actionTypes.SET_IS_SEVERITY_PICKER_OPEN, @@ -364,6 +385,28 @@ const Dropdowns: FC = () => { + {state.isIncidentResolved && ( +
+ { + dispatch({ + type: actionTypes.SET_IS_INCIDENT_RESOLVED, + payload: false, + }); + }} + > + + +
+ )} ); }; diff --git a/src/Pages/Incidents/Incidents.module.scss b/src/Pages/Incidents/Incidents.module.scss index 5e938cb..4acb2bb 100644 --- a/src/Pages/Incidents/Incidents.module.scss +++ b/src/Pages/Incidents/Incidents.module.scss @@ -208,3 +208,26 @@ font-size: 13px; font-weight: 400; } +.incident-label { + font-size: 13px; + font-weight: 400; +} +@mixin flex-center { + display: flex; + justify-content: space-between; + align-items: center; +} +.drawer { + width: 444px !important; + height: 100% !important; + > div { + padding: 16px 16px 16px 0px !important; + } +} +.header { + padding: 16px !important; + border-bottom: 1px solid var(--navi-drawer-divider-color); + > span { + @include flex-center; + } +} diff --git a/src/Pages/Incidents/ResolveForm/ResolveForm.module.scss b/src/Pages/Incidents/ResolveForm/ResolveForm.module.scss new file mode 100644 index 0000000..283ab2a --- /dev/null +++ b/src/Pages/Incidents/ResolveForm/ResolveForm.module.scss @@ -0,0 +1,116 @@ +@import 'mixins'; + +.Factor-wrapper, +.jira-wrapper, +.Team-wrapper, +.Tag-wrapper, +.RCA-wrapper { + @include wrapper-styles; +} +.button-wrapper { + @include button-styles; + bottom: 0; + position: fixed; + display: flex; + margin-bottom: 24px; + border-top: 1px solid var(--navi-drawer-divider-color); + padding-top: 20px; + width: 100%; +} +.resolve { + width: 184px; + height: 36px; + margin-left: 24px; +} +.cancel { + width: 184px; + height: 36px; + margin-left: 24px; +} +.textarea { + min-width: 404px !important; + color: var(--navi-color-gray-c2); + + > div > textarea { + box-sizing: border-box; + } +} +.footer-wrapper { + position: relative; + box-sizing: border-box; + width: 100%; +} + +.select-div { + position: relative; + width: 404px; + height: 36px; + justify-content: center; + align-items: center; + border: 1px solid var(--navi-color-gray-border); + border-radius: 8px; +} + +.select-team { + position: relative; + margin-top: 10px; + display: flex; + justify-content: space-between; + padding: 0 10px; + cursor: pointer; +} + +.select-picker { + border-radius: 4px; + position: fixed; +} + +.select-picker-style { + position: absolute; + cursor: pointer; + z-index: 1; +} +.select-picker-item { + min-width: 404px; + margin-top: 5px; +} +.arrow-down { + width: 8px; + height: 8px; + display: flex; + margin-top: 4px; +} +.arrow-dropdown { + width: 8px; + height: 8px; + display: flex; + margin-bottom: 2px; +} +.autocomplete { + height: 36px; + width: 384px; + justify-content: center; + align-items: center; + padding: 0px 10px; + > div { + > div { + input { + display: none; + } + } + } +} + +.form-wrapper { + margin: 16px 4px 0px 20px; +} +.factor-label { + margin-bottom: 8px; +} +.jira { + margin-bottom: 8px; +} +.add-jira-link { + margin-top: 8px; + margin-bottom: 8px; +} diff --git a/src/Pages/Incidents/ResolveForm/constants.ts b/src/Pages/Incidents/ResolveForm/constants.ts new file mode 100644 index 0000000..718499f --- /dev/null +++ b/src/Pages/Incidents/ResolveForm/constants.ts @@ -0,0 +1,79 @@ +import { URL_PREFIX } from '../constants'; + +export const FETCH_RESOLVE_CONFIG = `${URL_PREFIX}/tags/resolve`; +export const RESOLVE_INCIDENTS = `${URL_PREFIX}/incidents/resolve`; + +export interface Team { + tag_value_id: number; + tag_value_name: string; +} +export interface ResolveFormProps { + handleDrawerClose: () => void; + incidentId: number | null; + slackChannel: string; +} +export const initialState = { + team: [], + factors: [], + tags: [], + openFactorPicker: false, + selectedTeam: [], + selectedFactor: [], + selectedTag: [], + rca: '', + jiraLinks: [], +}; + +export const actionTypes = { + SET_TEAM: 'SET_TEAM', + SET_FACTORS: 'SET_FACTORS', + SET_TAGS: 'SET_TAGS', + SET_OPEN_FACTOR_PICKER: 'SET_OPEN_FACTOR_PICKER', + SET_SELECTED_TEAM: 'SET_SELECTED_TEAM', + SET_SELECTED_FACTOR: 'SET_SELECTED_FACTOR', + SET_SELECTED_TAG: 'SET_SELECTED_TAG', + SET_RCA: 'SET_RCA', + RESET_STATE: 'RESET_STATE', + ADD_JIRA_LINK: 'ADD_JIRA_LINK', + UPDATE_JIRA_LINK: 'UPDATE_JIRA_LINK', +}; +export const reducer = (state, action) => { + switch (action.type) { + case actionTypes.SET_TEAM: + return { ...state, team: action.payload }; + case actionTypes.SET_FACTORS: + return { ...state, factors: action.payload }; + case actionTypes.SET_TAGS: + return { ...state, tags: action.payload }; + case actionTypes.SET_OPEN_FACTOR_PICKER: + return { ...state, openFactorPicker: action.payload }; + case actionTypes.SET_SELECTED_TEAM: + return { ...state, selectedTeam: action.payload }; + case actionTypes.SET_SELECTED_FACTOR: + return { ...state, selectedFactor: action.payload }; + case actionTypes.SET_SELECTED_TAG: + return { ...state, selectedTag: action.payload }; + case actionTypes.SET_RCA: + return { ...state, rca: action.payload }; + case actionTypes.RESET_STATE: + return { + ...state, + selectedTeam: [], + selectedFactor: [], + selectedTag: [], + rca: '', + jiraLinks: [''], + openFactorPicker: false, + }; + case actionTypes.ADD_JIRA_LINK: + return { ...state, jiraLinks: [...state.jiraLinks, ''] }; + case actionTypes.UPDATE_JIRA_LINK: { + const { index, value } = action.payload; + const updatedLinks = [...state.jiraLinks]; + updatedLinks[index] = value; + return { ...state, jiraLinks: updatedLinks }; + } + default: + return state; + } +}; diff --git a/src/Pages/Incidents/ResolveForm/index.tsx b/src/Pages/Incidents/ResolveForm/index.tsx new file mode 100644 index 0000000..68d0bed --- /dev/null +++ b/src/Pages/Incidents/ResolveForm/index.tsx @@ -0,0 +1,325 @@ +import React, { useEffect, MutableRefObject } from 'react'; +import { + BorderedInput, + Button, + TextArea, + Typography, +} from '@navi/web-ui/lib/primitives'; +import { toast } from '@navi/web-ui/lib/primitives/Toast'; +import { AddIcon, ArrowDownSolidIcon } from '@navi/web-ui/lib/icons'; +import { AutoComplete, SelectPicker } from '@navi/web-ui/lib/components'; +import { SelectPickerOptionProps } from '@navi/web-ui/lib/components/SelectPicker/types'; +import ErrorBoundary from '@src/components/ErrorBoundary/ErrorBoundary'; +import useOutsideClick from '@src/hooks/useOutsideClick'; +import useClickStream from '@src/services/clickStream'; +import { CLICK_STREAM_EVENT_FACTORY } from '@src/services/clickStream/constants/values'; +import { ApiService } from '@src/services/api'; +import LoadingIcon from '@src/assets/LoadingIcon'; +import { ResolveFormProps } from './constants'; +import { + reducer, + actionTypes, + initialState, + FETCH_RESOLVE_CONFIG, + RESOLVE_INCIDENTS, +} from './constants'; +import styles from './ResolveForm.module.scss'; +import useIncidentApis from '../useIncidentApis'; + +const ResolveForm: React.FC = ({ + handleDrawerClose, + incidentId, +}) => { + const [state, dispatch] = React.useReducer(reducer, initialState); + const { startIncidentSearch } = useIncidentApis(); + const { fireEvent } = useClickStream(); + const { EVENT_NAME, SCREEN_NAME } = CLICK_STREAM_EVENT_FACTORY; + const getData = () => { + ApiService.get(FETCH_RESOLVE_CONFIG) + .then(response => { + const apiResponse = response?.data?.data; + if (!apiResponse) { + toast.error('Something went wrong. Please try again later'); + return; + } + dispatch({ + type: actionTypes.SET_TEAM, + payload: apiResponse.tags[0].tag_values, + }); + dispatch({ + type: actionTypes.SET_FACTORS, + payload: apiResponse.tags[1].tag_values, + }); + dispatch({ + type: actionTypes.SET_TAGS, + payload: apiResponse.tags[2].tag_values, + }); + }) + .catch(error => { + const toastMessage = error?.response?.data?.error?.message + ? `${error?.response?.data?.error?.message},` + : ''; + toast.error(toastMessage); + }); + }; + const business_options = state.team.map(team => ({ + label: team.tag_value_name, + value: team.tag_value_id, + })); + const factor_options = state.factors.map(factor => ({ + label: factor.tag_value_name, + value: factor.tag_value_id, + })); + const tag_options = state.tags.map(tag => ({ + label: tag.tag_value_name, + value: tag.tag_value_id, + })); + const factor_ref = useOutsideClick({ + callback: () => { + dispatch({ type: actionTypes.SET_OPEN_FACTOR_PICKER, payload: false }); + }, + }) as MutableRefObject; + + const handleAddJiraLink = (): void => { + dispatch({ type: actionTypes.ADD_JIRA_LINK }); + }; + + const handleJiraLinkChange = (index, value): void => { + dispatch({ type: actionTypes.UPDATE_JIRA_LINK, payload: { index, value } }); + }; + + const handleFactorClick = (): void => { + dispatch({ + type: actionTypes.SET_OPEN_FACTOR_PICKER, + payload: !state.openFactorPicker, + }); + }; + const handleTeamChange = ( + val: SelectPickerOptionProps | SelectPickerOptionProps[], + ): void => { + dispatch({ type: actionTypes.SET_SELECTED_TEAM, payload: val }); + }; + + const handleFactorChange = ( + val: SelectPickerOptionProps | SelectPickerOptionProps[], + ): void => { + dispatch({ type: actionTypes.SET_SELECTED_FACTOR, payload: val }); + dispatch({ type: actionTypes.SET_OPEN_FACTOR_PICKER, payload: false }); + }; + const handleTagsChange = ( + val: SelectPickerOptionProps | SelectPickerOptionProps[], + ): void => { + dispatch({ type: actionTypes.SET_SELECTED_TAG, payload: val }); + }; + const handleRCAchange = (e: React.ChangeEvent): void => { + dispatch({ type: actionTypes.SET_RCA, payload: e.target.value }); + }; + const clearTags = (): void => { + dispatch({ type: actionTypes.SET_SELECTED_TAG, payload: [] }); + }; + const clearTeam = (): void => { + dispatch({ type: actionTypes.SET_SELECTED_TEAM, payload: [] }); + }; + + const isDisabled = () => { + return ( + state.selectedTeam.length === 0 || + state.selectedFactor.length === 0 || + !(state.rca && state.rca.trim()) + ); + }; + + const buildRequestPayload = () => { + const { selectedTeam, selectedFactor, selectedTag, rca, jiraLinks } = state; + const validJiraLinks = jiraLinks + .map(link => link.trim()) + .filter(link => link !== ''); + + return { + incident_id: incidentId, + business_affected: selectedTeam?.map(item => item?.value) || [], + contributing_factors: selectedFactor?.value, + additional_tags: selectedTag?.map(item => item?.value) || [], + rca_summary: rca.trim() || '', + jira_links: validJiraLinks || [], + }; + }; + + const handleResolveIncident = () => { + fireEvent(EVENT_NAME.Houston_Resolve_Incident_submit, { + screen_name: SCREEN_NAME.INCIDENT_PAGE, + }); + const endPoint = RESOLVE_INCIDENTS; + const requestPayload = buildRequestPayload(); + toast('Updating ticket. Please wait a moment.', { + icon: , + }); + ApiService.post(endPoint, requestPayload) + .then(response => { + if (response?.status === 200) { + const toastMessage = response?.data?.message + ? `${response?.data?.message}` + : 'Incident resolved successfully'; + toast.success(toastMessage); + handleDrawerClose(); + startIncidentSearch(incidentId?.toString() || ''); + } + }) + .catch(error => { + const toastMessage = `${ + error?.response?.data?.error?.message + ? `${error?.response?.data?.error?.message}` + : 'Something went wrong. Please try again.' + }`; + toast.error(toastMessage); + startIncidentSearch(incidentId?.toString() || ''); + }); + }; + + useEffect(() => { + getData(); + }, []); + return ( + +
+
+
+ Business affected +
+ item?.value)} + updateOnChipRemoval={handleTeamChange} + updateClearAllCallback={clearTeam} + variant="bordered" + placeholder="" + persistMenuOnSelect + containerClassName={styles['autocomplete']} + dropdownIconContainerClass={styles['arrow-dropdown']} + enableSelectedOptionSorting + /> +
+
+
+ + Contributing factor + +
+
+ + {state.selectedFactor.length === 0 + ? '' + : ` ${state.selectedFactor.label}`} + + +
+
+
+ {state.openFactorPicker && ( +
+ +
+ )} +
+
+
+ Additional tags (optional) +
+ item?.value)} + updateOnChipRemoval={handleTagsChange} + variant="bordered" + persistMenuOnSelect + containerClassName={styles['autocomplete']} + dropdownIconContainerClass={styles['arrow-dropdown']} + autoCompletePickerWrapper={styles['autocomplete-picker']} + enableSelectedOptionSorting + /> +
+
+ +
+ RCA +