TP-49551 | Create Incident from UI (#101)

* TP-49551 | added functionalities for creating incident

* TP-49551 | styling added

* TP-49551 | added selectpicker and changed endpoint

* TP-49551 | added input validations , selectpicker position

* TP-49551 | added loader and minor css changes

* TP-49551 | title and desc placeholder correction

* TP-49551 | added clickstream and some design review changes

* TP-49551 | design review and pr comments changes

* TP-49551 | PR comments changes

* TP-49551 | pr reviews'2

* TP-49551 | implemented container pattern

* TP-49551 | add PR template

* TP-49551 | added PR template

* TP-49551 | updated imports
This commit is contained in:
Pooja Jaiswal
2023-12-01 15:05:45 +05:30
committed by GitHub
parent dc236b16a5
commit 86c29600e9
12 changed files with 639 additions and 7 deletions

18
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,18 @@
**PR Checklist to be followed:**
**Checklist:**
- [ ] Imports to be ordered : React library =>Other libraries => Other Folders => Current Folder
- [ ] Commented code should be removed(Write TODO if required)
- [ ] Aliasing of imports is preferred
- [ ] Dont hard code URL
- [ ] No camel casing in css, use hyphens (example: use-like-this)
- [ ] Maintain consistency while writing dimensions
- [ ] Avoid console.log
- [ ] Use global utils if its common for many pages, if not maintain in the folder level
- [ ] DateFormat should be followed same
- [ ] Avoid inline styling
- [ ] Proper TS convention ( dont use any, instead define interface and use it)
- [ ] Peer review
- [ ] Font-family / Basic css shouldnt be overridden
- [ ] File changes shouldnt be more than 20-30, if its more than 25 please create a draft PR and share it so it can reviewed properly.

View File

@@ -7,3 +7,15 @@
.dashboard-wrapper {
padding: 24px;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}

View File

@@ -14,6 +14,8 @@ export const FETCH_INCIDENTS_DATA = (payload: string): string => {
export const FETCH_FILTER_CONFIG = `${URL_PREFIX}/filters`;
export const CREATE_INCIDENT = createURL('/houston/create-incident-v2');
export const TimeSpan = [
{
label: 'Last 24 hours',
@@ -55,3 +57,68 @@ export const getSeverityColor = (value: string) => {
break;
}
};
export const LIMITS = {
title: { max: 100 },
description: { max: 500 },
};
export const actionTypes = {
SET_DRAWER_OPEN: 'SET_DRAWER_OPEN',
SET_TEAMS: 'SET_TEAMS',
SET_TITLE: 'SET_TITLE',
SET_SEVERITY: 'SET_SEVERITY',
SET_SELECTED_SEVERITY: 'SET_SELECTED_SEVERITY',
SET_DESCRIPTION: 'SET_DESCRIPTION',
SET_SELECTED_TEAM: 'SET_SELECTED_TEAM',
SET_OPEN_SELECT: 'SET_OPEN_SELECT',
SET_IS_TITLE_VALID: 'SET_IS_TITLE_VALID',
SET_IS_DESCRIPTION_VALID: 'SET_IS_DESCRIPTION_VALID',
CLEAR_DRAWER: 'CLEAR_DRAWER',
};
export const initialState = {
open: false,
teams: [],
title: '',
severity: [],
selectedSeverity: [],
description: '',
selectedTeam: [],
openSelect: false,
isTitleValid: true,
isDescriptionValid: true,
};
export const reducer = (state, action) => {
switch (action.type) {
case actionTypes.SET_TEAMS:
return { ...state, teams: action.payload };
case actionTypes.SET_TITLE:
return { ...state, title: action.payload };
case actionTypes.SET_SEVERITY:
return { ...state, severity: action.payload };
case actionTypes.SET_SELECTED_SEVERITY:
return { ...state, selectedSeverity: action.payload };
case actionTypes.SET_DESCRIPTION:
return { ...state, description: action.payload };
case actionTypes.SET_SELECTED_TEAM:
return { ...state, selectedTeam: action.payload };
case actionTypes.SET_OPEN_SELECT:
return { ...state, openSelect: action.payload };
case actionTypes.SET_IS_TITLE_VALID:
return { ...state, isTitleValid: action.payload };
case actionTypes.SET_IS_DESCRIPTION_VALID:
return { ...state, isDescriptionValid: action.payload };
case actionTypes.SET_DRAWER_OPEN:
return { ...state, open: action.payload };
case actionTypes.CLEAR_DRAWER:
return {
...state,
title: '',
selectedSeverity: null,
selectedTeam: [],
description: '',
openSelect: false,
};
default:
return state;
}
};

View File

@@ -122,7 +122,11 @@ const Dashboard: FC = () => {
return (
<div className={styles['dashboard-wrapper']}>
<DashboardHeader fetchIncidentData={fetchIncidentData} />
<DashboardHeader
fetchIncidentData={fetchIncidentData}
startIncidentSearch={startIncidentSearch}
/>
{returnTable()}
</div>
);

View File

@@ -0,0 +1,130 @@
@mixin flex-center {
display: flex;
justify-content: space-between;
align-items: center;
}
@mixin wrapper-styles {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
@mixin button-styles {
cursor: pointer;
height: 36px;
}
.name-wrapper,
.Desc-name,
.incident-name {
color: var(--navi-color-gray-c2);
}
.name-wrapper {
margin-bottom: 4px;
}
.Desc-name {
margin-bottom: 160px;
}
.incident-name {
margin-bottom: 65px;
}
.drawer {
width: 550px !important;
}
.header {
padding: 16px !important;
> span {
@include flex-center;
}
}
.textarea {
min-width: 350px !important;
color: var(--navi-color-gray-c2);
> div > textarea {
box-sizing: border-box;
}
}
.create-incident {
background: var(--navi-color-blue-base);
@include button-styles;
width: 153px;
margin: 20px 0 0 40px;
}
.create,
.cancel {
@include button-styles;
width: 231px;
}
.footer-wrapper {
@include flex-center;
margin-top: 380px;
}
.incident-wrapper,
.Description-wrapper,
.severity-picker,
.Team-wrapper {
@include wrapper-styles;
}
.severity-wrapper {
width: calc(100% - 156px);
display: flex;
justify-content: flex-start;
align-items: center;
gap: 0 16px;
padding: 0 8px;
box-sizing: border-box;
}
.team-div {
width: 352px;
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;
margin-top: 5px;
}
.select-picker-style {
position: absolute;
margin-top: 25px;
margin-left: 152px;
cursor: pointer;
z-index: 1;
}
.select-picker-item {
min-width: 352px;
margin-top: 2px;
margin-left: 14px;
}
.arrow-down {
display: inline;
width: 8px;
height: 8px;
margin-top: 4px;
}

View File

@@ -0,0 +1,67 @@
import { useReducer } from 'react';
import { Button, Drawer } from '@navi/web-ui/lib/primitives';
import { AddIcon } from '@navi/web-ui/lib/icons';
import ErrorBoundary from '@src/components/ErrorBoundary/ErrorBoundary';
import useClickStream from '@src/services/clickStream';
import { CLICK_STREAM_EVENT_FACTORY } from '@src/services/clickStream/constants/values';
import {
actionTypes,
initialState,
reducer,
} from '@src/Pages/Dashboard/constants';
import CreateIncidentForm from '@src/Pages/Dashboard/partials/CreateIncidentForm';
import styles from './CreateIncident.module.scss';
interface CreateIncidentProps {
startIncidentSearch: () => void;
}
const CreateIncident: React.FC<CreateIncidentProps> = ({
startIncidentSearch,
}) => {
const [state, dispatch] = useReducer(reducer, initialState);
const { fireEvent } = useClickStream();
const { EVENT_NAME, SCREEN_NAME } = CLICK_STREAM_EVENT_FACTORY;
const handleDrawerOpen = () => {
fireEvent(EVENT_NAME.Houston_Create_Incident_initiate, {
screen_name: SCREEN_NAME.DASHBOARD_PAGE,
});
dispatch({ type: actionTypes.SET_DRAWER_OPEN, payload: true });
};
const handleDrawerClose = () => {
dispatch({ type: actionTypes.SET_DRAWER_OPEN, payload: false });
};
return (
<ErrorBoundary>
<div>
<Button
startAdornment={<AddIcon color="white" />}
fullWidth
onClick={handleDrawerOpen}
variant="primary"
className={styles['create-incident']}
>
Create incident
</Button>
<Drawer
open={state.open}
headerText="Create incident"
className={`scroll-smooth ${styles.drawer}`}
headerContainerClasses={styles.header}
onClose={handleDrawerClose}
onOutSideClick={handleDrawerClose}
showHeaderDivider
>
<CreateIncidentForm
startIncidentSearch={startIncidentSearch}
handleDrawerClose={handleDrawerClose}
/>
</Drawer>
</div>
</ErrorBoundary>
);
};
export default CreateIncident;

View File

@@ -0,0 +1,289 @@
import { useReducer, MutableRefObject, useEffect, useState } from 'react';
import { ArrowDownSolidIcon } from '@navi/web-ui/lib/icons';
import { SelectPicker } from '@navi/web-ui/lib/components';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import {
Button,
Chip,
TextArea,
Typography,
} from '@navi/web-ui/lib/primitives';
import { SelectPickerOptionProps } from '@navi/web-ui/lib/components/SelectPicker/types';
import useOutsideClick from '@src/hooks/useOutsideClick';
import LoadingIcon from '@src/assets/LoadingIcon';
import { ApiService } from '@src/services/api';
import {
CREATE_INCIDENT,
FETCH_FILTER_CONFIG,
LIMITS as LIMITS,
actionTypes,
initialState,
reducer,
} from '@src/Pages/Dashboard/constants';
import styles from './CreateIncident.module.scss';
import { Team } from '@src/Pages/Dashboard/type';
import useClickStream from '@src/services/clickStream';
import { CLICK_STREAM_EVENT_FACTORY } from '@src/services/clickStream/constants/values';
interface CreateIncidentFormProps {
startIncidentSearch: () => void;
handleDrawerClose: () => void;
}
const CreateIncidentForm: React.FC<CreateIncidentFormProps> = ({
startIncidentSearch,
handleDrawerClose,
}) => {
const [state, dispatch] = useReducer(reducer, initialState);
const [loading, setIsLoading] = useState(false);
const { fireEvent } = useClickStream();
const { EVENT_NAME, SCREEN_NAME } = CLICK_STREAM_EVENT_FACTORY;
const ref = useOutsideClick({
callback: handleClickOutside,
}) as MutableRefObject<HTMLDivElement>;
function handleClickOutside() {
dispatch({ type: actionTypes.SET_OPEN_SELECT, payload: false });
}
const handleDivClick = () => {
dispatch({ type: actionTypes.SET_OPEN_SELECT, payload: true });
};
const validateTitle = (value: string): void => {
dispatch({
type: actionTypes.SET_IS_TITLE_VALID,
payload: value.length <= LIMITS.title.max,
});
};
const userData = JSON.parse(localStorage.getItem('user-data') || '{}');
const userEmail = userData?.emailId || [];
const validateDescription = (value: string): void => {
dispatch({
type: actionTypes.SET_IS_DESCRIPTION_VALID,
payload: value.length <= LIMITS.description.max,
});
};
const handleSeverityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
dispatch({ type: actionTypes.SET_SELECTED_SEVERITY, payload: event });
};
const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = event.target.value;
dispatch({ type: actionTypes.SET_TITLE, payload: inputValue });
validateTitle(inputValue);
};
const handleTeamChange = (
val: SelectPickerOptionProps | SelectPickerOptionProps[],
) => {
dispatch({ type: actionTypes.SET_SELECTED_TEAM, payload: val });
dispatch({ type: actionTypes.SET_OPEN_SELECT, payload: false });
};
const handleDescriptionChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const description = event.target.value;
dispatch({
type: actionTypes.SET_DESCRIPTION,
payload: event.target.value,
});
validateDescription(description);
};
const getData = () => {
ApiService.get(FETCH_FILTER_CONFIG)
.then(response => {
const apiResponse = response?.data?.data;
if (!apiResponse) {
toast.error('Something went wrong. Please try again later');
return;
}
dispatch({
type: actionTypes.SET_TEAMS,
payload: apiResponse[2]?.filter_data,
});
dispatch({
type: actionTypes.SET_SEVERITY,
payload: apiResponse[1]?.filter_data || [],
});
dispatch({
type: actionTypes.SET_SELECTED_SEVERITY,
payload: apiResponse[1]?.filter_data.map(item => item.label) || [],
});
})
.catch(error => {
const toastMessage = error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message},`
: '';
toast.error(toastMessage);
});
};
useEffect(() => {
getData();
}, []);
const getTeams = (teamsState: Team[]) => {
return teamsState.map(team => ({
label: team.label,
value: team.value,
}));
};
const clearDrawer = () => {
dispatch({ type: actionTypes.CLEAR_DRAWER });
handleDrawerClose();
};
const isDisabled = () => {
return (
!state.isTitleValid ||
!state.isDescriptionValid ||
!state.title ||
!state.selectedSeverity ||
!state.selectedTeam ||
!state.description
);
};
const createIncidentHandler = () => {
toast('Creating incident. Please wait a moment.', {
icon: <LoadingIcon />,
});
fireEvent(EVENT_NAME.Houston_Create_Incident_submit, {
screen_name: SCREEN_NAME.DASHBOARD_PAGE,
});
ApiService.post(CREATE_INCIDENT, {
title: state.title,
severity: state.selectedSeverity.value,
type: state.selectedTeam.value,
description: state.description,
createdBy: userEmail,
})
.then(response => {
setIsLoading(false);
toast.success('Incident created successfully');
clearDrawer();
startIncidentSearch();
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message}`
: 'Something went wrong. Please try again later'
}`;
toast.error(toastMessage);
handleDrawerClose();
});
};
return (
<div>
<div className={styles['incident-wrapper']}>
<div className={styles['incident-name']}>Incident title</div>
<TextArea
name="title"
placeholder="Enter incident title"
containerClassName={styles.textarea}
rows={3}
onChange={handleTitleChange as any}
value={state.title}
fullWidth
error={
!state.isTitleValid
? `Title should be less than ${LIMITS.title.max} characters`
: ''
}
size="large"
/>
</div>
<div className={styles['severity-picker']}>
<div className={styles['name-wrapper']}>Choose severity</div>
<div className={styles['severity-wrapper']}>
{state.severity.map((severityOption: SelectPickerOptionProps) => (
<Chip
key={severityOption.value}
label={severityOption.label}
size="medium"
onChipClick={() => handleSeverityChange(severityOption as any)}
selected={severityOption === state.selectedSeverity}
/>
))}
</div>
</div>
<div className={styles['Team-wrapper']}>
<div className={styles['name-wrapper']}>Choose team</div>
<div className={styles['team-div']} onClick={handleDivClick}>
<div className={styles['select-team']}>
<Typography
variant="p3"
color={
state.selectedTeam.length === 0
? 'var(--navi-color-gray-c2)'
: 'var(--navi-color-gray-c1)'
}
>
{state.selectedTeam.length === 0
? 'Select team'
: ` ${state.selectedTeam.label}`}
</Typography>
<ArrowDownSolidIcon
color="var(--navi-color-gray-c3)"
className={styles['arrow-down']}
/>
</div>
</div>
<div className={styles['select-picker-style']}>
{state.openSelect && (
<div className={styles['select-picker']} ref={ref}>
<SelectPicker
options={getTeams(state.teams)}
placeholder="Select team"
onSelectionChange={handleTeamChange}
multiSelect={false}
showSearchBar
wrapperClasses={styles['select-picker-item']}
/>
</div>
)}
</div>
</div>
<div className={styles['Description-wrapper']}>
<div className={styles['Desc-name']}>Description</div>
<TextArea
name="title"
placeholder="Enter incident description"
containerClassName={styles.textarea}
rows={7}
value={state.description}
onChange={handleDescriptionChange as any}
error={
!state.isDescriptionValid
? `Description should be less than ${LIMITS.description.max} characters`
: ''
}
fullWidth
size="large"
/>
</div>
<div className={styles['footer-wrapper']}>
<div className={styles['cancel']}>
<Button variant="secondary" fullWidth onClick={clearDrawer}>
Cancel
</Button>
</div>
<div className={styles['create']}>
<Button
variant="primary"
fullWidth
onClick={() => createIncidentHandler()}
disabled={isDisabled()}
>
Create incident
</Button>
</div>
</div>
</div>
);
};
export default CreateIncidentForm;

View File

@@ -143,4 +143,6 @@
.filter {
margin-top: 20px;
margin-left: 30px;
padding-left: 10px;
}

View File

@@ -7,12 +7,17 @@ import { DashboardHeaderConstants } from '../constants';
import styles from './DashboardHeader.module.scss';
import SmartSearch from './SmartSearch';
import Date from './Date';
import CreateIncident from './CreateIncident';
interface DashboardHeaderProps {
fetchIncidentData: (payload: any) => void;
startIncidentSearch: () => void;
}
const DashboardHeader: FC<DashboardHeaderProps> = ({ fetchIncidentData }) => {
const DashboardHeader: FC<DashboardHeaderProps> = ({
fetchIncidentData,
startIncidentSearch,
}) => {
const { title } = DashboardHeaderConstants;
const [searchValue, setSearchValue] = useState<string>('');
const navigate = useNavigate();
@@ -45,12 +50,17 @@ const DashboardHeader: FC<DashboardHeaderProps> = ({ fetchIncidentData }) => {
setSearchValue={setSearchValue}
updateURLAndFetchData={updateURLAndFetchData}
/>
<div className={styles['filter']}>
<FilterWrapper
clearSearchValue={clearSearchValue}
fetchIncidentData={fetchIncidentData}
/>
</div>
</div>
<div className={styles['filter']}>
<FilterWrapper
clearSearchValue={clearSearchValue}
fetchIncidentData={fetchIncidentData}
/>
<div className={styles['filter']}></div>
<div>
<CreateIncident startIncidentSearch={startIncidentSearch} />
</div>
</div>
</div>

View File

@@ -6,3 +6,7 @@ export interface filterItem {
filter_data: any;
selection_config: SelectionType;
}
export interface Team {
label: string;
value: string;
}

View File

@@ -0,0 +1,27 @@
import React from 'react';
interface useOutsideClickProps {
callback: Function;
}
const useOutsideClick = ({ callback }: useOutsideClickProps) => {
const ref = React.useRef<HTMLElement>();
React.useEffect(() => {
const handleClick = (event: any) => {
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
};
document.addEventListener('click', handleClick, true);
return () => {
document.removeEventListener('click', handleClick, true);
};
}, [ref]);
return ref;
};
export default useOutsideClick;

View File

@@ -18,6 +18,8 @@ const EVENT_NAME = {
Houston_Add_Oncall: 'Houston_Add_Oncall',
Houston_Add_Pseconcall: 'Houston_Add_Pseconcall',
Houston_Create_Team_Initiate: 'Houston_Create_Team_Initiate',
Houston_Create_Incident_initiate: 'Houston_Create_Incident_initiate',
Houston_Create_Incident_submit: 'Houston_Create_Incident_submit',
};
const SCREEN_NAME = {