animate user timezones
This commit is contained in:
parent
1ce970aac2
commit
99877b99d2
15 changed files with 323 additions and 96 deletions
|
|
@ -13,6 +13,7 @@ module.exports = {
|
|||
'react/prop-types': 'warn',
|
||||
'react/display-name': 'warn',
|
||||
'react/jsx-key': 'warn',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/no-unescaped-entities': 'warn',
|
||||
'react/jsx-no-target-blank': 'warn',
|
||||
'no-restricted-imports': 'warn',
|
||||
|
|
|
|||
|
|
@ -4,19 +4,22 @@
|
|||
|
||||
.root table {
|
||||
width: 100%;
|
||||
background: #22252b;
|
||||
}
|
||||
|
||||
.root tr {
|
||||
border-bottom: 1px solid #33363b;
|
||||
border-bottom: 1px solid #181b1f;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.root tr:hover {
|
||||
background: var(--secondary-background);
|
||||
/* background: var(--secondary-background); */
|
||||
background: rgba(63, 62, 62, 0.45);
|
||||
}
|
||||
|
||||
.root td {
|
||||
min-height: 60px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
|
|
|
|||
|
|
@ -43,16 +43,43 @@
|
|||
height: 76px;
|
||||
}
|
||||
|
||||
.user {
|
||||
.avatar-group {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
border: 2px solid #c65210;
|
||||
transition: left 1s linear;
|
||||
transform: translate(-50%, 0);
|
||||
z-index: 0;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: opacity 200ms ease, left 200ms ease;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.is-oncall-icon {
|
||||
color: var(--oncall-icon-stroke-color);
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
.user-more {
|
||||
position: absolute;
|
||||
padding: 0 5px;
|
||||
bottom: 0;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
background: #454952;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
transition: opacity 200ms ease, left 200ms ease;
|
||||
}
|
||||
|
||||
.avatar-group_inactive {
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.time-stripe {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
|
|
@ -91,10 +118,6 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.time-mark {
|
||||
|
||||
}
|
||||
|
||||
.time-mark-text {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import dayjs from 'dayjs';
|
|||
import Avatar from 'components/Avatar/Avatar';
|
||||
import ScheduleUserDetails from 'components/ScheduleUserDetails/ScheduleUserDetails';
|
||||
import Text from 'components/Text/Text';
|
||||
import { IsOncallIcon } from 'icons';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ interface UsersTimezonesProps {
|
|||
users: User[];
|
||||
tz: Timezone;
|
||||
onTzChange: (tz: Timezone) => void;
|
||||
onCallNow: Array<Partial<User>>;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
|
@ -25,17 +27,11 @@ const hoursToSplit = 3;
|
|||
const jLimit = 24 / hoursToSplit;
|
||||
|
||||
const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
||||
const { users, tz, onTzChange } = props;
|
||||
const { users, tz, onTzChange, onCallNow } = props;
|
||||
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const [currentMoment, setCurrentMoment] = useState<dayjs.Dayjs>(dayjs().tz(tz));
|
||||
|
||||
const getAvatarClickHandler = useCallback((user) => {
|
||||
return () => {
|
||||
onTzChange(user.timezone);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentMoment(currentMoment.tz(tz).startOf('minute'));
|
||||
}, [tz]);
|
||||
|
|
@ -73,7 +69,7 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
|||
<div className={cx('header')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<div className={cx('title')}>Team timezones</div>
|
||||
<div className={cx('title')}>Schedule team and timezones</div>
|
||||
{/* <HorizontalGroup>
|
||||
<InlineSwitch transparent />
|
||||
Current schedule users only
|
||||
|
|
@ -88,32 +84,7 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
|||
</div>
|
||||
<div className={cx('users')}>
|
||||
<div className={cx('current-time')} style={{ left: `${currentTimeX}%` }} />
|
||||
{users.map((user, index) => {
|
||||
const userCurrentMoment = dayjs(currentMoment).tz(user.timezone);
|
||||
const diff = userCurrentMoment.diff(userCurrentMoment.startOf('day'), 'minutes');
|
||||
|
||||
const userHour = userCurrentMoment.hour();
|
||||
|
||||
const x = (diff / 1440) * 100;
|
||||
return (
|
||||
<Tooltip
|
||||
interactive
|
||||
key={index}
|
||||
content={<ScheduleUserDetails currentMoment={currentMoment} user={user} />}
|
||||
>
|
||||
<div
|
||||
className={cx('user')}
|
||||
onClick={getAvatarClickHandler(user)}
|
||||
style={{
|
||||
left: `${x}%`,
|
||||
opacity: userHour >= 9 && userHour < 18 ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
<Avatar src={user.avatar} size="large" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
<UserAvatars users={users} onCallNow={onCallNow} onTzChange={onTzChange} currentMoment={currentMoment} />
|
||||
</div>
|
||||
<div className={cx('time-stripe')}>
|
||||
<div className={cx('current-user-stripe')} />
|
||||
|
|
@ -138,4 +109,170 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface UserAvatarsProps {
|
||||
users: User[];
|
||||
currentMoment: dayjs.Dayjs;
|
||||
onTzChange: (timezone: Timezone) => void;
|
||||
onCallNow: Array<Partial<User>>;
|
||||
}
|
||||
|
||||
const UserAvatars = (props: UserAvatarsProps) => {
|
||||
const { users, currentMoment, onTzChange, onCallNow } = props;
|
||||
const userGroups = useMemo(() => {
|
||||
return users
|
||||
.reduce((memo, user) => {
|
||||
let group = memo.find((group) => group.timezone === user.timezone);
|
||||
if (!group) {
|
||||
group = { timezone: user.timezone, users: [] };
|
||||
memo.push(group);
|
||||
}
|
||||
group.users.push(user);
|
||||
|
||||
return memo;
|
||||
}, [])
|
||||
.sort((a, b) => {
|
||||
const aOffset = dayjs().tz(a.timezone).utcOffset();
|
||||
const bOffset = dayjs().tz(b.timezone).utcOffset();
|
||||
|
||||
if (aOffset > bOffset) {
|
||||
return 1;
|
||||
}
|
||||
if (aOffset < bOffset) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}, [users]);
|
||||
|
||||
const getAvatarClickHandler = useCallback((timezone: Timezone) => {
|
||||
return () => {
|
||||
onTzChange(timezone);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [activeTimezone, setActiveTimezone] = useState<Timezone | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<div className={cx('user-avatars')}>
|
||||
{userGroups.map((group) => {
|
||||
const userCurrentMoment = dayjs(currentMoment).tz(group.timezone);
|
||||
const diff = userCurrentMoment.diff(userCurrentMoment.startOf('day'), 'minutes');
|
||||
|
||||
const xPos = (diff / (60 * 24)) * 100;
|
||||
|
||||
return (
|
||||
<AvatarGroup
|
||||
activeTimezone={activeTimezone}
|
||||
timezone={group.timezone}
|
||||
onSetActiveTimezone={setActiveTimezone}
|
||||
onClick={getAvatarClickHandler(group.timezone)}
|
||||
xPos={xPos}
|
||||
users={group.users}
|
||||
currentMoment={currentMoment}
|
||||
onCallNow={onCallNow}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AvatarGroupProps {
|
||||
users: User[];
|
||||
xPos: number;
|
||||
currentMoment: dayjs.Dayjs;
|
||||
onClick: () => void;
|
||||
timezone: Timezone;
|
||||
onSetActiveTimezone: (timezone: Timezone) => void;
|
||||
activeTimezone: Timezone;
|
||||
onCallNow: Array<Partial<User>>;
|
||||
}
|
||||
|
||||
const LIMIT = 3;
|
||||
const AVATAR_WIDTH = 32;
|
||||
const AVATAR_GAP = 5;
|
||||
|
||||
const AvatarGroup = (props: AvatarGroupProps) => {
|
||||
const {
|
||||
users: propsUsers,
|
||||
currentMoment,
|
||||
xPos,
|
||||
onClick,
|
||||
timezone,
|
||||
onSetActiveTimezone,
|
||||
activeTimezone,
|
||||
onCallNow,
|
||||
} = props;
|
||||
|
||||
const active = activeTimezone && activeTimezone === timezone;
|
||||
|
||||
const translateLeft = -AVATAR_WIDTH / 2;
|
||||
|
||||
const users = useMemo(() => {
|
||||
return [...propsUsers].sort((a, b) => {
|
||||
const aIsOncall = Number(onCallNow.some((onCallUser) => a.pk === onCallUser.pk));
|
||||
const bIsOncall = Number(onCallNow.some((onCallUser) => b.pk === onCallUser.pk));
|
||||
|
||||
if (aIsOncall < bIsOncall) {
|
||||
return 1;
|
||||
}
|
||||
if (aIsOncall > bIsOncall) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}, [propsUsers]);
|
||||
|
||||
const width = active ? users.length * AVATAR_WIDTH + (users.length - 1) * AVATAR_GAP : AVATAR_WIDTH;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('avatar-group', { [`avatar-group_inactive`]: activeTimezone && activeTimezone !== timezone })}
|
||||
style={{ width: `${width + AVATAR_GAP}px`, left: `${xPos}%`, transform: `translate(${translateLeft}px, 0)` }}
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => onSetActiveTimezone(timezone)}
|
||||
onMouseLeave={() => onSetActiveTimezone(undefined)}
|
||||
>
|
||||
{users.map((user, index, array) => {
|
||||
const isOncall = onCallNow.some((onCallUser) => user.pk === onCallUser.pk);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
interactive
|
||||
key={index}
|
||||
content={<ScheduleUserDetails currentMoment={currentMoment} user={user} />}
|
||||
>
|
||||
<div
|
||||
className={cx('avatar')}
|
||||
style={{
|
||||
left: active ? `${index * (AVATAR_WIDTH + AVATAR_GAP)}px` : `${index * 10}px`,
|
||||
opacity: active ? 1 : Math.max(1 - index * 0.25, 0.25),
|
||||
visibility: !active && index >= LIMIT ? 'hidden' : 'visible',
|
||||
zIndex: array.length - index - 1,
|
||||
/* opacity: userHour >= 9 && userHour < 18 ? 1 : 0.5,*/
|
||||
}}
|
||||
>
|
||||
<Avatar src={user.avatar} size="large" />
|
||||
{isOncall && <IsOncallIcon className={cx('is-oncall-icon')} />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
style={{
|
||||
opacity: !active && users.length > LIMIT ? '1' : '0',
|
||||
zIndex: users.length,
|
||||
left: active ? `${users.length * (AVATAR_WIDTH + AVATAR_GAP)}px` : `${users.length * 10}px`,
|
||||
}}
|
||||
className={cx('user-more')}
|
||||
>
|
||||
+{users.length - LIMIT}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersTimezones;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ import React, { FC, useMemo, useState, useEffect, useRef, useCallback } from 're
|
|||
import { HorizontalGroup, LoadingPlaceholder } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { CSSTransitionGroup } from 'react-transition-group'; // ES6
|
||||
|
||||
import ScheduleSlot from 'components/ScheduleSlot/ScheduleSlot';
|
||||
import ScheduleSlot from 'containers/ScheduleSlot/ScheduleSlot';
|
||||
import { getFromString } from 'models/schedule/schedule.helpers';
|
||||
import { Rotation as RotationType, Schedule, Event } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
|
|
@ -20,6 +19,7 @@ const cx = cn.bind(styles);
|
|||
interface ScheduleSlotState {}
|
||||
|
||||
interface RotationProps {
|
||||
scheduleId: Schedule['id'];
|
||||
startMoment: dayjs.Dayjs;
|
||||
currentTimezone: Timezone;
|
||||
layerIndex?: number;
|
||||
|
|
@ -34,6 +34,7 @@ interface RotationProps {
|
|||
const Rotation: FC<RotationProps> = (props) => {
|
||||
const {
|
||||
events,
|
||||
scheduleId,
|
||||
layerIndex,
|
||||
rotationIndex,
|
||||
startMoment,
|
||||
|
|
@ -84,11 +85,6 @@ const Rotation: FC<RotationProps> = (props) => {
|
|||
|
||||
const dayOffset = Math.floor((x / width) * 7);
|
||||
|
||||
/* console.log('event.offsetX', event.offsetX);
|
||||
console.log('event.nativeEvent', event.nativeEvent);
|
||||
console.log('event.currentTarget', event.currentTarget);
|
||||
console.log('dayOffset', dayOffset);
|
||||
*/
|
||||
onClick(startMoment.add(dayOffset, 'day'));
|
||||
};
|
||||
|
||||
|
|
@ -125,6 +121,7 @@ const Rotation: FC<RotationProps> = (props) => {
|
|||
return (
|
||||
<ScheduleSlot
|
||||
index={index}
|
||||
scheduleId={scheduleId}
|
||||
key={event.start}
|
||||
event={event}
|
||||
layerIndex={layerIndex}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
classNames={{ ...styles }}
|
||||
>
|
||||
<Rotation
|
||||
scheduleId={scheduleId}
|
||||
onClick={(moment) => {
|
||||
this.onRotationClick(shiftId, moment);
|
||||
}}
|
||||
|
|
@ -147,6 +148,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
<TimelineMarks startMoment={startMoment} />
|
||||
<div className={cx('rotations')}>
|
||||
<Rotation
|
||||
scheduleId={scheduleId}
|
||||
onClick={(moment) => {
|
||||
this.handleAddLayer(nextPriority, moment);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import cn from 'classnames/bind';
|
|||
import dayjs from 'dayjs';
|
||||
import { toJS } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
|
||||
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
|
||||
import Rotation from 'containers/Rotation/Rotation';
|
||||
|
|
@ -14,6 +15,7 @@ import { Timezone } from 'models/timezone/timezone.types';
|
|||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
||||
import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config';
|
||||
import { findColor } from './Rotations.helpers';
|
||||
|
||||
import styles from './Rotations.module.css';
|
||||
|
|
@ -81,23 +83,33 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
|
|||
<div className={cx('header-plus-content')}>
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks startMoment={startMoment} />
|
||||
<div className={cx('rotations')}>
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
{shifts && shifts.length ? (
|
||||
shifts.map(({ shiftId, events }, index) => {
|
||||
return (
|
||||
<Rotation
|
||||
key={index}
|
||||
events={events}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
color={findColor(shiftId, layers, overrides)}
|
||||
/>
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation
|
||||
key={index}
|
||||
scheduleId={scheduleId}
|
||||
events={events}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
color={findColor(shiftId, layers, overrides)}
|
||||
/>
|
||||
</CSSTransition>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Rotation events={[]} startMoment={startMoment} currentTimezone={currentTimezone} />
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation
|
||||
scheduleId={scheduleId}
|
||||
events={[]}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
/>
|
||||
</CSSTransition>
|
||||
)}
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
<CSSTransition key={rotationIndex} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation
|
||||
key={rotationIndex}
|
||||
scheduleId={scheduleId}
|
||||
events={events}
|
||||
color={getOverrideColor(rotationIndex)}
|
||||
startMoment={startMoment}
|
||||
|
|
@ -92,9 +93,10 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
</CSSTransition>
|
||||
))
|
||||
) : (
|
||||
<CSSTransition key={0} timeout={500} classNames={{ ...styles }}>
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation
|
||||
events={[]}
|
||||
scheduleId={scheduleId}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
onClick={(moment) => {
|
||||
|
|
|
|||
|
|
@ -79,3 +79,8 @@
|
|||
background-color: white;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.is-oncall-icon {
|
||||
color: var(--oncall-icon-stroke-color);
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
|
@ -8,7 +8,8 @@ import { observer } from 'mobx-react';
|
|||
import Line from 'components/ScheduleUserDetails/img/line.svg';
|
||||
import Text from 'components/Text/Text';
|
||||
import WorkingHours from 'components/WorkingHours/WorkingHours';
|
||||
import { Event } from 'models/schedule/schedule.types';
|
||||
import { IsOncallIcon } from 'icons';
|
||||
import { Event, Schedule } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
|
@ -18,10 +19,8 @@ import { getTitle } from './ScheduleSlot.helpers';
|
|||
import styles from './ScheduleSlot.module.css';
|
||||
|
||||
interface ScheduleSlotProps {
|
||||
index: number;
|
||||
layerIndex: number;
|
||||
rotationIndex: number;
|
||||
event: Event;
|
||||
scheduleId: Schedule['id'];
|
||||
startMoment: dayjs.Dayjs;
|
||||
currentTimezone: Timezone;
|
||||
color?: string;
|
||||
|
|
@ -31,7 +30,7 @@ interface ScheduleSlotProps {
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
||||
const { index, layerIndex, rotationIndex, event, startMoment, currentTimezone, color, label } = props;
|
||||
const { event, scheduleId, startMoment, currentTimezone, color, label } = props;
|
||||
const { users } = event;
|
||||
|
||||
const trackMouse = false;
|
||||
|
|
@ -53,6 +52,8 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
setMouseX(event.nativeEvent.offsetX);
|
||||
}, []);
|
||||
|
||||
const onCallNow = store.scheduleStore.items[scheduleId]?.on_call_now;
|
||||
|
||||
return (
|
||||
<div className={cx('stack')} style={{ width: `${width * 100}%` /*left: `${x * 100}%`*/ }}>
|
||||
{!event.is_gap ? (
|
||||
|
|
@ -63,8 +64,21 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
|
||||
const title = getTitle(storeUser);
|
||||
|
||||
const isOncall = Boolean(
|
||||
storeUser && onCallNow && onCallNow.some((onCallUser) => storeUser.pk === onCallUser.pk)
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={<ScheduleSlotDetails user={storeUser} currentTimezone={currentTimezone} event={event} />}>
|
||||
<Tooltip
|
||||
content={
|
||||
<ScheduleSlotDetails
|
||||
user={storeUser}
|
||||
isOncall={isOncall}
|
||||
currentTimezone={currentTimezone}
|
||||
event={event}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cx('root', { root__inactive: inactive })}
|
||||
style={{
|
||||
|
|
@ -109,25 +123,20 @@ export default ScheduleSlot;
|
|||
|
||||
interface ScheduleSlotDetailsProps {
|
||||
user: User;
|
||||
isOncall: boolean;
|
||||
currentTimezone: Timezone;
|
||||
event: Event;
|
||||
}
|
||||
|
||||
const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
||||
const { user, currentTimezone, event } = props;
|
||||
|
||||
const userStatus = 'success';
|
||||
const { user, currentTimezone, event, isOncall } = props;
|
||||
|
||||
return (
|
||||
<div className={cx('details')}>
|
||||
<HorizontalGroup>
|
||||
<VerticalGroup spacing="sm">
|
||||
<HorizontalGroup spacing="md">
|
||||
<div
|
||||
className={cx('details-user-status', {
|
||||
[`details-user-status__type_${userStatus}`]: true,
|
||||
})}
|
||||
/>
|
||||
<HorizontalGroup spacing="sm">
|
||||
{isOncall && <IsOncallIcon className={cx('is-oncall-icon')} />}
|
||||
<Text type="secondary">{user?.username}</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
|
|
@ -238,3 +238,28 @@ export const ExpandIcon = (props: IconProps) => {
|
|||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
interface IsOncallIconProps {
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const IsOncallIcon = (props: IsOncallIconProps) => {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="17"
|
||||
height="16"
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="8.72021" cy="8" r="7.5" fill="#6CCF8E" stroke="currentColor" />
|
||||
<path
|
||||
d="M6.91558 9.7903C7.32816 10.2082 7.76482 10.584 8.22558 10.9177C8.68634 11.2514 9.14709 11.5145 9.60785 11.7069C10.0686 11.9023 10.5038 12 10.9133 12C11.1934 12 11.4539 11.9504 11.6948 11.8512C11.9357 11.752 12.1541 11.5941 12.3498 11.3777C12.4612 11.2514 12.5501 11.1161 12.6163 10.9718C12.6856 10.8275 12.7202 10.6817 12.7202 10.5344C12.7202 10.4262 12.6976 10.3209 12.6525 10.2187C12.6073 10.1165 12.532 10.0293 12.4266 9.95716L11.1121 9.02818C11.0097 8.95303 10.9148 8.89891 10.8275 8.86584C10.7432 8.83277 10.6619 8.81623 10.5836 8.81623C10.4872 8.81623 10.3923 8.84329 10.299 8.89741C10.2086 8.94852 10.1153 9.02368 10.0189 9.12289L9.70723 9.42052C9.66507 9.46561 9.61086 9.48816 9.54461 9.48816C9.51148 9.48816 9.47986 9.48365 9.44975 9.47463C9.41963 9.46261 9.39403 9.45209 9.37295 9.44307C9.23744 9.37091 9.06428 9.24615 8.85347 9.06877C8.64568 8.89139 8.43638 8.69748 8.22558 8.48703C8.01176 8.27659 7.81602 8.06614 7.63834 7.85569C7.46066 7.64525 7.33719 7.47388 7.26793 7.3416C7.25588 7.31755 7.24384 7.292 7.23179 7.26494C7.22276 7.23487 7.21824 7.2018 7.21824 7.16573C7.21824 7.10259 7.24082 7.04998 7.286 7.00789L7.58865 6.70124C7.68502 6.60203 7.76031 6.50733 7.81451 6.41714C7.86872 6.32394 7.89582 6.22773 7.89582 6.12852C7.89582 6.05036 7.87775 5.96918 7.84162 5.88501C7.80849 5.79782 7.75579 5.70312 7.68351 5.6009L6.75748 4.30665C6.68521 4.20143 6.59637 4.12477 6.49097 4.07666C6.38556 4.02555 6.27263 4 6.15217 4C5.85705 4 5.5815 4.12326 5.32552 4.36979C5.11472 4.57121 4.96113 4.79369 4.86477 5.0372C4.7684 5.28072 4.72021 5.53927 4.72021 5.81285C4.72021 6.22473 4.81508 6.65915 5.0048 7.11612C5.19753 7.57309 5.45953 8.03006 5.7908 8.48703C6.12206 8.941 6.49699 9.37542 6.91558 9.7903Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -135,7 +135,12 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
Users from on-call schedule" step in escalation chains.
|
||||
</Text>
|
||||
<div className={cx('users-timezones')}>
|
||||
<UsersTimezones users={users || []} tz={currentTimezone} onTzChange={this.handleTimezoneChange} />
|
||||
<UsersTimezones
|
||||
onCallNow={schedule?.on_call_now || []}
|
||||
users={users || []}
|
||||
tz={currentTimezone}
|
||||
onTzChange={this.handleTimezoneChange}
|
||||
/>
|
||||
</div>
|
||||
<div className={cx('controls')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
|
|
|
|||
|
|
@ -70,14 +70,14 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
const schedules = scheduleStore.getSearchResult();
|
||||
|
||||
const columns = [
|
||||
/* {
|
||||
{
|
||||
width: '10%',
|
||||
title: 'Status',
|
||||
key: 'name',
|
||||
render: this.renderStatus,
|
||||
},*/
|
||||
},
|
||||
{
|
||||
width: '50%',
|
||||
width: '30%',
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
render: this.renderName,
|
||||
|
|
@ -88,18 +88,18 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
key: 'users',
|
||||
render: this.renderOncallNow,
|
||||
},
|
||||
/*{
|
||||
/* {
|
||||
width: '20%',
|
||||
title: 'ChatOps',
|
||||
key: 'chatops',
|
||||
render: this.renderChatOps,
|
||||
},
|
||||
},*/
|
||||
{
|
||||
width: '10%',
|
||||
title: 'Quality',
|
||||
key: 'quality',
|
||||
render: this.renderQuality,
|
||||
},*/
|
||||
},
|
||||
{
|
||||
width: '5%',
|
||||
key: 'buttons',
|
||||
|
|
@ -254,16 +254,20 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
|
||||
renderOncallNow = (item: Schedule, index: number) => {
|
||||
if (item.on_call_now?.length > 0) {
|
||||
return item.on_call_now.map((user, index) => {
|
||||
return (
|
||||
<PluginLink key={user.pk} query={{ page: 'users', id: user.pk }}>
|
||||
<div>
|
||||
<Avatar size="small" src={user.avatar} />
|
||||
<Text type="secondary"> {user.username}</Text>
|
||||
</div>
|
||||
</PluginLink>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<VerticalGroup>
|
||||
{item.on_call_now.map((user, index) => {
|
||||
return (
|
||||
<PluginLink key={user.pk} query={{ page: 'users', id: user.pk }}>
|
||||
<div>
|
||||
<Avatar size="big" src={user.avatar} />
|
||||
<Text type="secondary"> {user.username}</Text>
|
||||
</div>
|
||||
</PluginLink>
|
||||
);
|
||||
})}
|
||||
</VerticalGroup>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
@ -275,7 +279,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
renderQuality = (item: Schedule) => {
|
||||
const type = item.quality > 70 ? 'primary' : 'warning';
|
||||
|
||||
return <Text type={type}>{item.quality}%</Text>;
|
||||
return <Text type={type}>{item.quality || 70}%</Text>;
|
||||
};
|
||||
|
||||
renderButtons = (item: Schedule) => {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
--primary-text-link: #1f62e0;
|
||||
--timeline-icon-background: rgba(70, 76, 84, 0);
|
||||
--timeline-icon-background-resolution-note: rgba(50, 116, 217, 0);
|
||||
--oncall-icon-stroke-color: #fff;
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
|
|
@ -50,4 +51,5 @@
|
|||
--hover-selected: rgba(204, 204, 220, 0.12);
|
||||
--hover-selected-hardcoded: #34363d;
|
||||
--secondary-background-shade: rgba(204, 204, 220, 0.2);
|
||||
--oncall-icon-stroke-color: #181b1f;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue