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

![image](https://github.com/grafana/oncall/assets/12073649/45cec62d-b62b-4085-b536-906ef9dbef1f)

![image](https://github.com/grafana/oncall/assets/12073649/692dea50-10c7-47e9-9371-565e21d80cfe)


## 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:
Dominik Broj 2024-04-30 11:00:16 +02:00 committed by GitHub
parent 0790d45ab5
commit b5d900de96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 66 additions and 17 deletions

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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;
}

View file

@ -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>
);
}

View file

@ -522,7 +522,7 @@ export class ScheduleStore extends BaseStore {
...this.events[scheduleId],
[type]: {
...this.events[scheduleId]?.[type],
[fromString]: layers ? layers : shifts,
[fromString]: layers || shifts,
},
},
};

View file

@ -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;