TP-42065 : UI for add team oncall and psec oncall, show team oncall, psec oncall, removal of user type, separation of update types, and other update team UI changes (#64)

* TP-4206 : API integration for add psec oncall and dev oncall

* TP-42065 : Dropdown UI for team oncall and psec oncall

* TP-42065 : Dropdown UI for team oncall and psec oncall

* Move bot fetching to page level from componenet

* Minor changes

* Updated update team ui

* Move bots fetching call to a separate file

* Remove uncesessary props

* PR Review Fixes

* Remove changes from other branch

* Build failure fix and Api timing issue fix

* Design signoff changes

* Changes after second design review

* Changes after third design review

* PR Review Changes

* More PR reviews and rebase
This commit is contained in:
Vijay Joshi
2023-10-20 13:04:48 +05:30
committed by GitHub
parent b0cc0bd97a
commit b18a1314db
10 changed files with 350 additions and 105 deletions

0
package-lock.json generated Normal file
View File

View File

@@ -33,7 +33,7 @@
a {
text-decoration: none;
color: #24b4d2;
color: var(--navi-color-blue-base);
}
}

View File

@@ -21,6 +21,15 @@
}
.custom-bordered-input {
margin-top: 20px;
display: block;
}
.info-icon {
margin: -2.5px 4px;
}
.slack-details-header {
margin-bottom: 18px;
}
.custom-textarea {
@@ -35,6 +44,40 @@
padding: 6px 0;
}
.slack-details-wrapper {
margin-top: 16px;
}
.slack-property-name {
display: inline;
width: 50%;
}
.slack-channel-wrapper {
float: right;
text-align: right;
}
.channel-name-detail {
float: right;
text-align: right;
}
.slack-property-detail {
display: inline;
width: 50%;
float: right;
text-align: right;
color: black;
font-weight: 500;
line-height: 20px;
word-wrap: break-word;
}
.slack-channel-content {
text-align: right;
}
.team-name-wrapper {
display: flex;
align-items: center;
@@ -47,7 +90,10 @@
.update-team-btn {
width: fit-content;
margin-top: 12px;
border: 1px solid var(--navi-color-blue-base);
color: var(--navi-color-blue-base);
float: right;
margin: 12px 0px;
}
.create-team-btn-wrapper {
padding: 6px 0 12px 0px;
@@ -64,7 +110,13 @@
.vertical-line {
transform: rotateX(180deg);
border: 1px solid #e8e8e8;
border: 0.5px solid var(--navi-color-gray-border);
}
.horizontal-line {
height: 1px;
background: var(--navi-color-gray-border);
margin-top: 50px;
}
.loader-container {
@@ -76,13 +128,27 @@
.accordion-header {
display: flex;
align-items: center;
justify-content: space-between;
height: fit-content;
width: 100%;
.title {
padding: 12px;
.team-name {
margin: 16px 3px;
font-weight: 400;
font-size: 16px;
line-height: 18px;
}
.updated-at {
border: 1px solid var(--navi-color-gray-border);
border-radius: 8px;
padding: 2px 8px;
margin-right: 8px;
}
}
.add-member-btn {
border: 1px solid var(--navi-color-blue-base);
color: var(--navi-color-blue-base);
float: right;
margin: 12px 0px;
}
.team-header-wrapper {
@@ -122,3 +188,13 @@
margin-bottom: 20px;
gap: 0 8px;
}
.team-input::placeholder {
color: var(--navi-color-navigation-blue-c2);
}
.oncall-selector {
width: 100%;
margin-top: 8px;
height: 38px;
}

23
src/Pages/Team/bots.ts Normal file
View File

@@ -0,0 +1,23 @@
import { ApiService } from '@src/services/api';
import { FETCH_ALL_BOTS_DATA, PickerOptionProps } from './constants';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
let bots: Array<PickerOptionProps> = [];
export const getBots = (): Array<PickerOptionProps> => bots;
export const fetchAllBots = (): void => {
ApiService.get(FETCH_ALL_BOTS_DATA)
.then(response => {
bots = [];
response?.data?.data.map(data => {
bots.push({
label: '@' + data.name,
value: data.id,
});
});
})
.catch(error => {
toast.error(error?.message);
});
};

View File

@@ -1,4 +1,5 @@
export const FETCH_TEAM_DATA = `${window?.config?.BASE_API_URL}/teams`;
export const FETCH_ALL_BOTS_DATA = `${window?.config?.BASE_API_URL}/houston/bots`;
export const CREATE_TEAM = `${window?.config?.BASE_API_URL}/houston/teams/add`;
export const FETCH_SINGLE_TEAM_DATA = (payload: string): string => {
@@ -29,6 +30,11 @@ export interface TeamsData {
expand: boolean;
}
export interface BotsData {
id: string;
name: string;
}
export interface TeamFormProps {
teamId: string;
isExpanded: boolean;
@@ -56,3 +62,8 @@ export interface CreateTeamProps {
startTeamSearch: (id: number) => void;
setOpen: (open: boolean) => void;
}
export interface PickerOptionProps {
label: string;
value: string;
}

View File

@@ -10,6 +10,7 @@ import styles from './Team.module.scss';
import Button from '@navi/web-ui/lib/primitives/Button';
import { AddIcon } from '@navi/web-ui/lib/icons';
import { useAuthData } from './Hook';
import { fetchAllBots } from './bots';
const Team: FC = () => {
const [data, setData] = useState<any>([]);
@@ -50,6 +51,7 @@ const Team: FC = () => {
};
useEffect(() => {
fetchAllBots();
startTeamSearch();
}, []);

View File

@@ -3,31 +3,36 @@ import { useEffect, useState } from 'react';
import LoadingIcon from '@navi/web-ui/lib/icons/LoadingIcon';
import Button from '@navi/web-ui/lib/primitives/Button';
import { toast } from '@navi/web-ui/lib/primitives/Toast';
import { BorderedInput, Typography } from '@navi/web-ui/lib/primitives';
import { Filter } from '@navi/web-ui/lib/components';
import SlackIcon from '@src/assets/SlackIcon';
import {
BorderedInput,
Tooltip,
Typography,
} from '@navi/web-ui/lib/primitives';
import { InfoIcon } from '@navi/web-ui/lib/icons';
import {
FETCH_SINGLE_TEAM_DATA,
TeamFormProps,
UPDATE_TEAM_DATA,
slackUserOptions,
userInputPlaceholders,
} from '../constants';
import { ApiService } from '@src/services/api';
import MembersDetails from '@src/components/MembersDetails';
import DrawerStyles from '@src/Pages/Incidents/DrawerMode/DrawerMode.module.scss';
import styles from '../Team.module.scss';
import { ConfigProvider, Select, ThemeConfig } from 'antd';
import { getBots } from '../bots';
import SlackIcon from '@src/assets/SlackIcon';
const TeamForm = (props: TeamFormProps) => {
const { teamId, isExpanded, setTeamId, setLastUpdatedAt } = props;
const [data, setData] = useState<any>({});
const [slackUserIds, setSlackUserIds] = useState('');
const [slackUserEmails, setSlackUserEmails] = useState('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [slackChannelId, setSlackChannelId] = useState<string>('');
const [userType, setUserType] = useState<any>('Email-id');
const [updateloading, setUpdateloading] = useState<boolean>(false);
const [oncallHandle, setOncallHandle] = useState<string>('');
const [psecOncallHandle, setPsecOncallHandle] = useState<string>('');
const [showTooltip, setShowTooltip] = useState<boolean>(false);
const fetchTeamById = (updateDate = false, showMainLoader = true) => {
const endPoint = FETCH_SINGLE_TEAM_DATA(teamId);
@@ -58,29 +63,39 @@ const TeamForm = (props: TeamFormProps) => {
}
}, [teamId, isExpanded]);
const submitHandler = () => {
const addMemberHandler = (): void => {
const endPoint = UPDATE_TEAM_DATA();
const slackIds =
data?.participants?.map(participant => participant?.id) || [];
const finalSlackData = slackUserIds?.includes(',')
? slackUserIds.split(',').map(item => item?.trim())
: [slackUserIds];
const userIds =
userType === slackUserOptions[0].label
? {
workEmailIds: finalSlackData,
}
: {
slackUserIds: finalSlackData,
};
const finalSlackData = slackUserEmails?.includes(',')
? slackUserEmails.split(',').map(item => item?.trim())
: [slackUserEmails];
ApiService.post(endPoint, {
id: teamId,
...userIds,
webhook_slack_channel: slackChannelId,
workEmailIds: finalSlackData,
})
.then(response => {
toast.success(response?.data?.data);
fetchTeamById(true);
})
.catch(error => {
const toastMessage = `${
error?.response?.data?.error?.message
? `${error?.response?.data?.error?.message}`
: ''
}`;
toast.error(toastMessage);
});
};
const submitHandler = (): void => {
const endPoint = UPDATE_TEAM_DATA();
ApiService.post(endPoint, {
id: teamId,
webhook_slack_channel: slackChannelId,
on_call_handle: oncallHandle,
pse_on_call_id: psecOncallHandle,
})
.then(response => {
toast.success(response?.data?.data);
setUpdateloading(true);
fetchTeamById(true, false);
})
.catch(error => {
@@ -93,35 +108,58 @@ const TeamForm = (props: TeamFormProps) => {
});
};
const getSubmitButtonState = (): boolean => {
if (slackUserIds.length) {
return !slackUserIds.length;
}
return !slackChannelId.length;
const filterOption = (input: string, option): boolean =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase());
const dropdownTheme: ThemeConfig = {
token: {
colorTextPlaceholder: 'var(--navi-color-navigation-blue-c2)',
borderRadius: 8,
colorBorder: 'var(--navi-bordered-input-input-wrapper-border-default)',
fontSize: 14,
},
};
const handleFilterChange = option => {
setUserType(option?.label);
const getSubmitButtonState = (): boolean => {
if (oncallHandle?.length) {
return !oncallHandle.length;
}
if (psecOncallHandle?.length) {
return !psecOncallHandle.length;
}
return !slackChannelId?.length;
};
const getAddMemberButtonState = (): boolean => {
return !slackUserEmails.length;
};
const returnSlackChannel = () => {
return data?.webhookSlackChannelName && data?.webhookSlackChannelId ? (
<div className="slack-channel-wrapper">
<Typography variant="p4" className={DrawerStyles['slack-channel']}>
<SlackIcon />
return data?.webhookSlackChannelName != 'not found' &&
data?.webhookSlackChannelId ? (
<div className={styles['slack-channel-wrapper']}>
<Typography
variant="p4"
className={
DrawerStyles['slack-channel'] + ' ' + styles['channel-name-detail']
}
>
<SlackIcon width={16} height={16} />
<div className="slack-channel-content">
<a
href={`https://go-navi.slack.com/archives/${data?.webhookSlackChannelId}`}
target="_blank"
rel="noreferrer"
>
{data?.webhookSlackChannelName}
{'#' + data?.webhookSlackChannelName}
</a>
</div>
</Typography>
</div>
) : (
'-'
<Typography variant="p3" className={styles['slack-property-detail']}>
-
</Typography>
);
};
@@ -138,18 +176,57 @@ const TeamForm = (props: TeamFormProps) => {
<div className={styles['team-form-wrapper']}>
<div className={styles['content-wrapper']}>
<div className={styles['custom-bordered-input']}>
<Typography
variant="p3"
color="#585757"
style={{ marginBottom: '12px' }}
>
Slack Channel Name:
<Typography variant="p4" color="var(--navi-color-gray-c3)">
Slack details
</Typography>
{returnSlackChannel()}
<div className={styles['slack-details-wrapper']}>
<Typography
variant="p4"
color="var(--navi-color-gray-c2)"
className={styles['slack-property-name']}
>
Channel
</Typography>
{returnSlackChannel()}
</div>
<div className={styles['slack-details-wrapper']}>
<Typography
variant="p4"
color="var(--navi-color-gray-c2)"
className={styles['slack-property-name']}
>
Team on call
</Typography>
<Typography
variant="p3"
className={styles['slack-property-detail']}
>
{data?.oncall && data?.oncall?.name != ''
? '@' + data?.oncall?.name
: '-'}
</Typography>
</div>
<div className={styles['slack-details-wrapper']}>
<Typography
variant="p4"
color="var(--navi-color-gray-c2)"
className={styles['slack-property-name']}
>
PSEC on call
</Typography>
<Typography
variant="p3"
className={styles['slack-property-detail']}
>
{data?.pse_oncall && data?.pse_oncall?.name != ''
? '@' + data?.pse_oncall?.name
: '-'}
</Typography>
</div>
</div>
<div className={styles['custom-bordered-input']}>
<Typography variant="p3" color="#585757">
Members:
<Typography variant="p4" color="var(--navi-color-gray-c3)">
Members
</Typography>
<MembersDetails data={data} fetchTeamById={fetchTeamById} />
@@ -157,56 +234,111 @@ const TeamForm = (props: TeamFormProps) => {
</div>
<hr className={styles['vertical-line']} />
<div className={styles['content-wrapper']}>
{updateloading && (
<div className={styles['loader']}>
<LoadingIcon size="md" />
</div>
)}
<div className={styles['custom-bordered-input']}>
<Typography variant="p3" color="#585757">
Add members
<Typography variant="p3" color="var(--navi-color-gray-c2)">
Add member(s)
</Typography>
<div className={styles['filter-wrapper']}>
<Typography variant="p4" color="#585757">
Choose user type:
</Typography>
<Filter
title={userType}
options={slackUserOptions}
onSelectionChange={handleFilterChange}
filterClass={styles['filter-container']}
isSingleSelect
/>
</div>
<BorderedInput
inputSize="medium"
placeholder={`Please add ${userInputPlaceholders[userType]} here`}
onChange={e => setSlackUserIds(e.target.value)}
hintMsg="Please enter the values separated by commas."
disabled={!userType.length}
placeholder={'Enter email ids'}
onChange={e => setSlackUserEmails(e.target.value)}
hintMsg="Please enter the emails separated by commas."
fullWidth
className={styles['team-input']}
/>
<Button
variant="secondary"
className={styles['add-member-btn']}
disabled={getAddMemberButtonState()}
onClick={addMemberHandler}
>
Add member(s)
</Button>
<br></br>
</div>
<div className={styles['horizontal-line']} />
<div className={styles['custom-bordered-input']}>
<Typography
variant="p4"
color="var(--navi-color-gray-c3)"
className={styles['slack-details-header']}
>
Slack details
</Typography>
<Typography variant="p4" color="var(--navi-color-gray-c2)">
Channel I.D.
<InfoIcon
onMouseEnter={() => {
setShowTooltip(true);
}}
onMouseLeave={() => {
setShowTooltip(false);
}}
className={styles['info-icon']}
/>
<Tooltip
text="Slack channel I.D. can be found at the bottom of
the channel details window"
position="right"
showTooltip={showTooltip}
/>
<BorderedInput
inputLabel=""
inputSize="medium"
placeholder="Please add the slack channel id here."
onChange={e => setSlackChannelId(e.target?.value)}
fullWidth
className={styles['team-input']}
/>
</Typography>
</div>
<div className={styles['custom-bordered-input']}>
<BorderedInput
inputLabel="Add slack details"
inputSize="medium"
placeholder="Please add the slack channel id here."
onChange={e => setSlackChannelId(e.target?.value)}
hintMsg="Add the slack channel to get incident updates."
fullWidth
/>
<Typography variant="p4" color="var(--navi-color-gray-c2)">
Team on call
</Typography>
<ConfigProvider theme={dropdownTheme}>
<Select
showSearch
placeholder="@oncall_handle"
optionFilterProp="children"
filterOption={filterOption}
options={getBots()}
onChange={e => setOncallHandle(e)}
className={styles['oncall-selector']}
allowClear
/>
</ConfigProvider>
</div>
<div className={styles['custom-bordered-input']}>
<Typography variant="p4" color="var(--navi-color-gray-c2)">
PSEC on call
</Typography>
<ConfigProvider theme={dropdownTheme}>
<Select
showSearch
placeholder="@psec_oncall_handle"
optionFilterProp="children"
filterOption={filterOption}
options={getBots()}
onChange={e => setPsecOncallHandle(e)}
className={styles['oncall-selector']}
allowClear
/>
</ConfigProvider>
</div>
<div className={styles['update-team-btn-wrapper']}>
<Button
fullWidth
variant="primary"
variant="secondary"
onClick={submitHandler}
className={styles['update-team-btn']}
disabled={getSubmitButtonState()}
>
Update team details
Update details
</Button>
</div>
</div>

View File

@@ -32,18 +32,16 @@ const TeamResultsTable = (props: TeamResultsTableProps) => {
expanded={team?.expand || team.id === teamId}
header={
<div className={styles['accordion-header']}>
<Typography variant="h4" className={styles['title']}>
<Typography variant="h4" className={styles['team-name']}>
{team?.name}
</Typography>
{team?.lastUpdatedAt && (
<Typography
variant="p5"
className={styles['title']}
color="#585858"
>
Updated At: {returnDate(team?.lastUpdatedAt)}
</Typography>
)}
<Typography
variant="h4"
className={styles['team-name']}
color="#969696"
>
({team?.slackUserIds?.length})
</Typography>
</div>
}
>

View File

@@ -13,6 +13,7 @@
.participant-detail-nest {
display: flex;
margin-right: 10px;
}
.remove-logo {

View File

@@ -165,9 +165,9 @@ const MembersDetails = (props: any) => {
</div>
<div className={styles['participant-detail']}>
<div className={styles['participant-detail-nest']}>
{showRemoveButton &&
!(participant.email === managerEmail) && (
{showRemoveButton &&
!(participant.email === managerEmail) && (
<div className={styles['participant-detail-nest']}>
<div className={styles.personIcon}>
<Tooltip
position="left"
@@ -184,12 +184,14 @@ const MembersDetails = (props: any) => {
/>
</Tooltip>
</div>
)}
</div>
</div>
)}
<div className={styles['participant-detail-nest']}>
{participant.email === managerEmail && <ManagerIcon />}
</div>
{participant.email === managerEmail && (
<div className={styles['participant-detail-nest']}>
<ManagerIcon />
</div>
)}
{showRemoveButton &&
!(participant.email === managerEmail) && (