Merge branch 'master' into TP-53680

This commit is contained in:
Pooja Jaiswal
2024-01-09 15:18:14 +05:30
committed by GitHub
33 changed files with 1577 additions and 226 deletions

View File

@@ -33,7 +33,7 @@
},
"dependencies": {
"@navi/dark-knight": "^1.0.13",
"@navi/web-ui": "^1.58.13",
"@navi/web-ui": "^1.59.2",
"@reduxjs/toolkit": "^1.9.7",
"@stoddabr/react-tableau-embed-live": "^0.3.26",
"antd": "^5.9.4",

View File

@@ -55,6 +55,8 @@ const Date: React.FC<DateProps> = ({
const clearDate = () => {
searchParams.delete('from');
searchParams.delete('to');
searchParams.set('page_number', '0');
searchParams.set('page_size', '10');
const updatedQuery = searchParams.toString();
updateURLAndFetchData(updatedQuery);
};
@@ -70,6 +72,8 @@ const Date: React.FC<DateProps> = ({
searchParams.set('from', formattedStartDate);
searchParams.set('to', formattedEndDate);
searchParams.delete('incident_name');
searchParams.set('page_number', '0');
searchParams.set('page_size', '10');
clearSearchValue();
const updatedQuery = searchParams.toString();
updateURLAndFetchData(updatedQuery);

View File

@@ -32,6 +32,8 @@ const SmartSearch: FC<SmartSearchProps> = ({
};
const clearSearch = () => {
searchParams.delete('incident_name');
searchParams.set('page_number', '0');
searchParams.set('page_size', '10');
const updatedQuery = searchParams.toString();
updateURLAndFetchData(updatedQuery);
};
@@ -41,6 +43,8 @@ const SmartSearch: FC<SmartSearchProps> = ({
clearSearch();
} else {
searchParams.set('incident_name', searchValue);
searchParams.set('page_number', '0');
searchParams.set('page_size', '10');
const updatedQuery = searchParams.toString();
updateURLAndFetchData(updatedQuery);
}
@@ -54,6 +58,8 @@ const SmartSearch: FC<SmartSearchProps> = ({
clearSearch();
} else {
searchParams.set('incident_name', searchValue);
searchParams.set('page_number', '0');
searchParams.set('page_size', '10');
const updatedQuery = searchParams.toString();
updateURLAndFetchData(updatedQuery);
}

View File

@@ -183,3 +183,25 @@
.popup-style {
display: inline;
}
.hint-text {
margin-top: 24px;
font-size: 12px;
color: var(--navi-color-blue-base);
border: none;
:hover {
cursor: pointer;
}
}
.incident-input {
margin-top: 20px;
input {
color: var(--navi-color-gray-c2);
}
}
.incident-label {
font-size: 13px;
font-weight: 400;
}

View File

@@ -15,7 +15,7 @@ export const IncidentConstants = {
incidentId: 'Incident Id',
};
const URL_PREFIX = createURL('/houston');
export const URL_PREFIX = createURL('/houston');
export const FETCH_INCIDENT_DATA = (payload: any): string => {
return `${URL_PREFIX}/incidents/${payload}`;
@@ -38,6 +38,7 @@ export const FETCH_AUDIT_LOG = (incidentId: string): string => {
return `${URL_PREFIX}/logs/incident/${incidentId}`;
};
export const incidentRegrex = /^_houston-0*[1-9][0-9]*$/;
export interface ContentProps {
incidentData: any;
}
@@ -64,7 +65,10 @@ export interface CreatedInfoProps {
updatedAt: string;
isLastItem: boolean;
}
export interface ResponseType {
data: '';
status: number;
}
export const actionTypes = {
SET_INCIDENT_DATA: 'SET_INCIDENT_DATA',
SET_HEADER_DATA: 'SET_HEADER_DATA',
@@ -84,6 +88,10 @@ export const actionTypes = {
SET_IS_SEVERITY_PICKER_OPEN: 'SET_IS_SEVERITY_PICKER_OPEN',
SET_IS_STATUS_PICKER_OPEN: 'SET_IS_STATUS_PICKER_OPEN',
SET_IS_TEAM_PICKER_OPEN: 'SET_IS_TEAM_PICKER_OPEN',
SET_DUPLICATE_DIALOG: 'SET_DUPLICATE_DIALOG',
SET_INCIDENT_NAME: 'SET_INCIDENT_NAME',
SET_ERROR_MSG: 'SET_ERROR_MSG',
RESET_DUPLICATE_DIALOG: 'RESET_DUPLICATE_DIALOG',
};
export const reducer = (state, action) => {
@@ -108,6 +116,8 @@ export const reducer = (state, action) => {
return { ...state, totalLog: action.payload };
case actionTypes.SET_OPEN_DIALOG:
return { ...state, openDialog: action.payload };
case actionTypes.SET_DUPLICATE_DIALOG:
return { ...state, openDuplicateDialog: action.payload };
case actionTypes.SET_OPEN_CONFIRMATION_DIALOG:
return { ...state, openConfirmationDialog: action.payload };
case actionTypes.SET_SELECTED_OPTION:
@@ -124,6 +134,17 @@ export const reducer = (state, action) => {
return { ...state, isStatusPickerOpen: action.payload };
case actionTypes.SET_IS_TEAM_PICKER_OPEN:
return { ...state, isTeamPickerOpen: action.payload };
case actionTypes.SET_INCIDENT_NAME:
return { ...state, incidentName: action.payload };
case actionTypes.SET_ERROR_MSG:
return { ...state, errorMsg: action.payload };
case actionTypes.RESET_DUPLICATE_DIALOG:
return {
...state,
openDuplicateDialog: false,
incidentName: '',
errorMsg: '',
};
default:
return state;
}
@@ -148,6 +169,9 @@ export const initialState = {
isSeverityPickerOpen: false,
isStatusPickerOpen: false,
isTeamPickerOpen: false,
incidentName: '',
openDuplicateDialog: false,
errorMsg: '',
};
export const RESOLVE_STATUS = '4';

View File

@@ -1,12 +1,18 @@
import { FC, useEffect, useState, useReducer, MutableRefObject } from 'react';
import { useMatch } from 'react-router-dom';
import classnames from 'classnames';
import { Tabs } from 'antd';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import { AlertOutlineIcon, ArrowDownIcon } from '@navi/web-ui/lib/icons';
import TabItem from '@navi/web-ui/lib-esm/components/Tabs/TabItem';
import Tabs from '@navi/web-ui/lib-esm/components/Tabs/Tabs';
import { SelectPicker } from '@navi/web-ui/lib/components';
import { ModalDialog, Tooltip, Typography } from '@navi/web-ui/lib/primitives';
import {
BorderedInput,
ModalDialog,
Tooltip,
Typography,
Button,
} from '@navi/web-ui/lib/primitives';
import { SelectPickerOptionProps } from '@navi/web-ui/lib/components/SelectPicker/types';
import GoToLinkIcon from '@src/assets/GoToLinkIcon';
import { ApiService } from '@src/services/api';
@@ -34,15 +40,15 @@ import {
StatusType,
TeamType,
SLACK_BASE_URL,
incidentRegrex,
ResponseType,
} from './constants';
import styles from './Incidents.module.scss';
const Incident: FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { fireEvent } = useClickStream();
const { EVENT_NAME, SCREEN_NAME } = CLICK_STREAM_EVENT_FACTORY;
const updatedSeverities = generateOptions(state.headerData?.severities);
const updatedStatuses = generateOptions(state.headerData?.incidentStatuses);
const updatedTeams = generateOptions(state.headerData?.teams);
@@ -54,6 +60,8 @@ const Incident: FC = () => {
path: '/incident/:incidentId',
});
const incidentId = IncidentMatch?.params?.incidentId || '';
const currentIncidentId = parseInt(incidentId, 10);
const incidentName = state.incidentName;
const severityMap = state.headerData?.severities?.reduce((map, severity) => {
map[severity.value] = severity.label;
@@ -70,6 +78,7 @@ const Incident: FC = () => {
map[Incidentteam.value] = Incidentteam.label;
return map;
}, {});
const handleSevClickOutside = () => {
dispatch({
type: actionTypes.SET_IS_SEVERITY_PICKER_OPEN,
@@ -376,16 +385,6 @@ const Incident: FC = () => {
payload: 'resolved',
});
dispatch({ type: actionTypes.SET_OPEN_DIALOG, payload: true });
} else if (value === DUPLICATE_STATUS) {
dispatch({
type: actionTypes.SET_DIALOG_TEXT,
payload: 'Duplicate incident',
});
dispatch({
type: actionTypes.SET_DIALOG_BODY_TEXT,
payload: 'duplicate',
});
dispatch({ type: actionTypes.SET_OPEN_DIALOG, payload: true });
} else {
toast('Updating ticket. Please wait a moment.', {
icon: <LoadingIcon />,
@@ -436,11 +435,16 @@ const Incident: FC = () => {
payload: false,
});
dispatch({ type: actionTypes.SET_IS_TEAM_PICKER_OPEN, payload: false });
if (
updateType === StatusType &&
(selectedvalue === RESOLVE_STATUS || selectedvalue === DUPLICATE_STATUS)
) {
if (updateType === StatusType && selectedvalue === RESOLVE_STATUS) {
handleStatusSelectionChange(selectedOption);
} else if (
updateType === StatusType &&
selectedvalue === DUPLICATE_STATUS
) {
dispatch({
type: actionTypes.SET_DUPLICATE_DIALOG,
payload: true,
});
} else {
dispatch({ type: actionTypes.SET_UPDATE_TYPE, payload: updateType });
dispatch({
@@ -508,6 +512,81 @@ const Incident: FC = () => {
} `;
}
};
const handleIncidentChange = (
e: React.ChangeEvent<HTMLInputElement>,
): void => {
const inputValue = e.target.value;
validateIncidentID(inputValue);
dispatch({
type: actionTypes.SET_INCIDENT_NAME,
payload: inputValue,
});
};
const handleResetDialog = (): void => {
dispatch({
type: actionTypes.RESET_DUPLICATE_DIALOG,
});
};
const validateIncidentID = (value: string): void => {
dispatch({
type: actionTypes.SET_ERROR_MSG,
payload: !incidentRegrex.test(value)
? 'Please enter a valid incident I.D.'
: '',
});
};
const extractIncidentId = (incidentName: string): number | null => {
return (match => (match ? parseInt(match[1], 10) : null))(
incidentName.match(/_houston-(\d+)/),
);
};
const duplicateOfId = extractIncidentId(incidentName);
const disable = (): boolean => {
return !incidentName || !validate(incidentName);
};
const goToIncident = (): void => {
if (state.incidentName) {
const incidentId = extractIncidentId(state.incidentName);
window.open(`/incident/${incidentId}`, '_blank');
}
};
const validate = (value: string): boolean => incidentRegrex.test(value);
const isDisabled = (): boolean => {
const incidentId = extractIncidentId(state.incidentName);
return !incidentId;
};
const markDuplicateIncident = (): void => {
const endPoint = UPDATE_INCIDENT;
toast('Updating ticket. Please wait a moment.', {
icon: <LoadingIcon />,
});
ApiService.post(endPoint, {
id: currentIncidentId,
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();
fetchIncidentLog();
handleResetDialog();
fetchParticipants();
}
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message}`
: 'Something went wrong. Please try again.'
}`;
toast.error(toastMessage);
startIncidentSearch();
});
};
return (
<ErrorBoundary>
@@ -521,222 +600,232 @@ const Incident: FC = () => {
<div className={styles['tab-content-wrapper']}>
<Tabs
onTabChange={function noRefCheck() {}}
defaultSelectedKey={'detail-key'}
>
<TabItem label="Details" key="detail-key">
<div className={styles['horizontal-line']}>
<div className={styles['content-wrapper']}>
<div className={styles['audit-log']}>
<div className={styles['log-update-text']}>
<Typography
variant="h6"
color="var(--navi-color-gray-c3)"
>
UPDATE INCIDENT
</Typography>
{!isUserParticipantList && (
<div className={styles['info-icon']}>
<Tooltip
text="Only Slack channel participants can update this incident."
withPointer
position={'right'}
>
<div className={styles['alert-icon']}>
<AlertOutlineIcon />
</div>
</Tooltip>
</div>
)}
</div>
<div className={styles['log-update-dropdown']}>
<div className={styles['dropdown-severity']}>
<div>
defaultActiveKey="1"
items={[
{
key: '1',
label: 'Details',
children: (
<div>
<div className={styles['content-wrapper']}>
<div className={styles['audit-log']}>
<div className={styles['log-update-text']}>
<Typography
variant="p5"
color="var(--navi-color-gray-c2"
variant="h6"
color="var(--navi-color-gray-c3)"
>
Severity
UPDATE INCIDENT
</Typography>
</div>
<div ref={refSeverity}>
<div
className={combinedSevClassNames}
onClick={handleSevClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
{!isUserParticipantList && (
<div className={styles['info-icon']}>
<Tooltip
text="Only Slack channel participants can update this incident."
withPointer
position={'right'}
>
{severityMap && state.severity?.value
? severityMap[state.severity.value]
: '-'}
</Typography>
<div className={styles['alert-icon']}>
<AlertOutlineIcon />
</div>
</Tooltip>
</div>
<div className={styles['arrowdown-style']}>
<ArrowDownIcon />
</div>
</div>
<div className={styles['severity-selectpicker']}>
{state.isSeverityPickerOpen && (
<div
className={
styles['severity-selectpicker-style']
}
>
<SelectPicker
multiSelect={false}
onSelectionChange={selectedOption =>
handleOpenConfirmationDailog(
selectedOption,
SeverityType,
)
}
options={updatedSeverities}
selectedValue={state.severity?.value.toString()}
/>
</div>
)}
</div>
)}
</div>
</div>
<div className={styles['dropdown-status']}>
<div>
<Typography
variant="p5"
color="var(--navi-color-gray-c2)"
>
Status
</Typography>
</div>
<div ref={refStatus}>
<div
className={combinedStatusClassNames}
onClick={handleStatusClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{incidentStatusMap && state.status?.value
? incidentStatusMap[state.status.value]
: '-'}
</Typography>
</div>
<div>
<ArrowDownIcon
className={styles['arrowdown-style']}
/>
</div>
</div>
<div className={styles['status-selectpicker']}>
{state.isStatusPickerOpen && (
<div
className={styles['status-selectpicker-style']}
>
<SelectPicker
multiSelect={false}
onSelectionChange={selectedOption =>
handleOpenConfirmationDailog(
selectedOption,
StatusType,
)
}
options={updatedStatuses}
selectedValue={state.status?.value.toString()}
/>
</div>
)}
</div>
</div>
</div>
<div className={styles['dropdown-team']}>
<div>
<Typography variant="p5" color="#585757">
Team
</Typography>
</div>
<div ref={refTeam}>
<div
className={combinedTeamClassNames}
onClick={handleTeamClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{teamsMap && state.team?.value
? teamsMap[state.team.value]
: '-'}
</Typography>
</div>
<div className={styles['log-update-dropdown']}>
<div className={styles['dropdown-severity']}>
<div>
<ArrowDownIcon
className={styles['arrowdown-style']}
/>
<Typography
variant="p5"
color="var(--navi-color-gray-c2"
>
Severity
</Typography>
</div>
<div ref={refSeverity}>
<div
className={combinedSevClassNames}
onClick={handleSevClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{severityMap && state.severity?.value
? severityMap[state.severity.value]
: '-'}
</Typography>
</div>
<div className={styles['arrowdown-style']}>
<ArrowDownIcon />
</div>
</div>
<div className={styles['severity-selectpicker']}>
{state.isSeverityPickerOpen && (
<div
className={
styles['severity-selectpicker-style']
}
>
<SelectPicker
multiSelect={false}
onSelectionChange={selectedOption =>
handleOpenConfirmationDailog(
selectedOption,
SeverityType,
)
}
options={updatedSeverities}
selectedValue={state.severity?.value.toString()}
/>
</div>
)}
</div>
</div>
</div>
<div className={styles['team-selectpicker']}>
{state.isTeamPickerOpen && (
<div
className={styles['team-selectpicker-style']}
<div className={styles['dropdown-status']}>
<div>
<Typography
variant="p5"
color="var(--navi-color-gray-c2)"
>
<SelectPicker
multiSelect={false}
onSelectionChange={selectedOption =>
handleOpenConfirmationDailog(
selectedOption,
TeamType,
)
}
options={updatedTeams}
selectedValue={state.team?.value.toString()}
/>
Status
</Typography>
</div>
<div ref={refStatus}>
<div
className={combinedStatusClassNames}
onClick={handleStatusClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{incidentStatusMap && state.status?.value
? incidentStatusMap[state.status.value]
: '-'}
</Typography>
</div>
<div>
<ArrowDownIcon
className={styles['arrowdown-style']}
/>
</div>
</div>
)}
<div className={styles['status-selectpicker']}>
{state.isStatusPickerOpen && (
<div
className={
styles['status-selectpicker-style']
}
>
<SelectPicker
multiSelect={false}
onSelectionChange={selectedOption =>
handleOpenConfirmationDailog(
selectedOption,
StatusType,
)
}
options={updatedStatuses}
selectedValue={state.status?.value.toString()}
/>
</div>
)}
</div>
</div>
</div>
<div className={styles['dropdown-team']}>
<div>
<Typography
variant="p5"
color="var(--navi-color-gray-c2)"
>
Team
</Typography>
</div>
<div ref={refTeam}>
<div
className={combinedTeamClassNames}
onClick={handleTeamClick}
>
<div>
<Typography
variant="p4"
color={
isUserParticipantList
? 'var(--navi-color-gray-c1)'
: 'var(--navi-color-gray-c3)'
}
>
{teamsMap && state.team?.value
? teamsMap[state.team.value]
: '-'}
</Typography>
</div>
<div>
<ArrowDownIcon
className={styles['arrowdown-style']}
/>
</div>
</div>
<div className={styles['team-selectpicker']}>
{state.isTeamPickerOpen && (
<div
className={
styles['team-selectpicker-style']
}
>
<SelectPicker
multiSelect={false}
onSelectionChange={selectedOption =>
handleOpenConfirmationDailog(
selectedOption,
TeamType,
)
}
options={updatedTeams}
selectedValue={state.team?.value.toString()}
/>
</div>
)}
</div>
</div>
</div>
</div>
<ActivityLog
incidentLog={state.incidentLog}
totalLog={state.totalLog}
/>
</div>
</div>
{
<ActivityLog
incidentLog={state.incidentLog}
totalLog={state.totalLog}
<DescriptionContent
id={state.incidentData?.id}
incidentName={state.incidentData?.incidentName}
description={state.incidentData?.description}
slackChannel={state.incidentData?.slackChannel}
incidentParticipants={state.incidentParticipants}
jiraIds={state.incidentData?.jiraLinks}
rcaLink={state.incidentData?.rcaLink}
/>
}
</div>
</div>
<DescriptionContent
id={state.incidentData?.id}
incidentName={state.incidentData?.incidentName}
description={state.incidentData?.description}
slackChannel={state.incidentData?.slackChannel}
incidentParticipants={state.incidentParticipants}
jiraIds={state.incidentData?.jiraLinks}
rcaLink={state.incidentData?.rcaLink}
/>
</div>
</div>
</TabItem>
</Tabs>
),
},
]}
/>
</div>
{open && (
<ModalDialog
@@ -748,7 +837,9 @@ const Incident: FC = () => {
},
{
label: 'Open Channel',
startAdornment: <GoToLinkIcon color="#fff" />,
startAdornment: (
<GoToLinkIcon color="var(--navi-color-gray-bg-primary)" />
),
onClick: handleGoToSlackChannel,
},
]}
@@ -761,6 +852,54 @@ const Incident: FC = () => {
</Typography>
</ModalDialog>
)}
{state.openDuplicateDialog ? (
<ModalDialog
open={state.openDuplicateDialog}
footerButtons={[
{
label: 'Cancel',
onClick: handleResetDialog,
},
{
label: 'Mark as duplicate',
disabled: disable(),
onClick: () => {
markDuplicateIncident();
},
},
]}
header={`Duplicate incident`}
onClose={handleResetDialog}
>
<Typography variant="p3" color="var(--navi-color-gray-c1)">
Once marked as duplicate, this incident will be archived after 24
hours.
</Typography>
<div className={styles['incident-input']}>
<BorderedInput
inputLabel="Attach incident to"
labelClassName={styles['incident-label']}
fullWidth
value={state.incidentName ? state.incidentName : '_houston-'}
onChange={handleIncidentChange}
error={state.errorMsg}
Icon={<AlertOutlineIcon color="var(--navi-color-red-base)" />}
/>
</div>
<Button
className={styles['hint-text']}
variant="text"
startAdornment={
<GoToLinkIcon color="var(--navi-color-blue-base)" />
}
disabled={isDisabled()}
onClick={goToIncident}
>
Go to incident
</Button>
</ModalDialog>
) : null}
{state.openDialog ? (
<ModalDialog
open={state.openDialog}
@@ -777,7 +916,9 @@ const Incident: FC = () => {
{
label: 'Go to slack channel',
onClick: handleGoToSlackChannel,
startAdornment: <GoToLinkIcon color="#fff" />,
startAdornment: (
<GoToLinkIcon color="var(--navi-color-gray-bg-primary)" />
),
},
]}
header={`${state.dialogText}`}

View File

@@ -0,0 +1,73 @@
.table-wrapper {
box-sizing: border-box;
padding-bottom: 24px;
margin-bottom: 70px;
}
.dashboard-wrapper {
padding: 24px;
}
.houston-id-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.id-wrapper {
display: flex;
align-items: center;
gap: 8px;
position: fixed;
}
.houston-id-wrapper {
display: flex;
align-items: center;
gap: 8px;
.hyperlink {
color: var(--navi-color-gray-c2);
text-decoration: none;
transition: color 0.3s;
}
.go-to-link-icon {
display: none;
transition: opacity 0.3s;
}
&:hover {
.hyperlink {
color: var(--navi-color-blue-base);
cursor: pointer;
}
.go-to-link-icon {
display: inline-block;
opacity: 1;
padding-top: 8px;
}
}
}
.alert-icon {
margin-top: 6px;
}
.team-parent-style {
display: flex;
align-items: center;
position: fixed;
}
.team-style {
display: flex;
align-items: center;
position: fixed;
}
:export {
goToLinkColor: #0276fe;
alertIconRed: #e92c2c;
}

View File

@@ -0,0 +1,59 @@
.desktop-table-search-list {
& > * {
.pagination-wrapper-class {
overflow: visible;
.incident-list-table-pagination {
border-radius: 0 0 8px 8px;
}
}
.ag-cell-value {
overflow: visible;
}
.ag-row {
z-index: 0;
&:hover {
z-index: 1;
}
}
.ag-row {
&.ag-row-focus {
z-index: 2;
}
}
.ag-root-wrapper,
.ag-root,
.ag-body-viewport,
.ag-body-viewport-wrapper,
.ag-center-cols-clipper,
.ag-center-cols-viewport {
overflow: visible !important;
}
}
}
.pagination-wrapper-class {
overflow: visible;
.search-list-table-pagination {
border-radius: 0 0 8px 8px;
}
}
.incident-search-list-table {
box-sizing: border-box;
margin-bottom: 56px;
}
.drawer-wrapper {
width: 458px !important;
}
:export {
sessionId: var(--navi-color-gray-c2);
}

View File

@@ -0,0 +1,11 @@
import { createURL } from '@src/services/globalUtils';
const URL_PREFIX = createURL('/houston');
export const DashboardHeaderConstants = {
title: 'JIRA tickets',
};
export const FETCH_JIRA_DATA = (payload: string): string => {
return `${URL_PREFIX}/get-jira-statuses?${payload}`;
};

View File

@@ -0,0 +1,130 @@
import { FC, useState, useEffect, useRef } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { toast } from '@navi/web-ui/lib/primitives/Toast/index';
import FallbackComponent from '@src/components/Fallback';
import {
setJiraDashboardData,
setPageDetails,
setIsLoading,
setCurrentPageNumber,
setCurrentPageSize,
} from '@src/slices/jiraDashboardSlice';
import { ApiService } from '@src/services/api';
import { JiraDashboardData, JiraDashboardState } from '@src/types';
import { FetchJiraDataProps } from './types';
import { FETCH_JIRA_DATA } from './constants';
import SearchResultsTable from './partials/SearchResultsTable';
import DashboardHeader from './partials/DashboardHeader';
import styles from './Dashboard.module.scss';
const JiraDashboard: FC = () => {
const data = useSelector(
(state: { jiraDashboard: { data: JiraDashboardData[] } }) =>
state.jiraDashboard.data,
);
const { pageDetails, isLoading, currentPageNumber, currentPageSize } =
useSelector(
(state: { jiraDashboard: JiraDashboardState }) => state.jiraDashboard,
);
const dispatch = useDispatch();
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const startJiraSearch = (param: string): void => {
const endPoint = FETCH_JIRA_DATA(param);
dispatch(setIsLoading(true));
ApiService.get(endPoint)
.then(response => {
dispatch(setIsLoading(false));
dispatch(setJiraDashboardData(response?.data?.data));
dispatch(setPageDetails(response?.data?.page));
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message},`
: ''
}`;
dispatch(setIsLoading(true));
toast.error(toastMessage);
dispatch(setJiraDashboardData([]));
});
};
useEffect(() => {
const pageNumberParam = searchParams.get('page_number');
const pageSizeParam = searchParams.get('page_size');
//TODO: creating a general util for below to be used across all both dashboards
if (pageNumberParam) {
dispatch(setCurrentPageNumber(Number(pageNumberParam)));
} else {
searchParams.set('page_number', '0');
dispatch(setCurrentPageNumber(0));
}
if (pageSizeParam) {
dispatch(setCurrentPageSize(Number(pageSizeParam)));
} else {
searchParams.set('page_size', '10');
dispatch(setCurrentPageSize(10));
}
const searchParam = searchParams.toString();
updateURLAndFetchData(searchParam);
}, [searchParams]);
const handlePageNumber = (pageNumber: number): void => {
searchParams.set('page_number', (pageNumber - 1).toString());
const updatedQuery = searchParams.toString();
updateURLAndFetchData(updatedQuery);
};
const handlePageSize = (pageSize: number): void => {
searchParams.set('page_size', pageSize.toString());
const updatedQuery = searchParams.toString();
updateURLAndFetchData(updatedQuery);
};
const fetchJiraData = (props: FetchJiraDataProps): void => {
const { filterQuery = '', isDrawer = false } = props;
const finalParams = filterQuery ? `${filterQuery}` : '';
dispatch(setCurrentPageNumber(0));
startJiraSearch(`${finalParams}`);
};
const updateURLAndFetchData = (updatedQuery: string): void => {
navigate({
search: updatedQuery,
});
fetchJiraData({
filterQuery: updatedQuery,
});
};
const returnTable = (): JSX.Element => {
if (isLoading) {
return <FallbackComponent />;
}
return (
<div className={styles['table-wrapper']}>
<SearchResultsTable
currentPageSize={currentPageSize}
currentPageData={data}
pageDetails={pageDetails}
handlePageNumberChange={handlePageNumber}
handlePageSizeChange={handlePageSize}
fetchJiraData={fetchJiraData}
/>
</div>
);
};
return (
<div className={styles['dashboard-wrapper']}>
<DashboardHeader fetchJiraData={fetchJiraData} />
{returnTable()}
</div>
);
};
export default JiraDashboard;

View File

@@ -0,0 +1,50 @@
.more-info-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
.filter-components-wrapper {
display: flex;
justify-content: space-between;
}
.input-wrapper {
display: flex;
align-items: center;
width: 360px;
border-radius: 6px;
padding: 11px 0 11px 0;
&_search-title {
margin-right: 8px;
}
&_on-error {
margin-top: -20px;
margin-right: 8px;
}
input {
margin: 0 12px;
&:-ms-input-placeholder {
color: var(--navi-color-gray-c3) !important;
}
}
}
.input-container {
min-width: 360px;
height: 36px;
&_disabled {
div {
cursor: not-allowed;
input {
cursor: not-allowed;
}
}
}
* input::placeholder {
color: var(--navi-color-gray-c3) !important;
}
}

View File

@@ -0,0 +1,45 @@
import { FC } from 'react';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Typography } from '@navi/web-ui/lib/primitives';
import { DashboardHeaderConstants } from '../constants';
import styles from './DashboardHeader.module.scss';
import SmartSearch from './SmartSearch';
interface DashboardHeaderProps {
fetchJiraData: (payload: { filterQuery: string }) => void;
}
const DashboardHeader: FC<DashboardHeaderProps> = ({ fetchJiraData }) => {
const { title } = DashboardHeaderConstants;
const [searchValue, setSearchValue] = useState<string>('');
const navigate = useNavigate();
const updateURLAndFetchData = (updatedQuery: string): void => {
navigate({
search: updatedQuery,
});
fetchJiraData({
filterQuery: updatedQuery,
});
};
return (
<div>
<div className={styles['more-info-wrapper']}>
<Typography variant="h2">{title}</Typography>
</div>
<div className={styles['filter-components-wrapper']}>
<div className={styles['search-wrapper']}>
<SmartSearch
searchValue={searchValue}
setSearchValue={setSearchValue}
updateURLAndFetchData={updateURLAndFetchData}
/>
</div>
</div>
</div>
);
};
export default DashboardHeader;

View File

@@ -0,0 +1,32 @@
import React, { FC } from 'react';
import { useNavigate } from 'react-router-dom';
import GoToLinkIcon from '@src/assets/GoToLinkIcon';
import styles from '../Dashboard.module.scss';
interface HyperlinkCellRendererProps {
value: string;
id: number;
}
const HyperlinkCellRenderer: FC<HyperlinkCellRendererProps> = ({
value,
id,
}) => {
const navigate = useNavigate();
const handleClick = (): void => {
navigate(`/incident/${id}`);
};
return (
<div className={styles['houston-id-wrapper']}>
<div onClick={handleClick} className={styles['hyperlink']}>
{value}
</div>
<div className={styles['go-to-link-icon']}>
<GoToLinkIcon color={styles.goToLinkColor} />
</div>
</div>
);
};
export default HyperlinkCellRenderer;

View File

@@ -0,0 +1,172 @@
import { FC } from 'react';
import { AgTable, Pagination } from '@navi/web-ui/lib/components';
import { DropDownPosition } from '@navi/web-ui/lib/components/Pagination/constant';
import { returnFormattedDate } from '@src/services/globalUtils';
import { JiraDashboardData } from '@src/types';
import cx from 'classnames';
import { FetchJiraDataProps, JiraTableRow } from '../types';
import HyperlinkCellRenderer from './HyperlinkCellRenderer';
import TeamAssignedCellRenderer from './TeamAssignedCellRenderer';
import TicketNameCell from './ticketNameCell';
import styles from '../SearchResultsTable.module.scss';
interface SearchResultTableProps {
currentPageData: JiraDashboardData[];
pageDetails: {
pageNumber: number;
totalElements: number;
};
currentPageSize: number;
handlePageNumberChange: (pageNumber: number) => void;
handlePageSizeChange: (pageSize: number) => void;
fetchJiraData: (props: FetchJiraDataProps) => void;
}
const defaultColDef = {
cellStyle: { lineHeight: 2 },
wrapText: true,
autoHeight: true,
width: 200,
};
const SearchResultsTable: FC<SearchResultTableProps> = ({
currentPageData,
pageDetails,
currentPageSize,
handlePageNumberChange,
handlePageSizeChange,
}) => {
const { pageNumber, totalElements } = pageDetails;
const cellStyle = {
'text-overflow': 'ellipsis',
'white-space': 'nowrap',
overflow: 'hidden',
enableColResize: true,
};
const rowData = currentPageData?.map((item: JiraTableRow, ind: number) => {
let index = 1;
if (pageNumber === 0) {
index = 1;
} else if (pageNumber > 0) {
index = currentPageSize * pageNumber + 1;
}
return {
sNo: ind === 0 ? index : index + ind,
id: item?.incidentID,
incidentName: item?.incidentName,
ticketName: item?.jiraKey,
ticketType: item?.jiraType,
JiraStatus: item?.jiraStatus,
JiraDescription: item?.jiraSummary,
JiraAssignedTo: item?.teamsInvolved,
JiraCreatedOn: returnFormattedDate(item?.jiraCreateAt),
JiraLink: item?.jiraLink,
};
});
const columnData = [
{
field: 'incidentName',
headerName: 'Linked Houston I.D.',
suppressMovable: true,
cellRenderer: params => (
<HyperlinkCellRenderer
value={params.data.incidentName}
id={params.data.id}
/>
),
width: 200,
},
{
field: 'ticketName',
headerName: 'JIRA ticket I.D.',
suppressMovable: true,
cellRenderer: params => (
<TicketNameCell
tickerName={params.data.ticketName}
ticketLink={params.data.JiraLink}
ticketType={params.data.ticketType}
/>
),
},
{
field: 'JiraStatus',
headerName: 'JIRA status',
suppressMovable: true,
},
{
field: 'JiraDescription',
headerName: 'JIRA description',
suppressMovable: true,
width: 500,
cellStyle: {
'text-overflow': 'ellipsis',
'white-space': 'nowrap',
overflow: 'hidden',
},
},
{
field: 'JiraAssignedTo',
headerName: 'JIRA assigned to',
suppressMovable: true,
cellRenderer: params => (
<TeamAssignedCellRenderer teamsInvolved={params.data.JiraAssignedTo} />
),
width: 300,
},
{
field: 'JiraCreatedOn',
headerName: 'JIRA created on',
suppressMovable: true,
width: 200,
flex: 1,
},
];
const onGridSizeChanged = (params: {
api: { sizeColumnsToFit: () => void };
}): void => {
if (params?.api?.sizeColumnsToFit) params.api.sizeColumnsToFit();
};
return (
<div
className={cx(
styles['session-search-list-table'],
styles['desktop-table-search-list'],
)}
>
<AgTable
PaginationComponent={
<Pagination
pageNumberDropDownPosition={DropDownPosition.BOTTOM}
pageSize={currentPageSize}
currentPage={pageNumber + 1}
totalCount={totalElements}
onPageChange={handlePageNumberChange}
onPageSizeChange={handlePageSizeChange}
containerClasses={styles['search-list-table-pagination']}
/>
}
columnDefs={columnData}
rowData={rowData}
theme="alpine"
sizeColumnsToFit={true}
domLayout="autoHeight"
paginationWrapperClasses={styles['pagination-wrapper-class']}
suppressCellFocus
defaultColDef={defaultColDef}
suppressRowHoverHighlight={true}
onGridSizeChanged={onGridSizeChanged}
suppressColumnMoveAnimation
rowHeight={54}
detailRowAutoHeight={true}
/>
</div>
);
};
export default SearchResultsTable;

View File

@@ -0,0 +1,66 @@
import React, { FC, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import cx from 'classnames';
import { BorderedInput } from '@navi/web-ui/lib/primitives';
import SearchIcon from '@navi/web-ui/lib/icons/SearchIcon/SearchIcon';
import styles from './DashboardHeader.module.scss';
interface SmartSearchProps {
setSearchValue: (payload: string) => void;
searchValue: string;
updateURLAndFetchData: (payload: string) => void;
}
const SmartSearch: FC<SmartSearchProps> = ({
setSearchValue,
searchValue,
updateURLAndFetchData,
}) => {
const [searchParams] = useSearchParams();
useEffect(() => {
const searchParam = searchParams.get('incident_name');
if (searchParam) {
setSearchValue(searchParam);
} else {
setSearchValue('');
}
}, [searchParams]);
const handleSearchInput = (e: React.ChangeEvent<HTMLInputElement>): void => {
setSearchValue(e.target.value);
};
const clearSearch = () => {
searchParams.delete('incident_name');
const updatedQuery = searchParams.toString();
updateURLAndFetchData(updatedQuery);
};
const onKeyPressClickHandler = (event: React.KeyboardEvent): void => {
if (event.key === 'Enter') {
if (searchValue === '') {
clearSearch();
} else {
searchParams.set('incident_name', searchValue);
const updatedQuery = searchParams.toString();
updateURLAndFetchData(updatedQuery);
}
}
};
return (
<div className={styles['input-wrapper']}>
<BorderedInput
LeftInputAdornment={<SearchIcon />}
fullWidth
value={searchValue}
onChange={handleSearchInput}
placeholder="Search by Houston I.D."
containerClassName={cx(styles['input-container'])}
onKeyPress={onKeyPressClickHandler}
/>
</div>
);
};
export default SmartSearch;

View File

@@ -0,0 +1,45 @@
import React, { FC } from 'react';
import styles from '../Dashboard.module.scss';
import { Tooltip, Typography } from '@navi/web-ui/lib/primitives';
interface TeamAssignedCellRendererProps {
teamsInvolved: string[];
}
const TeamAssignedCellRenderer: FC<TeamAssignedCellRendererProps> = ({
teamsInvolved,
}) => {
const renderTeamNames = (): React.ReactNode => {
if (!teamsInvolved || teamsInvolved.length === 0) {
return null;
}
const firstTeam = teamsInvolved[0];
const remainingTeams = teamsInvolved.slice(1);
if (remainingTeams.length === 0) {
return <div>{firstTeam}</div>;
} else {
const tooltipText = remainingTeams.join(', ');
return (
<div className={styles['team-parent-style']}>
<div className={styles['team-style']}>
{firstTeam} &
<div className={styles['team-tooltip']}>
<Tooltip text={tooltipText} position="left" withPointer={false}>
<Typography variant="p3" color={styles.goToLinkColor}>
&nbsp;{remainingTeams.length} more
</Typography>
</Tooltip>
</div>
</div>
</div>
);
}
};
return <div>{renderTeamNames()}</div>;
};
export default TeamAssignedCellRenderer;

View File

@@ -0,0 +1,75 @@
import React, { FC } from 'react';
import { AlertOutlineIcon } from '@navi/web-ui/lib/icons';
import { Tooltip } from '@navi/web-ui/lib/primitives';
import {
GoToLinkIcon,
StoryIcon,
SubTaskIcon,
TaskIcon,
EpicIcon,
BugIcon,
TechTaskIcon,
} from '@src/assets';
import styles from '../Dashboard.module.scss';
const getTicketIcon = (ticketType: string): JSX.Element | null => {
switch (ticketType) {
case 'Story':
return <StoryIcon />;
case 'Sub-task':
return <SubTaskIcon />;
case 'Task':
return <TaskIcon />;
case 'Epic':
return <EpicIcon />;
case 'Bug':
return <BugIcon />;
case 'Tech Task':
return <TechTaskIcon />;
default:
return <AlertOutlineIcon color={styles.alertIconRed} />;
}
};
interface TicketNameCellProps {
tickerName: string;
ticketLink: string;
ticketType: string;
}
const TicketNameCell: FC<TicketNameCellProps> = ({
tickerName,
ticketLink,
ticketType,
}) => {
const handleClick = (): void => {
window.open(ticketLink, '_blank');
};
return (
<div className={styles['houston-id-wrapper']}>
<div className={styles['id-wrapper']}>
{ticketType ? (
getTicketIcon(ticketType)
) : (
<Tooltip
text="Unable to fetch JIRA ticket details"
position="left"
withPointer={false}
>
<div className={styles['alert-icon']}>
{getTicketIcon(ticketType)}
</div>
</Tooltip>
)}
<div onClick={handleClick} className={styles['hyperlink']}>
{tickerName}
</div>
<div className={styles['go-to-link-icon']}>
<GoToLinkIcon color={styles.goToLinkColor} />
</div>
</div>
</div>
);
};
export default TicketNameCell;

View File

@@ -0,0 +1,16 @@
export type FetchJiraDataProps = {
filterQuery?: string;
isDrawer?: boolean;
};
export type JiraTableRow = {
incidentID: number;
incidentName: string;
jiraKey: string;
jiraLink: string;
jiraType: string;
jiraSummary: string;
jiraStatus: string;
teamsInvolved: string[];
jiraCreateAt: string;
};

View File

@@ -0,0 +1,64 @@
import React, { FC } from 'react';
import { IconProps } from '@navi/web-ui/lib/icons/types';
const JiraDashboardIcon: FC<IconProps> = () => {
return (
<svg
width="23"
height="24"
viewBox="0 0 23 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="Icon">
<path
id="Vector"
d="M11.2328 24.0001C13.504 21.6571 13.504 17.8893 11.2328 15.5463L3.71358 7.78906L0.276217 11.3352C-0.0920724 11.7151 -0.0920724 12.3167 0.276217 12.665L11.2328 24.0001Z"
fill="white"
/>
<path
id="Vector_2"
d="M22.2199 11.3351L11.2326 0L11.2019 0.0316627C8.96148 2.37466 8.96148 6.14246 11.2326 8.4538L18.7825 16.211L22.2199 12.6648C22.5882 12.285 22.5882 11.6833 22.2199 11.3351Z"
fill="white"
/>
<path
id="Vector_3"
d="M11.2329 8.45336C8.99249 6.14204 8.9618 2.37424 11.2022 0.03125L3.40674 8.1051L7.48861 12.3161L11.2329 8.45336Z"
fill="url(#paint0_linear_2040_73334)"
/>
<path
id="Vector_4"
d="M15.0074 11.6523L11.2324 15.5468C13.5035 17.8898 13.5035 21.6577 11.2324 24.0006L19.0892 15.8951L15.0074 11.6523Z"
fill="url(#paint1_linear_2040_73334)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_2040_73334"
x1="11.0622"
y1="4.41341"
x2="6.16608"
y2="9.1593"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.0680226" stopColor="white" stopOpacity="0.4" />
<stop offset="1" stopColor="white" />
</linearGradient>
<linearGradient
id="paint1_linear_2040_73334"
x1="11.5437"
y1="19.4554"
x2="16.9052"
y2="14.2584"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.0680226" stopColor="white" stopOpacity="0.4" />
<stop offset="0.9077" stopColor="white" />
</linearGradient>
</defs>
</svg>
);
};
export default JiraDashboardIcon;

View File

@@ -0,0 +1,35 @@
import { FC } from 'react';
import { IconProps } from '../types';
const BugIcon: FC<IconProps> = ({
width = '16',
height = '16',
...restProps
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<g clipPath="url(#clip0_3026_2901)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 0H14C14.5304 0 15.0391 0.210714 15.4142 0.585786C15.7893 0.960859 16 1.46957 16 2V14C16 14.5304 15.7893 15.0391 15.4142 15.4142C15.0391 15.7893 14.5304 16 14 16H2C1.46957 16 0.960859 15.7893 0.585786 15.4142C0.210714 15.0391 0 14.5304 0 14V2C0 1.46957 0.210714 0.960859 0.585786 0.585786C0.960859 0.210714 1.46957 0 2 0V0ZM8 12C9.06087 12 10.0783 11.5786 10.8284 10.8284C11.5786 10.0783 12 9.06087 12 8C12 6.93913 11.5786 5.92172 10.8284 5.17157C10.0783 4.42143 9.06087 4 8 4C6.93913 4 5.92172 4.42143 5.17157 5.17157C4.42143 5.92172 4 6.93913 4 8C4 9.06087 4.42143 10.0783 5.17157 10.8284C5.92172 11.5786 6.93913 12 8 12Z"
fill="#FF5630"
/>
</g>
<defs>
<clipPath id="clip0_3026_2901">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
};
export default BugIcon;

View File

@@ -0,0 +1,35 @@
import { FC } from 'react';
import { IconProps } from '../types';
const EpicIcon: FC<IconProps> = ({
width = '16',
height = '16',
...restProps
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<g clipPath="url(#clip0_3026_2852)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14 0C15.1046 0 16 0.89543 16 2V14C16 15.1046 15.1046 16 14 16H2C0.89543 16 0 15.1046 0 14V2C0 0.89543 0.89543 0 2 0H14ZM8.5 3C8.35 3 8.22 3.069 8.128 3.173L4.149 8.145C4.146 8.148 4.113 8.19 4.113 8.19C4.047 8.279 4 8.382 4 8.5C4 8.776 4.224 9 4.5 9C4.528 9 4.551 9.005 4.577 9H7V12.5C7 12.776 7.224 13 7.5 13C7.624 13 7.734 12.95 7.821 12.876C7.821 12.876 7.883 12.809 7.906 12.776L11.84 7.863C11.859 7.845 11.871 7.824 11.887 7.803L11.914 7.77C11.963 7.689 12 7.6 12 7.5C12 7.224 11.776 7 11.5 7H9V3.5C9 3.224 8.776 3 8.5 3Z"
fill="#6554C0"
/>
</g>
<defs>
<clipPath id="clip0_3026_2852">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
};
export default EpicIcon;

View File

@@ -0,0 +1,35 @@
import { FC } from 'react';
import { IconProps } from '../types';
const StoryIcon: FC<IconProps> = ({
width = '16',
height = '16',
...restProps
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<g clipPath="url(#clip0_2996_8110)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 0H14C14.5304 0 15.0391 0.210714 15.4142 0.585786C15.7893 0.960859 16 1.46957 16 2V14C16 14.5304 15.7893 15.0391 15.4142 15.4142C15.0391 15.7893 14.5304 16 14 16H2C1.46957 16 0.960859 15.7893 0.585786 15.4142C0.210714 15.0391 0 14.5304 0 14V2C0 1.46957 0.210714 0.960859 0.585786 0.585786C0.960859 0.210714 1.46957 0 2 0V0ZM8 11L5.137 12.822C4.717 13.202 4 12.933 4 12.395V4.205C4 3.54 4.596 3 5.333 3H10.667C11.403 3 12 3.539 12 4.206V12.396C12 12.933 11.281 13.202 10.861 12.822L8 11ZM8 8.629L10 9.903V5H6V9.902L8 8.63V8.629Z"
fill="#36B37E"
/>
</g>
<defs>
<clipPath id="clip0_2996_8110">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
};
export default StoryIcon;

View File

@@ -0,0 +1,35 @@
import { FC } from 'react';
import { IconProps } from '../types';
const SubTaskIcon: FC<IconProps> = ({
width = '16',
height = '16',
...restProps
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<g clipPath="url(#clip0_3026_1398)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9 7V4C9 3.73478 8.89464 3.48043 8.70711 3.29289C8.51957 3.10536 8.26522 3 8 3H4C3.73478 3 3.48043 3.10536 3.29289 3.29289C3.10536 3.48043 3 3.73478 3 4V8C3 8.26522 3.10536 8.51957 3.29289 8.70711C3.48043 8.89464 3.73478 9 4 9H7V12C7 12.2652 7.10536 12.5196 7.29289 12.7071C7.48043 12.8946 7.73478 13 8 13H12C12.2652 13 12.5196 12.8946 12.7071 12.7071C12.8946 12.5196 13 12.2652 13 12V8C13 7.73478 12.8946 7.48043 12.7071 7.29289C12.5196 7.10536 12.2652 7 12 7H9ZM0 1.994C0 0.893 0.895 0 1.994 0H14.006C15.107 0 16 0.895 16 1.994V14.006C15.9997 14.5348 15.7896 15.0418 15.4157 15.4157C15.0418 15.7896 14.5348 15.9997 14.006 16H1.994C1.46524 15.9997 0.958212 15.7896 0.584322 15.4157C0.210432 15.0418 0.000264976 14.5348 0 14.006L0 1.994ZM9 9H11V11H9V9ZM5 5H7V7H5V5Z"
fill="#2684FF"
/>
</g>
<defs>
<clipPath id="clip0_3026_1398">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
};
export default SubTaskIcon;

View File

@@ -0,0 +1,35 @@
import { FC } from 'react';
import { IconProps } from '../types';
const TaskIcon: FC<IconProps> = ({
width = '16',
height = '16',
...restProps
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<g clipPath="url(#clip0_3026_4498)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 1.994C0 0.893 0.895 0 1.994 0H14.006C15.107 0 16 0.895 16 1.994V14.006C15.9997 14.5348 15.7896 15.0418 15.4157 15.4157C15.0418 15.7896 14.5348 15.9997 14.006 16H1.994C1.46524 15.9997 0.958212 15.7896 0.584322 15.4157C0.210432 15.0418 0.000264976 14.5348 0 14.006L0 1.994ZM4.667 3C3.747 3 3 3.746 3 4.667V11.333C3 12.253 3.746 13 4.667 13H11.333C12.253 13 13 12.254 13 11.333V4.667C13 3.747 12.254 3 11.333 3H4.667ZM5 5V11H11V5H5Z"
fill="#2684FF"
/>
</g>
<defs>
<clipPath id="clip0_3026_4498">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
};
export default TaskIcon;

View File

@@ -0,0 +1,33 @@
import { FC } from 'react';
import { IconProps } from '../types';
const TechTaskIcon: FC<IconProps> = ({
width = '16',
height = '16',
...restProps
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M13.7143 0H2.28571C1.02335 0 0 1.02335 0 2.28571V13.7143C0 14.9767 1.02335 16 2.28571 16H13.7143C14.9767 16 16 14.9767 16 13.7143V2.28571C16 1.02335 14.9767 0 13.7143 0Z"
fill="#9DA8B5"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.4284 8.0001C11.4284 9.89382 9.89357 11.4287 7.99986 11.4287C6.10615 11.4287 4.57129 9.89382 4.57129 8.0001C4.57129 6.10639 6.10615 4.57153 7.99986 4.57153C9.89357 4.57153 11.4284 6.10639 11.4284 8.0001Z"
stroke="white"
strokeWidth="1.5"
/>
</svg>
);
};
export default TechTaskIcon;

7
src/assets/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export { default as GoToLinkIcon } from './GoToLinkIcon';
export { default as StoryIcon } from './JiraIcons/StoryIcon';
export { default as SubTaskIcon } from './JiraIcons/SubTaskIcon';
export { default as TaskIcon } from './JiraIcons/TaskIcon';
export { default as EpicIcon } from './JiraIcons/EpicIcon';
export { default as BugIcon } from './JiraIcons/BugIcon';
export { default as TechTaskIcon } from './JiraIcons/TechTaskIcon';

View File

@@ -123,6 +123,8 @@ const FilterVerticalTabs: FC<FilterVerticalTabsProps> = ({
}
selectedFilters.forEach(([key, value]) => {
searchParams.set(key, [value].toString());
searchParams.set('page_number', '0');
searchParams.set('page_size', '10');
});
handleFetchData(searchParams.toString());
getSelectedFilterMap(selectedFiltersMap);
@@ -227,6 +229,8 @@ const FilterVerticalTabs: FC<FilterVerticalTabsProps> = ({
searchParams.delete('team_ids');
searchParams.delete('statuses');
searchParams.delete('severity_ids');
searchParams.set('page_number', '0');
searchParams.set('page_size', '10');
handleFetchData(searchParams.toString());
};

View File

@@ -10,11 +10,11 @@ import LeadIcon from '@navi/web-ui/lib/icons/LeadIcon';
import { NavItemType } from '@navi/web-ui/lib/components/Navbar/types';
import { Typography } from '@navi/web-ui/lib/primitives';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import JiraDashboardIcon from '@src/assets/JiraDashboardIcon';
import Dialog from '../Dialog';
import styles from './LeftNav.module.scss';
import Footer from '../Footer';
import GroupIcon from '../../assets/GroupIcon';
interface LeftNavProps {
children?: React.ReactNode;
}
@@ -68,6 +68,13 @@ const LeftNav: React.FC<LeftNavProps> = ({ children }) => {
Icon: GroupIcon,
handleNavigation: () => navigate('/metrics'),
},
{
itemType: 'simpleNavItem',
label: 'Jira dashboard',
route: '/jiraDashboard',
Icon: JiraDashboardIcon,
handleNavigation: () => navigate('/jiraDashboard'),
},
];
const returnUserData = () => {

View File

@@ -5,6 +5,7 @@ const Incident = lazy(() => import('./Pages/Incidents/index'));
const Team = lazy(() => import('./Pages/Team/index'));
const Severity = lazy(() => import('./Pages/Severity/index'));
const Tableau = lazy(() => import('./Pages/Tableau/index'));
const JiraDashboard = lazy(() => import('./Pages/JiraDashboard/index'));
import { CustomRouteObject } from './types';
const routes: CustomRouteObject[] = [
@@ -33,6 +34,11 @@ const routes: CustomRouteObject[] = [
path: '/metrics',
element: <Tableau />,
},
{
id: 'JIRA_DASHBOARD',
path: '/jiraDashboard',
element: <JiraDashboard />,
},
];
export default routes;

View File

@@ -24,7 +24,7 @@ export const convertToCamelCase = (input: string): string => {
export const returnFormattedDate = (date: any): string => {
if (!date) {
return '-';
return '';
}
const formattedDate = new Date(date);
return `${formattedDate?.toLocaleDateString()} ${formattedDate?.toLocaleTimeString()}`;

View File

@@ -0,0 +1,52 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { JiraDashboardState, JiraDashboardData, PageDetails } from '@src/types';
type SetJiraDashboardDataAction = PayloadAction<JiraDashboardData[]>;
type SetPageDetailsAction = PayloadAction<PageDetails>;
type SetIsLoadingAction = PayloadAction<boolean>;
type SetCurrentPageNumberAction = PayloadAction<number>;
type SetCurrentPageSizeAction = PayloadAction<number>;
const initialState: JiraDashboardState = {
data: [],
pageDetails: {
pageNumber: 0,
pageSize: 10,
totalElements: 0,
},
isLoading: false,
currentPageNumber: 0,
currentPageSize: 10,
};
const jiraDashboardSlice = createSlice({
name: 'jiraDashboard',
initialState,
reducers: {
setJiraDashboardData: (state, action: SetJiraDashboardDataAction) => {
state.data = action.payload;
},
setPageDetails: (state, action: SetPageDetailsAction) => {
state.pageDetails = action.payload;
},
setIsLoading: (state, action: SetIsLoadingAction) => {
state.isLoading = action.payload;
},
setCurrentPageNumber: (state, action: SetCurrentPageNumberAction) => {
state.currentPageNumber = action.payload;
},
setCurrentPageSize: (state, action: SetCurrentPageSizeAction) => {
state.currentPageSize = action.payload;
},
},
});
export const {
setJiraDashboardData,
setPageDetails,
setIsLoading,
setCurrentPageNumber,
setCurrentPageSize,
} = jiraDashboardSlice.actions;
export default jiraDashboardSlice.reducer;

View File

@@ -2,11 +2,13 @@ import { configureStore } from '@reduxjs/toolkit';
import teamReducer from '../slices/teamSlice';
import severityReducer from '../slices/sevSlice';
import dashboardReducer from '../slices/dashboardSlice';
import jiraDashboardReducer from '../slices/jiraDashboardSlice';
const store = configureStore({
reducer: {
team: teamReducer,
severity: severityReducer,
dashboard: dashboardReducer,
jiraDashboard: jiraDashboardReducer,
},
});

30
src/types/index.d.ts vendored
View File

@@ -23,3 +23,33 @@ export interface CustomRouteObject {
element: JSX.Element;
}
declare module '*.module.scss';
export interface JiraDashboardData {
incidentID: number;
incidentName: string;
jiraKey: string;
jiraLink: string;
jiraType: string;
jiraSummary: string;
jiraStatus: string;
teamsInvolved: string[];
jiraCreateAt: string;
}
export interface JiraDashboardState {
data: JiraDashboardData[];
pageDetails: {
pageNumber: number;
pageSize: number;
totalElements: number;
};
isLoading: boolean;
currentPageNumber: number;
currentPageSize: number;
}
export interface PageDetails {
pageNumber: number;
pageSize: number;
totalElements: number;
}