Merge branch 'master' into TP-53680
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
73
src/Pages/JiraDashboard/Dashboard.module.scss
Normal file
73
src/Pages/JiraDashboard/Dashboard.module.scss
Normal 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;
|
||||
}
|
||||
59
src/Pages/JiraDashboard/SearchResultsTable.module.scss
Normal file
59
src/Pages/JiraDashboard/SearchResultsTable.module.scss
Normal 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);
|
||||
}
|
||||
11
src/Pages/JiraDashboard/constants.ts
Normal file
11
src/Pages/JiraDashboard/constants.ts
Normal 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}`;
|
||||
};
|
||||
130
src/Pages/JiraDashboard/index.tsx
Normal file
130
src/Pages/JiraDashboard/index.tsx
Normal 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;
|
||||
50
src/Pages/JiraDashboard/partials/DashboardHeader.module.scss
Normal file
50
src/Pages/JiraDashboard/partials/DashboardHeader.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
45
src/Pages/JiraDashboard/partials/DashboardHeader.tsx
Normal file
45
src/Pages/JiraDashboard/partials/DashboardHeader.tsx
Normal 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;
|
||||
32
src/Pages/JiraDashboard/partials/HyperlinkCellRenderer.tsx
Normal file
32
src/Pages/JiraDashboard/partials/HyperlinkCellRenderer.tsx
Normal 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;
|
||||
172
src/Pages/JiraDashboard/partials/SearchResultsTable.tsx
Normal file
172
src/Pages/JiraDashboard/partials/SearchResultsTable.tsx
Normal 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;
|
||||
66
src/Pages/JiraDashboard/partials/SmartSearch.tsx
Normal file
66
src/Pages/JiraDashboard/partials/SmartSearch.tsx
Normal 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;
|
||||
@@ -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}>
|
||||
{remainingTeams.length} more
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return <div>{renderTeamNames()}</div>;
|
||||
};
|
||||
|
||||
export default TeamAssignedCellRenderer;
|
||||
75
src/Pages/JiraDashboard/partials/ticketNameCell.tsx
Normal file
75
src/Pages/JiraDashboard/partials/ticketNameCell.tsx
Normal 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;
|
||||
16
src/Pages/JiraDashboard/types.ts
Normal file
16
src/Pages/JiraDashboard/types.ts
Normal 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;
|
||||
};
|
||||
64
src/assets/JiraDashboardIcon.tsx
Normal file
64
src/assets/JiraDashboardIcon.tsx
Normal 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;
|
||||
35
src/assets/JiraIcons/BugIcon.tsx
Normal file
35
src/assets/JiraIcons/BugIcon.tsx
Normal 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;
|
||||
35
src/assets/JiraIcons/EpicIcon.tsx
Normal file
35
src/assets/JiraIcons/EpicIcon.tsx
Normal 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;
|
||||
35
src/assets/JiraIcons/StoryIcon.tsx
Normal file
35
src/assets/JiraIcons/StoryIcon.tsx
Normal 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;
|
||||
35
src/assets/JiraIcons/SubTaskIcon.tsx
Normal file
35
src/assets/JiraIcons/SubTaskIcon.tsx
Normal 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;
|
||||
35
src/assets/JiraIcons/TaskIcon.tsx
Normal file
35
src/assets/JiraIcons/TaskIcon.tsx
Normal 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;
|
||||
33
src/assets/JiraIcons/TechTaskIcon.tsx
Normal file
33
src/assets/JiraIcons/TechTaskIcon.tsx
Normal 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
7
src/assets/index.ts
Normal 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';
|
||||
@@ -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());
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()}`;
|
||||
|
||||
52
src/slices/jiraDashboardSlice.tsx
Normal file
52
src/slices/jiraDashboardSlice.tsx
Normal 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;
|
||||
@@ -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
30
src/types/index.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user