Merge branch 'master' into INFRA-2836

This commit is contained in:
Saurabh Bhagwan Sathe
2024-02-12 12:42:12 +05:30
committed by GitHub
10 changed files with 606 additions and 5 deletions

View File

@@ -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 = () => {
</div>
</div>
<AllDailogBox />
{state.isIncidentResolved && (
<div className={styles['incident-resolved']}>
<Drawer
open={state.isIncidentResolved}
headerText="Resolve incident"
headerContainerClasses={styles.header}
className={styles.drawer}
onClose={() => {
dispatch({
type: actionTypes.SET_IS_INCIDENT_RESOLVED,
payload: false,
});
}}
>
<ResolveForm
handleDrawerClose={handleDrawerClose}
incidentId={currentIncidentId}
slackChannel={slackChannel}
/>
</Drawer>
</div>
)}
</div>
);
};

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
};

View File

@@ -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<ResolveFormProps> = ({
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<HTMLDivElement>;
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<HTMLTextAreaElement>): 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: <LoadingIcon />,
});
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 (
<ErrorBoundary>
<div>
<div className={styles['form-wrapper']}>
<div className={styles['Team-wrapper']}>
<Typography variant="p3">Business affected</Typography>
<div>
<AutoComplete
limitChipThreshold={2}
options={business_options}
updateSelectedOptions={handleTeamChange}
selectedOptions={state.selectedTeam?.map(item => item?.value)}
updateOnChipRemoval={handleTeamChange}
updateClearAllCallback={clearTeam}
variant="bordered"
placeholder=""
persistMenuOnSelect
containerClassName={styles['autocomplete']}
dropdownIconContainerClass={styles['arrow-dropdown']}
enableSelectedOptionSorting
/>
</div>
</div>
<div className={styles['Factor-wrapper']} ref={factor_ref}>
<Typography variant="p3" className={styles['factor-label']}>
Contributing factor
</Typography>
<div className={styles['select-div']} onClick={handleFactorClick}>
<div className={styles['select-team']}>
<Typography
variant="p3"
color={
state.selectedFactor.length === 0
? 'var(--navi-color-gray-c2)'
: 'var(--navi-color-gray-c1)'
}
>
{state.selectedFactor.length === 0
? ''
: ` ${state.selectedFactor.label}`}
</Typography>
<ArrowDownSolidIcon
color="var(--navi-color-gray-c3)"
className={styles['arrow-down']}
/>
</div>
</div>
<div className={styles['select-picker-style']}>
{state.openFactorPicker && (
<div className={styles['select-picker']}>
<SelectPicker
options={factor_options}
onSelectionChange={handleFactorChange}
multiSelect={false}
showSearchBar={false}
wrapperClasses={styles['select-picker-item']}
/>
</div>
)}
</div>
</div>
<div className={styles['Tag-wrapper']}>
<Typography variant="p3"> Additional tags (optional)</Typography>
<div>
<AutoComplete
limitChipThreshold={1}
options={tag_options}
updateClearAllCallback={clearTags}
updateSelectedOptions={handleTagsChange}
selectedOptions={state.selectedTag?.map(item => item?.value)}
updateOnChipRemoval={handleTagsChange}
variant="bordered"
persistMenuOnSelect
containerClassName={styles['autocomplete']}
dropdownIconContainerClass={styles['arrow-dropdown']}
autoCompletePickerWrapper={styles['autocomplete-picker']}
enableSelectedOptionSorting
/>
</div>
</div>
<div className={styles['RCA-wrapper']}>
<Typography variant="p3">RCA </Typography>
<TextArea
name="title"
placeholder="Enter RCA"
containerClassName={styles.textarea}
rows={3}
onChange={handleRCAchange}
value={state.rca}
size="medium"
/>
</div>
<div className={styles['jira-wrapper']}>
<Typography variant="p3" className={styles['jira']}>
Jira tickets (optional)
</Typography>
{state.jiraLinks?.map((link, index) => (
<BorderedInput
key={index}
placeholder="Enter Jira ticket"
value={link}
containerClassName={styles.textarea}
onChange={e => handleJiraLinkChange(index, e.target.value)}
hintMsg="Please enter jira link"
/>
))}
<Button
variant="text"
onClick={handleAddJiraLink}
startAdornment={<AddIcon color="var(--navi-color-blue-base)" />}
className={styles['add-jira-link']}
>
Add Jira link
</Button>
</div>
</div>
<div className={styles['footer-wrapper']}>
<div className={styles['button-wrapper']}>
<Button
variant="secondary"
className={styles['cancel']}
fullWidth
onClick={handleDrawerClose}
>
Cancel
</Button>
<Button
variant="primary"
fullWidth
className={styles['resolve']}
onClick={handleResolveIncident}
disabled={isDisabled()}
>
Resolve incident
</Button>
</div>
</div>
</div>
</ErrorBoundary>
);
};
export default ResolveForm;

View File

@@ -0,0 +1,8 @@
@mixin wrapper-styles {
align-items: center;
margin-bottom: 24px;
}
@mixin button-styles {
cursor: pointer;
height: 36px;
}

View File

@@ -61,6 +61,7 @@ export const actionTypes = {
SET_INCIDENT_NAME: 'SET_INCIDENT_NAME',
SET_ERROR_MSG: 'SET_ERROR_MSG',
RESET_DUPLICATE_DIALOG: 'RESET_DUPLICATE_DIALOG',
SET_IS_INCIDENT_RESOLVED: 'SET_IS_INCIDENT_RESOLVED',
};
export const reducer = (state, action) => {
@@ -114,6 +115,8 @@ export const reducer = (state, action) => {
incidentName: '',
errorMsg: '',
};
case actionTypes.SET_IS_INCIDENT_RESOLVED:
return { ...state, isIncidentResolved: action.payload };
default:
return state;
}
@@ -141,6 +144,7 @@ export const initialState = {
incidentName: '',
openDuplicateDialog: false,
errorMsg: '',
isIncidentResolved: false,
};
export const RESOLVE_STATUS = '4';

View File

@@ -27,7 +27,6 @@ import {
const useIncidentApis = (): useIncidentApiProps => {
const dispatch = useDispatch();
const handleApiError = error => {
const toastMessage = `${
error?.response?.data?.error?.message
@@ -102,6 +101,7 @@ const useIncidentApis = (): useIncidentApiProps => {
if (response?.status === 200) {
const toastMessage = `This incident is marked as duplicate of _houston-${duplicateOfId}`;
toast.success(toastMessage);
dispatch(setOpenDialogDuplicate(false));
startIncidentSearch(incidentId);
dispatch(setOpenDialogDuplicate(false));
}

View File

@@ -149,6 +149,7 @@ const SearchResultsTable: FC<SearchResultTableProps> = ({
onPageChange={handlePageNumberChange}
onPageSizeChange={handlePageSizeChange}
containerClasses={styles['search-list-table-pagination']}
pageSizeOptions={[10, 20, 50]}
/>
}
columnDefs={columnData}

View File

@@ -22,6 +22,8 @@ const EVENT_NAME = {
Houston_Create_Team_Initiate: 'Houston_Create_Team_Initiate',
Houston_Create_Incident_initiate: 'Houston_Create_Incident_initiate',
Houston_Create_Incident_submit: 'Houston_Create_Incident_submit',
Houston_Resolve_Incident_initiate: 'Houston_Resolve_Incident_initiate',
Houston_Resolve_Incident_submit: 'Houston_Resolve_Incident_submit',
};
const SCREEN_NAME = {