diff --git a/package.json b/package.json index d3db39b..68826f2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Pages/Incidents/ActivityLog/index.tsx b/src/Pages/Incidents/ActivityLog/index.tsx index 3d0cd48..8df17db 100644 --- a/src/Pages/Incidents/ActivityLog/index.tsx +++ b/src/Pages/Incidents/ActivityLog/index.tsx @@ -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; - 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 = ({ incidentLog, totalLog }) => { return (
{incidentLog?.data?.logs && incidentLog.data.logs.length > 0 && ( diff --git a/src/Pages/Incidents/Content/Content.module.scss b/src/Pages/Incidents/Content/Content.module.scss deleted file mode 100644 index e0c1681..0000000 --- a/src/Pages/Incidents/Content/Content.module.scss +++ /dev/null @@ -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; -} diff --git a/src/Pages/Incidents/Content/index.tsx b/src/Pages/Incidents/Content/index.tsx deleted file mode 100644 index 3293785..0000000 --- a/src/Pages/Incidents/Content/index.tsx +++ /dev/null @@ -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(); - - const [isLoading, setIsLoading] = useState(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 ( -
- {IncidentConstants.incidentSummary} -
-
- - {IncidentConstants.title}:{' '} - {incidentData?.title} - -
-
- {IncidentConstants.description}: - {incidentData?.description} -
- {IncidentConstants.status}: -
- -
- -
- {IncidentConstants.team}: - {isLoading ? ( -
- -
- ) : ( - data?.map(participant => ( -
-
- {' '} - {participant?.real_name} -
-
- )) - )} -
-
- ); -}; - -export default Content; diff --git a/src/Pages/Incidents/DescriptionContent/index.tsx b/src/Pages/Incidents/DescriptionContent/index.tsx index a4c35ec..9374ffb 100644 --- a/src/Pages/Incidents/DescriptionContent/index.tsx +++ b/src/Pages/Incidents/DescriptionContent/index.tsx @@ -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 = ({ - 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): 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, - ): 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: , - }); - 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: , - }); - 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 => ( -
-
-    - {participant?.name} -
-
- )) - ) : ( -
-
- ); - }; - - 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 => (
   @@ -266,123 +83,15 @@ const DescriptionContent: React.FC = ({ rel="noreferrer" className={styles['rca-text']} > - + Go to document
)} -
-
- -   - - JIRA ticket(s)      - -
-
-
-
- - {state.jiraLinks?.filter(link => link !== '').length || 0}{' '} - linked tickets   - - {state.jiraLinks?.filter(link => link !== '').length > 0 && ( - <> - {state.showLinkedTickets && ( - - )} - {!state.showLinkedTickets && ( - - )} - - )} -
-
-
- -   - - Link JIRA ticket - -
-
-
- {state.showLinkedTickets && ( -
- {state.jiraLinks - .filter(jiraLink => jiraLink !== '') - ?.map((jiraLink, index) => ( -
- -    -
- handleCopyClick(jiraLink)} /> -
-
- handleDeleteIconClick(jiraLink)} - width={20} - height={20} - color="var(--navi-color-gray-c3)" - /> -
-
- ))} -
- )} -
- {state.showInput && ( -
-
- -
-
- -
-
- )} -
-
+
@@ -392,14 +101,14 @@ const DescriptionContent: React.FC = ({ Team - {returnParticipants()} + {renderParticipantList('participants')}
Others - {returnOthers()} + {renderParticipantList('others')}
diff --git a/src/Pages/Incidents/DrawerMode/index.tsx b/src/Pages/Incidents/DrawerMode/index.tsx deleted file mode 100644 index 76f47bd..0000000 --- a/src/Pages/Incidents/DrawerMode/index.tsx +++ /dev/null @@ -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 = ({ incidentId, slackChannel }) => { - const [severity, setSeverity] = useState(); - const [status, setStatus] = useState(); - const [team, setTeam] = useState(); - const [headerData, setHeaderData] = useState({}); - const [incidentDetails, setIncidentDetails] = useState({}); - const [incidentParticipants, setIncidentParticipants] = useState([]); - const [isLoading, setIsLoading] = useState(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 ( -
- Update Tags: -
- Severity: - { - setSeverity(val); - updateIncident({ - id: incidentId, - severityId: !Array.isArray(val) - ? `${val?.value}` - : val?.[0]?.value, - }); - }} - isSingleSelect - filterClass={styles['filter-wrapper']} - /> -
-
- Status: - { - setStatus(val); - updateIncident({ - id: incidentId, - status: !Array.isArray(val) ? `${val?.value}` : val?.[0]?.value, - }); - }} - isSingleSelect - filterClass={styles['filter-wrapper']} - /> -
-
- Team: - { - setTeam(val); - updateIncident({ - id: incidentId, - teamId: !Array.isArray(val) ? `${val?.value}` : val?.[0]?.value, - }); - }} - isSingleSelect - filterClass={styles['filter-wrapper']} - /> -
-
- ); - }; - - const returnContent = () => { - return ( -
-
- {IncidentConstants.title}: - {incidentDetails?.title} -
-
- {IncidentConstants.description}: - - {incidentDetails?.description || '-'} - -
-
- {IncidentConstants.channel}: - - - - {incidentDetails?.incidentName || '-'} - - -
-
- {IncidentConstants.incidentId}: - - {incidentDetails?.id} - -
-
- ); - }; - - const returnParticipants = () => { - return incidentParticipants?.length ? ( - incidentParticipants?.map(participant => ( -
-
- {' '} - {participant?.name} -
-
- )) - ) : ( -
-
- ); - }; - - if (isLoading) { - return ( -
- -
- ); - } - - return ( -
- {returnContent()} -
- {returnHeaderDetails()} -
- Participants: - {returnParticipants()} -
- ); -}; - -export default DrawerMode; diff --git a/src/Pages/Incidents/Dropdowns/AllDailogBox.tsx b/src/Pages/Incidents/Dropdowns/AllDailogBox.tsx new file mode 100644 index 0000000..faf6669 --- /dev/null +++ b/src/Pages/Incidents/Dropdowns/AllDailogBox.tsx @@ -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, + ): 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: , + }); + 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: , + }); + 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: , + }); + 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 => ( +
+ {openNotParticipants && ( + + ), + onClick: handleGoToSlackChannel, + }, + ]} + header="You are not authorised to update this incident" + onClose={handleopenNotParticipants} + > + + You must be a participant of this incident to update it. Please join + the Slack channel to be added a participant + + + )} +
+ ); + + const renderDuplicateDialog = (): JSX.Element => ( +
+ {openDuplicate ? ( + { + markDuplicateIncident(incidentId, duplicateOfId); + }, + }, + ]} + header={`Duplicate incident`} + onClose={handleResetDialog} + > + + Once marked as duplicate, this incident will be archived after 24 + hours. + +
+ } + /> +
+ +
+ ) : null} +
+ ); + + const renderResolveDialog = (): JSX.Element => ( +
+ {openResolve ? ( + { + reduxDispatch(setOpenDialogResolve(false)); + }, + }, + { + label: 'Go to slack channel', + onClick: handleGoToSlackChannel, + startAdornment: ( + + ), + }, + ]} + header={`Resolve incident`} + onClose={() => reduxDispatch(setOpenDialogResolve(false))} + > + + We’re working on improving this feature. For the time being please + mark this incident as resolved on Slack. + + + ) : null} +
+ ); + + const renderUpdateDialog = (): JSX.Element => ( +
+ {openUpdate ? ( + { + 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))} + > +
+ + You are updating this incident‘s   + {UpdateData?.type || '..'} +   from + + {UpdateData?.from || '..'} + + to  + + {getLabelFromOption(UpdateData?.to) || '..'} + + +
+
+ ) : null} +
+ ); + + return ( +
+ {renderNotAuthorizedDialog()} + {renderDuplicateDialog()} + {renderResolveDialog()} + {renderUpdateDialog()} +
+ ); +}; +export default AllDailogBox; diff --git a/src/Pages/Incidents/Dropdowns/index.tsx b/src/Pages/Incidents/Dropdowns/index.tsx new file mode 100644 index 0000000..d3e329a --- /dev/null +++ b/src/Pages/Incidents/Dropdowns/index.tsx @@ -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; + + const refSeverity = useOutsideClick({ + callback: handleSevClickOutside, + }) as MutableRefObject; + + const refTeam = useOutsideClick({ + callback: handleTeamClickOutside, + }) as MutableRefObject; + + return ( +
+
+
+
+ + Severity + +
+
+
+
+ + {currentSev} + +
+
+ +
+
+
+ {state.isSeverityPickerOpen && ( +
+ + handleOpenConfirmationDailog(selectedOption, SeverityType) + } + options={updatedSeverities} + selectedValue={incidentData?.severityId?.toString()} + /> +
+ )} +
+
+
+
+
+ + Status + +
+
+
+
+ + {currentStatus} + +
+
+ +
+
+
+ {state.isStatusPickerOpen && ( +
+ + handleOpenConfirmationDailog(selectedOption, StatusType) + } + options={updatedStatuses} + selectedValue={incidentData?.status?.toString()} + /> +
+ )} +
+
+
+
+
+ + Team + +
+
+
+
+ + {currentTeam} + +
+ +
+ +
+
+
+ {state.isTeamPickerOpen && ( +
+ + handleOpenConfirmationDailog(selectedOption, TeamType) + } + options={updatedTeams} + selectedValue={incidentData?.teamId?.toString()} + /> +
+ )} +
+
+
+
+ +
+ ); +}; +export default Dropdowns; diff --git a/src/Pages/Incidents/Header/index.tsx b/src/Pages/Incidents/Header/index.tsx index 2b0b146..fc386a8 100644 --- a/src/Pages/Incidents/Header/index.tsx +++ b/src/Pages/Incidents/Header/index.tsx @@ -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 = ({ incidentName, title }) => { +const Header: FC = () => { const navigate = useNavigate(); const handleBacktoDashboard = (): void => { - navigate('/'); + navigate(-1); }; + const incidentData = useSelector( + (state: IncidentPageState) => state.incidentLog.incidentData, + ); + return (
- - ) : null} - {state.openDialog ? ( - { - dispatch({ - type: actionTypes.SET_OPEN_DIALOG, - payload: false, - }); - }, - }, - { - label: 'Go to slack channel', - onClick: handleGoToSlackChannel, - startAdornment: ( - - ), - }, - ]} - header={`${state.dialogText}`} - onClose={() => - dispatch({ type: actionTypes.SET_OPEN_DIALOG, payload: false }) - } - > - - We’re working on improving this feature. For the time being please - mark this incident as {`${state.dialogBodyText}`} on Slack. - - - ) : null} -
- {state.openConfirmationDialog ? ( - { - dispatch({ - type: actionTypes.SET_OPEN_CONFIRMATION_DIALOG, - payload: false, - }); - }, - }, - { - label: 'Update incident', - onClick: () => { - dispatch({ - type: actionTypes.SET_OPEN_CONFIRMATION_DIALOG, - payload: false, - }); - handleCloseConfirmationDialog(); - }, - }, - ]} - header={`Are you sure you want to update the incident?`} - onClose={() => - dispatch({ - type: actionTypes.SET_OPEN_CONFIRMATION_DIALOG, - payload: false, - }) - } - > -
- - You are updating this incident‘s - {getUpdateTypeText(state.updateType)} - from - - {getUpdateValueText()} - - to  - - {state.selectedOption?.label || '..'} - - -
-
- ) : null} -
); diff --git a/src/Pages/Incidents/types.ts b/src/Pages/Incidents/types.ts new file mode 100644 index 0000000..fc41861 --- /dev/null +++ b/src/Pages/Incidents/types.ts @@ -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; +} + +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; +} diff --git a/src/Pages/Incidents/useIncidentApis.tsx b/src/Pages/Incidents/useIncidentApis.tsx new file mode 100644 index 0000000..d501b3c --- /dev/null +++ b/src/Pages/Incidents/useIncidentApis.tsx @@ -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: , + }); + 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; diff --git a/src/Pages/Incidents/utils.ts b/src/Pages/Incidents/utils.ts index 6eeef28..c748828 100644 --- a/src/Pages/Incidents/utils.ts +++ b/src/Pages/Incidents/utils.ts @@ -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] + : '-' + } `; + } +}; diff --git a/src/Pages/Incidents/DrawerMode/DrawerMode.module.scss b/src/Pages/Team/partials/TeamForm.module.scss similarity index 100% rename from src/Pages/Incidents/DrawerMode/DrawerMode.module.scss rename to src/Pages/Team/partials/TeamForm.module.scss diff --git a/src/Pages/Team/partials/TeamForm.tsx b/src/Pages/Team/partials/TeamForm.tsx index e2c0d1f..91c673c 100644 --- a/src/Pages/Team/partials/TeamForm.tsx +++ b/src/Pages/Team/partials/TeamForm.tsx @@ -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'; diff --git a/src/Pages/Team/partials/TeamResultsTable.tsx b/src/Pages/Team/partials/TeamResultsTable.tsx index 54458c0..a4f1fed 100644 --- a/src/Pages/Team/partials/TeamResultsTable.tsx +++ b/src/Pages/Team/partials/TeamResultsTable.tsx @@ -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'; diff --git a/src/components/MultiSelectPickerWithSearch/index.tsx b/src/components/MultiSelectPickerWithSearch/index.tsx index dcb01ed..062c16b 100644 --- a/src/components/MultiSelectPickerWithSearch/index.tsx +++ b/src/components/MultiSelectPickerWithSearch/index.tsx @@ -24,7 +24,6 @@ const MultiSelectPickerWithSearch: FC = ({ multiSelect, placeholder, debounceDelay, - inputSize = 'full-width', pickerWrapperClassName, clearAll = false, optionGroupKey = '', @@ -172,7 +171,7 @@ const MultiSelectPickerWithSearch: FC = ({ return options.length > searchEnableThreshold ? (
, + ): void => { + state.incidentLogData = action.payload; + }, + setIncidentData: (state, action: PayloadAction): void => { + state.incidentData = action.payload; + }, + setHeaderData: (state, action: PayloadAction): void => { + state.headerData = action.payload; + }, + setParticipantsData: ( + state, + action: PayloadAction, + ): void => { + state.participantsData = action.payload; + }, + setOpenDialogUpdate: (state, action: PayloadAction): void => { + state.openDialogUpdate = action.payload; + }, + setOpenDialogDuplicate: (state, action: PayloadAction): void => { + state.openDialogDuplicate = action.payload; + }, + setOpenDialogResolve: (state, action: PayloadAction): void => { + state.openDialogResolve = action.payload; + }, + setOpenDialognotParticipants: ( + state, + action: PayloadAction, + ): void => { + state.openDialognotParticipants = action.payload; + }, + setUpdateDetails: ( + state, + action: PayloadAction, + ): 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; diff --git a/src/store/index.tsx b/src/store/index.tsx index f226248..787c7b3 100644 --- a/src/store/index.tsx +++ b/src/store/index.tsx @@ -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, }, }); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index d91b444..8137953 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -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; +}