diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..249d179 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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 +- [ ] Don’t 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 ( don’t use any, instead define interface and use it) +- [ ] Peer review +- [ ] Font-family / Basic css shouldn’t be overridden +- [ ] File changes shouldn’t be more than 20-30, if its more than 25 please create a draft PR and share it so it can reviewed properly. diff --git a/src/Pages/Dashboard/Dashboard.module.scss b/src/Pages/Dashboard/Dashboard.module.scss index 3f42bb1..c508183 100644 --- a/src/Pages/Dashboard/Dashboard.module.scss +++ b/src/Pages/Dashboard/Dashboard.module.scss @@ -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; +} diff --git a/src/Pages/Dashboard/constants.ts b/src/Pages/Dashboard/constants.ts index 0675c03..ba713f3 100644 --- a/src/Pages/Dashboard/constants.ts +++ b/src/Pages/Dashboard/constants.ts @@ -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; + } +}; diff --git a/src/Pages/Dashboard/index.tsx b/src/Pages/Dashboard/index.tsx index 32f0cb7..b0f0b03 100644 --- a/src/Pages/Dashboard/index.tsx +++ b/src/Pages/Dashboard/index.tsx @@ -122,7 +122,11 @@ const Dashboard: FC = () => { return (
- + + {returnTable()}
); diff --git a/src/Pages/Dashboard/partials/CreateIncident.module.scss b/src/Pages/Dashboard/partials/CreateIncident.module.scss new file mode 100644 index 0000000..8c240e3 --- /dev/null +++ b/src/Pages/Dashboard/partials/CreateIncident.module.scss @@ -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; +} diff --git a/src/Pages/Dashboard/partials/CreateIncident.tsx b/src/Pages/Dashboard/partials/CreateIncident.tsx new file mode 100644 index 0000000..4bbbde9 --- /dev/null +++ b/src/Pages/Dashboard/partials/CreateIncident.tsx @@ -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 = ({ + 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 ( + +
+ + + + +
+
+ ); +}; + +export default CreateIncident; diff --git a/src/Pages/Dashboard/partials/CreateIncidentForm.tsx b/src/Pages/Dashboard/partials/CreateIncidentForm.tsx new file mode 100644 index 0000000..6b2019f --- /dev/null +++ b/src/Pages/Dashboard/partials/CreateIncidentForm.tsx @@ -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 = ({ + 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; + 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) => { + dispatch({ type: actionTypes.SET_SELECTED_SEVERITY, payload: event }); + }; + const handleTitleChange = (event: React.ChangeEvent) => { + 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, + ) => { + 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: , + }); + + 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 ( +
+
+
Incident title
+