Show info about missing users in schedules (#4294)
# What this PR does - Don't show global notification if one of the rolling_users is a user that doesn't exist anymore (has been deleted) - In case user present in rolling_users has been either deleted or his/her role has been downgraded to Viewer, show such explicit info on UI   ## Which issue(s) this PR closes Closes https://github.com/grafana/oncall/issues/4251 <!-- *Note*: if you have more than one GitHub issue that this PR closes, be sure to preface each issue link with a [closing keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue). This ensures that the issue(s) are auto-closed once the PR has been merged. --> ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes.
This commit is contained in:
parent
0790d45ab5
commit
b5d900de96
6 changed files with 66 additions and 17 deletions
|
|
@ -0,0 +1,19 @@
|
|||
import React, { ComponentProps, FC } from 'react';
|
||||
|
||||
import { HorizontalGroup, Icon, Tooltip } from '@grafana/ui';
|
||||
|
||||
interface NonExistentUserNameProps {
|
||||
justify?: ComponentProps<typeof HorizontalGroup>['justify'];
|
||||
userName?: string;
|
||||
}
|
||||
|
||||
const NonExistentUserName: FC<NonExistentUserNameProps> = ({ justify = 'space-between', userName }) => (
|
||||
<HorizontalGroup justify={justify}>
|
||||
<span>Missing user</span>
|
||||
<Tooltip content={`${userName || 'User'} } is not found or doesn't have permission to participate in the rotation`}>
|
||||
<Icon name="exclamation-triangle" />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
|
||||
export default NonExistentUserName;
|
||||
|
|
@ -6,6 +6,7 @@ import cn from 'classnames/bind';
|
|||
import dayjs from 'dayjs';
|
||||
import { COLORS } from 'styles/utils.styles';
|
||||
|
||||
import NonExistentUserName from 'components/NonExistentUserName/NonExistentUserName';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { WorkingHours } from 'components/WorkingHours/WorkingHours';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
|
|
@ -30,18 +31,17 @@ export const UserItem = ({ pk, shiftColor, shiftStart, shiftEnd }: UserItemProps
|
|||
|
||||
useEffect(() => {
|
||||
if (!userStore.items[pk]) {
|
||||
userStore.fetchItemById({ userPk: pk, skipIfAlreadyPending: true });
|
||||
userStore.fetchItemById({ userPk: pk, skipIfAlreadyPending: true, skipErrorHandling: true });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const name = userStore.items[pk]?.username;
|
||||
const desc = userStore.items[pk]?.timezone;
|
||||
const workingHours = userStore.items[pk]?.working_hours;
|
||||
const timezone = userStore.items[pk]?.timezone;
|
||||
const workingHours = userStore.items[pk]?.working_hours;
|
||||
const duration = dayjs(shiftEnd).diff(dayjs(shiftStart), 'seconds');
|
||||
|
||||
return (
|
||||
<div className={cx('user-item')} style={{ backgroundColor: shiftColor, width: '100%' }}>
|
||||
const slotContent = name ? (
|
||||
<>
|
||||
{duration <= WEEK_IN_SECONDS && (
|
||||
<WorkingHours
|
||||
timezone={timezone}
|
||||
|
|
@ -52,8 +52,18 @@ export const UserItem = ({ pk, shiftColor, shiftStart, shiftEnd }: UserItemProps
|
|||
/>
|
||||
)}
|
||||
<div className={cx('user-title')}>
|
||||
<Text strong>{name}</Text> <Text className={styles.gray}>({desc})</Text>
|
||||
<Text strong>{name}</Text> <Text className={styles.gray}>({timezone})</Text>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={cx('user-title')}>
|
||||
<NonExistentUserName justify="flex-start" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('user-item')} style={{ backgroundColor: shiftColor, width: '100%' }}>
|
||||
{slotContent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -56,9 +56,8 @@
|
|||
z-index: 1;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import dayjs from 'dayjs';
|
|||
import { observer } from 'mobx-react';
|
||||
|
||||
import { Avatar } from 'components/Avatar/Avatar';
|
||||
import NonExistentUserName from 'components/NonExistentUserName/NonExistentUserName';
|
||||
import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally';
|
||||
import { ScheduleFiltersType } from 'components/ScheduleFilters/ScheduleFilters.types';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { WorkingHours } from 'components/WorkingHours/WorkingHours';
|
||||
|
|
@ -59,7 +61,7 @@ export const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
|
||||
const currentMoment = useMemo(() => dayjs(), []);
|
||||
|
||||
const renderEvent = (event): React.ReactElement | React.ReactElement[] => {
|
||||
const renderEvent = (event: Event): React.ReactElement | React.ReactElement[] => {
|
||||
if (event.shiftSwapId) {
|
||||
return <ShiftSwapEvent currentMoment={currentMoment} event={event} />;
|
||||
}
|
||||
|
|
@ -74,12 +76,31 @@ export const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
|
||||
if (event.is_empty) {
|
||||
return (
|
||||
<div
|
||||
className={cx('root')}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
<RenderConditionally
|
||||
shouldRender={event.missing_users.length > 0}
|
||||
backupChildren={
|
||||
<div
|
||||
className={cx('root')}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{event.missing_users.map((name) => (
|
||||
<div
|
||||
key={name}
|
||||
className={cx('root')}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
>
|
||||
<div className={cx('title')}>
|
||||
<NonExistentUserName userName={name} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</RenderConditionally>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -522,7 +522,7 @@ export class ScheduleStore extends BaseStore {
|
|||
...this.events[scheduleId],
|
||||
[type]: {
|
||||
...this.events[scheduleId]?.[type],
|
||||
[fromString]: layers ? layers : shifts,
|
||||
[fromString]: layers || shifts,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export interface Event {
|
|||
end: string;
|
||||
is_empty: boolean;
|
||||
is_gap: boolean;
|
||||
missing_users: Array<{ display_name: ApiSchemas['User']['username']; pk: ApiSchemas['User']['pk'] }>;
|
||||
missing_users: Array<ApiSchemas['User']['username']>;
|
||||
priority_level: number;
|
||||
shift: Pick<Shift, 'name' | 'type'> & { pk: string };
|
||||
source: string;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue