Polish user settings and warnings (#2425)
# What this PR does * users table: added warnings: No default notifications set, No important notifications set * users table: removed warnings when messenger is not configured (e.g. telegram channels are not connected -> no need to show telegram warning in users table) * users table: moved current user to first place * user profile: cleaned up and added hints to notification channel connectors * user profile: cleaned up and added hints to calendar sync * chatops-slack: cleaned up and added hints to slack settings fixes https://github.com/grafana/oncall/issues/2418 ## Which issue(s) this PR fixes ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
This commit is contained in:
parent
35e4cf5bac
commit
8367a1ed4c
17 changed files with 490 additions and 410 deletions
|
|
@ -77,6 +77,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Modified DRF pagination class used by `GET /api/internal/v1/alert_receive_channels` and `GET /api/internal/v1/schedules`
|
||||
endpoints so that the `next` and `previous` pagination links are properly set when OnCall is run behind
|
||||
a reverse proxy by @joeyorlando ([#2467](https://github.com/grafana/oncall/pull/2467))
|
||||
- Polish user settings and warnings ([#2425](https://github.com/grafana/oncall/pull/2425))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -249,9 +249,8 @@ class UserView(
|
|||
|
||||
return queryset.order_by("id")
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
def list(self, request, *args, **kwargs) -> Response:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
context = {"request": self.request, "format": self.format_kwarg, "view": self}
|
||||
|
|
@ -272,7 +271,7 @@ class UserView(
|
|||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
def retrieve(self, request, *args, **kwargs) -> Response:
|
||||
context = {"request": self.request, "format": self.format_kwarg, "view": self}
|
||||
try:
|
||||
instance = self.get_object()
|
||||
|
|
@ -292,7 +291,7 @@ class UserView(
|
|||
serializer = self.get_serializer(instance, context=context)
|
||||
return Response(serializer.data)
|
||||
|
||||
def wrong_team_response(self):
|
||||
def wrong_team_response(self) -> Response:
|
||||
"""
|
||||
This method returns 403 and {"error_code": "wrong_team", "owner_team": {"name", "id", "email", "avatar_url"}}.
|
||||
Used in case if a requested instance doesn't belong to user's current_team.
|
||||
|
|
@ -314,12 +313,12 @@ class UserView(
|
|||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
def current(self, request):
|
||||
def current(self, request) -> Response:
|
||||
serializer = UserSerializer(self.get_queryset().get(pk=self.request.user.pk))
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=["get"])
|
||||
def timezone_options(self, request):
|
||||
def timezone_options(self, request) -> Response:
|
||||
return Response(pytz.common_timezones)
|
||||
|
||||
@action(
|
||||
|
|
@ -327,7 +326,7 @@ class UserView(
|
|||
methods=["get"],
|
||||
throttle_classes=[GetPhoneVerificationCodeThrottlerPerUser, GetPhoneVerificationCodeThrottlerPerOrg],
|
||||
)
|
||||
def get_verification_code(self, request, pk):
|
||||
def get_verification_code(self, request, pk) -> Response:
|
||||
logger.info("get_verification_code: validating reCAPTCHA code")
|
||||
valid = check_recaptcha_internal_api(request, "mobile_verification_code")
|
||||
if not valid:
|
||||
|
|
@ -354,7 +353,7 @@ class UserView(
|
|||
methods=["get"],
|
||||
throttle_classes=[GetPhoneVerificationCodeThrottlerPerUser, GetPhoneVerificationCodeThrottlerPerOrg],
|
||||
)
|
||||
def get_verification_call(self, request, pk):
|
||||
def get_verification_call(self, request, pk) -> Response:
|
||||
logger.info("get_verification_code_via_call: validating reCAPTCHA code")
|
||||
valid = check_recaptcha_internal_api(request, "mobile_verification_code")
|
||||
if not valid:
|
||||
|
|
@ -381,7 +380,7 @@ class UserView(
|
|||
methods=["put"],
|
||||
throttle_classes=[VerifyPhoneNumberThrottlerPerUser, VerifyPhoneNumberThrottlerPerOrg],
|
||||
)
|
||||
def verify_number(self, request, pk):
|
||||
def verify_number(self, request, pk) -> Response:
|
||||
target_user = self.get_object()
|
||||
code = request.query_params.get("token", None)
|
||||
if not code:
|
||||
|
|
@ -407,7 +406,7 @@ class UserView(
|
|||
return Response("Verification code is not correct", status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(detail=True, methods=["put"])
|
||||
def forget_number(self, request, pk):
|
||||
def forget_number(self, request, pk) -> Response:
|
||||
target_user = self.get_object()
|
||||
prev_state = target_user.insight_logs_serialized
|
||||
|
||||
|
|
@ -426,7 +425,7 @@ class UserView(
|
|||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["post"], throttle_classes=[TestCallThrottler])
|
||||
def make_test_call(self, request, pk):
|
||||
def make_test_call(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
try:
|
||||
phone_backend = PhoneBackend()
|
||||
|
|
@ -441,7 +440,7 @@ class UserView(
|
|||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["post"], throttle_classes=[TestCallThrottler])
|
||||
def send_test_sms(self, request, pk):
|
||||
def send_test_sms(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
try:
|
||||
phone_backend = PhoneBackend()
|
||||
|
|
@ -456,7 +455,7 @@ class UserView(
|
|||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["post"], throttle_classes=[TestPushThrottler])
|
||||
def send_test_push(self, request, pk):
|
||||
def send_test_push(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
critical = request.query_params.get("critical", "false") == "true"
|
||||
|
||||
|
|
@ -475,7 +474,7 @@ class UserView(
|
|||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def get_backend_verification_code(self, request, pk):
|
||||
def get_backend_verification_code(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
|
||||
backend_id = request.query_params.get("backend")
|
||||
|
|
@ -487,7 +486,7 @@ class UserView(
|
|||
return Response(code)
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def get_telegram_verification_code(self, request, pk):
|
||||
def get_telegram_verification_code(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
|
||||
if not user.is_telegram_connected:
|
||||
|
|
@ -511,7 +510,7 @@ class UserView(
|
|||
)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def unlink_slack(self, request, pk):
|
||||
def unlink_slack(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
user.slack_user_identity = None
|
||||
user.save(update_fields=["slack_user_identity"])
|
||||
|
|
@ -525,7 +524,7 @@ class UserView(
|
|||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def unlink_telegram(self, request, pk):
|
||||
def unlink_telegram(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
TelegramToUserConnector = apps.get_model("telegram", "TelegramToUserConnector")
|
||||
try:
|
||||
|
|
@ -543,7 +542,7 @@ class UserView(
|
|||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["post"])
|
||||
def unlink_backend(self, request, pk):
|
||||
def unlink_backend(self, request, pk) -> Response:
|
||||
# TODO: insight logs support
|
||||
user = self.get_object()
|
||||
|
||||
|
|
@ -566,7 +565,7 @@ class UserView(
|
|||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def upcoming_shifts(self, request, pk):
|
||||
def upcoming_shifts(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
try:
|
||||
days = int(request.query_params.get("days", UPCOMING_SHIFTS_DEFAULT_DAYS))
|
||||
|
|
@ -604,7 +603,7 @@ class UserView(
|
|||
return Response(upcoming, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=["get", "post", "delete"])
|
||||
def export_token(self, request, pk):
|
||||
def export_token(self, request, pk) -> Response | None:
|
||||
user = self.get_object()
|
||||
|
||||
if self.request.method == "GET":
|
||||
|
|
@ -645,7 +644,7 @@ class UserView(
|
|||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def check_availability(self, request, pk):
|
||||
def check_availability(self, request, pk) -> Response:
|
||||
user = self.get_object()
|
||||
warnings = check_user_availability(user=user, team=request.user.current_team)
|
||||
return Response(data={"warnings": warnings}, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ class Organization(MaintainableObject):
|
|||
ACKNOWLEDGE_REMIND_10H,
|
||||
) = range(5)
|
||||
ACKNOWLEDGE_REMIND_CHOICES = (
|
||||
(ACKNOWLEDGE_REMIND_NEVER, "Never remind about ack-ed incidents"),
|
||||
(ACKNOWLEDGE_REMIND_NEVER, "Never remind"),
|
||||
(ACKNOWLEDGE_REMIND_1H, "Remind every 1 hour"),
|
||||
(ACKNOWLEDGE_REMIND_3H, "Remind every 3 hours"),
|
||||
(ACKNOWLEDGE_REMIND_5H, "Remind every 5 hours"),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@
|
|||
}
|
||||
|
||||
.step .select {
|
||||
width: 250px !important;
|
||||
width: 200px !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ export default function Alerts() {
|
|||
const isChatOpsConnected = getIfChatOpsConnected(currentUser);
|
||||
const isPhoneVerified = currentUser?.cloud_connection_status === 3 || currentUser?.verified_phone_number;
|
||||
|
||||
const isDefaultNotificationsSet = currentUser?.notification_chain_verbal.default;
|
||||
const isImportantNotificationsSet = currentUser?.notification_chain_verbal.important;
|
||||
|
||||
if (!showSlackInstallAlert && !showBannerTeam() && !showMismatchWarning() && !showChannelWarnings()) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -125,23 +128,18 @@ export default function Alerts() {
|
|||
onRemove={getRemoveAlertHandler(AlertID.CONNECTIVITY_WARNING)}
|
||||
className={cx('alert')}
|
||||
severity="warning"
|
||||
title="Notification Warning"
|
||||
title="Notification Warning! Possible notification miss."
|
||||
>
|
||||
{
|
||||
<>
|
||||
{!isChatOpsConnected && (
|
||||
<>
|
||||
No messenger connected. Possible notification miss. Connect messenger(s) in{' '}
|
||||
<PluginLink query={{ page: 'users', id: 'me' }}>User profile settings</PluginLink> to receive all
|
||||
notifications.
|
||||
</>
|
||||
)}
|
||||
{!isPhoneVerified && (
|
||||
<>
|
||||
Your phone number is not verified. You can change your configuration in{' '}
|
||||
<PluginLink query={{ page: 'users', id: 'me' }}>User profile settings</PluginLink>
|
||||
</>
|
||||
)}
|
||||
{!isDefaultNotificationsSet && <>Default notification chain is not set. </>}
|
||||
{!isImportantNotificationsSet && <>Important notification chain is not set. </>}
|
||||
{!isChatOpsConnected && <>No messenger connected for ChatOps. </>}
|
||||
{!isPhoneVerified && <>Your phone number is not verified. </>}
|
||||
<>
|
||||
You can change your configuration in{' '}
|
||||
<PluginLink query={{ page: 'users', id: 'me' }}>User profile settings</PluginLink>
|
||||
</>
|
||||
</>
|
||||
}
|
||||
</Alert>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { Modal } from '@grafana/ui';
|
||||
import { HorizontalGroup, Modal } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import { Tabs, TabsContent } from 'containers/UserSettings/parts';
|
||||
import { User as UserType } from 'models/user/user.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
|
|
@ -55,15 +56,15 @@ const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserInfo }: U
|
|||
isCurrent,
|
||||
];
|
||||
|
||||
const title = (
|
||||
<HorizontalGroup>
|
||||
<Avatar className={cx('user-avatar')} size="large" src={storeUser.avatar} /> <h2>{storeUser.username}</h2>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={`${storeUser.username}`}
|
||||
className={cx('modal', 'modal-wide')}
|
||||
isOpen
|
||||
closeOnEscape={false}
|
||||
onDismiss={onHide}
|
||||
>
|
||||
<Modal title={title} className={cx('modal', 'modal-wide')} isOpen closeOnEscape={false} onDismiss={onHide}>
|
||||
<div className={cx('root')}>
|
||||
<Tabs
|
||||
onTabChange={onTabChange}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Button, Icon, Label, LoadingPlaceholder } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { Alert, Button, HorizontalGroup, InlineField, Input, LoadingPlaceholder, Tooltip } from '@grafana/ui';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openNotification } from 'utils';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
import styles from './index.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ICalConnectorProps {
|
||||
id: User['pk'];
|
||||
|
|
@ -54,69 +49,84 @@ const ICalConnector = (props: ICalConnectorProps) => {
|
|||
await userStore.deleteiCalLink(id);
|
||||
};
|
||||
|
||||
const isCurrentUser = id === store.userStore.currentUserPk;
|
||||
|
||||
return (
|
||||
<div className={cx('user-item')}>
|
||||
<Label>iCal link:</Label>
|
||||
<Text type="secondary">
|
||||
Secret iCal export link to add your assigned on call shifts to your calendar.
|
||||
<br />
|
||||
NOTE: We do not have control over when a client refreshes an imported calendar.
|
||||
</Text>
|
||||
<div className={cx('iCal-settings')}>
|
||||
{iCalLoading ? (
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
) : (
|
||||
<>
|
||||
{isiCalLinkExisting ? (
|
||||
<>
|
||||
{showiCalLink !== undefined ? (
|
||||
<>
|
||||
<div className={cx('iCal-link-container')}>
|
||||
<Icon name="exclamation-triangle" className={cx('warning-icon')} />{' '}
|
||||
<Text type="warning">Make sure you copy it - you won't be able to access it again.</Text>
|
||||
<div className={cx('iCal-link')}>{showiCalLink}</div>
|
||||
</div>
|
||||
<CopyToClipboard
|
||||
text={showiCalLink}
|
||||
onCopy={() => {
|
||||
openNotification('iCal link is copied');
|
||||
}}
|
||||
>
|
||||
<Button icon="copy" variant="secondary" className={cx('iCal-button')}>
|
||||
Copy iCal link
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text type="secondary">
|
||||
In case you lost your iCal link you can revoke it and generate a new one.
|
||||
</Text>
|
||||
<WithPermissionControlTooltip userAction={UserActions.UserSettingsWrite}>
|
||||
<Button
|
||||
icon="trash-alt"
|
||||
onClick={handleRevokeiCalLink}
|
||||
className={cx('iCal-button')}
|
||||
variant="destructive"
|
||||
fill="outline"
|
||||
<>
|
||||
{iCalLoading ? (
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
) : (
|
||||
<>
|
||||
{isiCalLinkExisting ? (
|
||||
<>
|
||||
{showiCalLink !== undefined ? (
|
||||
<>
|
||||
<InlineField
|
||||
label="iCal link"
|
||||
labelWidth={12}
|
||||
tooltip={'Secret iCal export link to add your assigned on call shifts to your calendar'}
|
||||
>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Tooltip content={'In case you lost your iCal link you can revoke it and generate a new one.'}>
|
||||
<Input disabled value={showiCalLink} />
|
||||
</Tooltip>
|
||||
<CopyToClipboard
|
||||
text={showiCalLink}
|
||||
onCopy={() => {
|
||||
openNotification('iCal link is copied');
|
||||
}}
|
||||
>
|
||||
Revoke iCal link
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<WithPermissionControlTooltip userAction={UserActions.UserSettingsWrite}>
|
||||
<Button icon="plus" onClick={handleCreateiCalLink} className={cx('iCal-button')} variant="secondary">
|
||||
Create iCal link
|
||||
<Button icon="copy">Copy</Button>
|
||||
</CopyToClipboard>
|
||||
</HorizontalGroup>
|
||||
</InlineField>
|
||||
<Alert severity="warning" title="Make sure you copy it - you won't be able to access it again." />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WithPermissionControlTooltip userAction={UserActions.UserSettingsWrite}>
|
||||
<InlineField
|
||||
label="iCal link"
|
||||
labelWidth={12}
|
||||
tooltip={'Secret iCal export link to add your assigned on call shifts to your calendar'}
|
||||
>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Tooltip content={'In case you lost your iCal link you can revoke it and generate a new one.'}>
|
||||
<Input value={'***'} />
|
||||
</Tooltip>
|
||||
<WithConfirm
|
||||
title={
|
||||
'Are you sure you want to revoke iCal link' + (!isCurrentUser ? ' for other user' : '')
|
||||
}
|
||||
confirmText="Revoke"
|
||||
>
|
||||
<Button icon="trash-alt" variant="destructive" onClick={handleRevokeiCalLink}>
|
||||
Revoke
|
||||
</Button>
|
||||
</WithConfirm>
|
||||
</HorizontalGroup>
|
||||
</InlineField>
|
||||
</WithPermissionControlTooltip>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<WithPermissionControlTooltip userAction={UserActions.UserSettingsWrite}>
|
||||
<InlineField
|
||||
label="iCal link"
|
||||
labelWidth={12}
|
||||
tooltip={'Secret iCal export link to add your assigned on call shifts to your calendar'}
|
||||
>
|
||||
<Button onClick={handleCreateiCalLink} variant="secondary">
|
||||
Create
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</InlineField>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Alert title="Note: We do not have control over when a client refreshes an imported calendar." severity="info" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +1,39 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Button, Label } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { Button, InlineField } from '@grafana/ui';
|
||||
|
||||
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
||||
|
||||
import styles from './index.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
interface MobileAppConnectorProps {
|
||||
id: User['pk'];
|
||||
onTabChange: (tab: UserSettingsTab) => void;
|
||||
}
|
||||
|
||||
const MobileAppConnector = (props: MobileAppConnectorProps) => {
|
||||
const { onTabChange } = props;
|
||||
const { onTabChange, id } = props;
|
||||
const store = useStore();
|
||||
const { userStore } = store;
|
||||
|
||||
const handleClickConfirmMobileAppButton = useCallback(() => {
|
||||
onTabChange(UserSettingsTab.MobileAppConnection);
|
||||
}, [onTabChange]);
|
||||
|
||||
const user = userStore.items[id];
|
||||
const isCurrentUser = id === store.userStore.currentUserPk;
|
||||
const isMobileAppConnected = user.messaging_backends['MOBILE_APP']?.connected;
|
||||
|
||||
return (
|
||||
<div className={cx('user-item')}>
|
||||
<Label>Mobile App:</Label>
|
||||
<div>
|
||||
<Button size="sm" fill="text" onClick={handleClickConfirmMobileAppButton}>
|
||||
Click to add a mobile app
|
||||
<InlineField label="Mobile App" labelWidth={12} disabled={!isCurrentUser}>
|
||||
{isMobileAppConnected ? (
|
||||
<Button variant="destructive" onClick={handleClickConfirmMobileAppButton}>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button onClick={handleClickConfirmMobileAppButton}>Connect</Button>
|
||||
)}
|
||||
</InlineField>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Button, Label, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { Alert, Button, HorizontalGroup, InlineField, Input } from '@grafana/ui';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './index.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface PhoneConnectorProps {
|
||||
id: User['pk'];
|
||||
onTabChange: (tab: UserSettingsTab) => void;
|
||||
|
|
@ -30,82 +25,129 @@ const PhoneConnector = (props: PhoneConnectorProps) => {
|
|||
onTabChange(UserSettingsTab.PhoneVerification);
|
||||
}, [storeUser?.unverified_phone_number, onTabChange]);
|
||||
|
||||
const isCurrentUser = storeUser.pk === userStore.currentUserPk;
|
||||
|
||||
const cloudVersionPhone = (user: User) => {
|
||||
switch (user.cloud_connection_status) {
|
||||
case 0:
|
||||
return <Text className={cx('error-message')}>Cloud is not synced</Text>;
|
||||
return (
|
||||
<>
|
||||
<InlineField
|
||||
label="Phone"
|
||||
labelWidth={12}
|
||||
tooltip={'OnCall uses Grafana Cloud for SMS and phone call notifications'}
|
||||
>
|
||||
<Button onClick={handleClickConfirmPhoneButton}>Connect to Cloud</Button>
|
||||
</InlineField>
|
||||
<Alert title="This instance is not connected to Cloud OnCall" severity="warning" />
|
||||
</>
|
||||
);
|
||||
|
||||
case 1:
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<Text className={cx('error-message')}>User is not matched with cloud</Text>
|
||||
<Button size="sm" fill="text" onClick={handleClickConfirmPhoneButton}>
|
||||
Sign Up to Cloud
|
||||
</Button>
|
||||
</VerticalGroup>
|
||||
<>
|
||||
<InlineField
|
||||
label="Phone"
|
||||
labelWidth={12}
|
||||
tooltip={'OnCall uses Grafana Cloud for SMS and phone call notifications'}
|
||||
>
|
||||
<Button onClick={handleClickConfirmPhoneButton}>Reload from Cloud</Button>
|
||||
</InlineField>
|
||||
<Alert title="User is not matched with cloud" severity="warning" />
|
||||
</>
|
||||
);
|
||||
|
||||
case 2:
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<Text type="warning">Phone number is not verified in Grafana Cloud</Text>
|
||||
<Button size="sm" fill="text" onClick={handleClickConfirmPhoneButton}>
|
||||
Verify or change
|
||||
</Button>
|
||||
</VerticalGroup>
|
||||
<>
|
||||
<InlineField
|
||||
label="Phone"
|
||||
labelWidth={12}
|
||||
tooltip={'OnCall uses Grafana Cloud for SMS and phone call notifications'}
|
||||
>
|
||||
<Button onClick={handleClickConfirmPhoneButton}>Verify in Cloud</Button>
|
||||
</InlineField>
|
||||
<Alert title="Phone number is not verified in Grafana Cloud" severity="warning" />
|
||||
</>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<Text type="success">Phone number verified</Text>
|
||||
<Button size="sm" fill="text" onClick={handleClickConfirmPhoneButton}>
|
||||
Change
|
||||
</Button>
|
||||
</VerticalGroup>
|
||||
<>
|
||||
<InlineField
|
||||
label="Phone"
|
||||
labelWidth={12}
|
||||
tooltip={'OnCall uses Grafana Cloud for SMS and phone call notifications'}
|
||||
>
|
||||
<Button onClick={handleClickConfirmPhoneButton}>Change in Cloud</Button>
|
||||
</InlineField>
|
||||
<Alert title="Phone number verified" severity="success" />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<Text className={cx('error-message')}>User is not matched with cloud</Text>
|
||||
<Button size="sm" fill="text" onClick={handleClickConfirmPhoneButton}>
|
||||
Sign Up to Cloud
|
||||
</Button>
|
||||
</VerticalGroup>
|
||||
<>
|
||||
<InlineField
|
||||
label="Phone"
|
||||
disabled={true}
|
||||
labelWidth={12}
|
||||
tooltip={'OnCall uses Grafana Cloud for SMS and phone call notifications'}
|
||||
>
|
||||
<Button onClick={handleClickConfirmPhoneButton}>Reload from Cloud</Button>
|
||||
</InlineField>
|
||||
<Alert title="User is not matched with cloud" severity="warning" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx('user-item')}>
|
||||
<div>
|
||||
{store.hasFeature(AppFeature.CloudNotifications) ? (
|
||||
<>
|
||||
<Label>Cloud phone status:</Label>
|
||||
{cloudVersionPhone(storeUser)}
|
||||
</>
|
||||
<>{cloudVersionPhone(storeUser)}</>
|
||||
) : (
|
||||
<>
|
||||
<Label>Verified phone number:</Label>
|
||||
<span className={cx('user-value')}>{storeUser.verified_phone_number || '—'}</span>
|
||||
{storeUser.verified_phone_number ? (
|
||||
<div>
|
||||
<Text type="secondary">Phone number is verified</Text>
|
||||
<Button size="sm" fill="text" onClick={handleClickConfirmPhoneButton}>
|
||||
Change
|
||||
</Button>
|
||||
<InlineField label="Phone" labelWidth={12}>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Input disabled={true} value={storeUser.verified_phone_number} />
|
||||
{isCurrentUser ? (
|
||||
<Button variant="secondary" icon="edit" onClick={handleClickConfirmPhoneButton} />
|
||||
) : (
|
||||
<WithConfirm title="Are you sure you want to edit other's phone number?" confirmText="Proceed">
|
||||
<Button variant="secondary" icon="edit" onClick={handleClickConfirmPhoneButton} />
|
||||
</WithConfirm>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</InlineField>
|
||||
</div>
|
||||
) : storeUser.unverified_phone_number ? (
|
||||
<div>
|
||||
<Text type="warning">Phone number is not verified</Text>
|
||||
<Button size="sm" fill="text" onClick={handleClickConfirmPhoneButton}>
|
||||
Verify or change
|
||||
</Button>
|
||||
<InlineField label="Phone" labelWidth={12}>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Input disabled={true} value={storeUser.unverified_phone_number} />
|
||||
{isCurrentUser ? (
|
||||
<Button onClick={handleClickConfirmPhoneButton}>Verify</Button>
|
||||
) : (
|
||||
<WithConfirm title="Are you sure you want to verify other's phone number?" confirmText="Proceed">
|
||||
<Button onClick={handleClickConfirmPhoneButton}>Verify</Button>
|
||||
</WithConfirm>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</InlineField>
|
||||
<Alert title="Phone number is not verified. Verify or change" severity="warning" />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Text type="warning">Phone number is not added</Text>
|
||||
<Button size="sm" fill="text" onClick={handleClickConfirmPhoneButton}>
|
||||
Add
|
||||
</Button>
|
||||
<InlineField label="Phone" labelWidth={12}>
|
||||
{isCurrentUser ? (
|
||||
<Button onClick={handleClickConfirmPhoneButton}>Add phone number</Button>
|
||||
) : (
|
||||
<WithConfirm title="Are you sure you want to add other's phone number?" confirmText="Proceed">
|
||||
<Button onClick={handleClickConfirmPhoneButton}>Add phone number</Button>
|
||||
</WithConfirm>
|
||||
)}
|
||||
</InlineField>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { Button, Label } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { Button, HorizontalGroup, InlineField, Input } from '@grafana/ui';
|
||||
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './index.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
import { getPathFromQueryParams } from 'utils/url';
|
||||
|
||||
interface SlackConnectorProps {
|
||||
id: User['pk'];
|
||||
|
|
@ -27,7 +21,7 @@ const SlackConnector = (props: SlackConnectorProps) => {
|
|||
|
||||
const storeUser = userStore.items[id];
|
||||
|
||||
const isCurrent = id === store.userStore.currentUserPk;
|
||||
const isCurrentUser = id === store.userStore.currentUserPk;
|
||||
|
||||
const handleConnectButtonClick = useCallback(() => {
|
||||
onTabChange(UserSettingsTab.SlackInfo);
|
||||
|
|
@ -37,43 +31,67 @@ const SlackConnector = (props: SlackConnectorProps) => {
|
|||
userStore.unlinkSlack(userStore.currentUserPk);
|
||||
}, []);
|
||||
|
||||
const chatOpsQuery = { page: 'chat-ops' };
|
||||
const chatOpsPath = useMemo(() => getPathFromQueryParams(chatOpsQuery), [chatOpsQuery]);
|
||||
|
||||
return (
|
||||
<div className={cx('user-item')}>
|
||||
<Label>Slack username:</Label>
|
||||
<span className={cx('user-value')}>{storeUser.slack_user_identity?.name || '—'}</span>
|
||||
<>
|
||||
{storeUser.slack_user_identity ? (
|
||||
<div>
|
||||
<Text type="secondary"> Slack account is connected</Text>
|
||||
{storeUser.pk === userStore.currentUserPk ? (
|
||||
<WithConfirm title="Are you sure to disconnect your Slack account?" confirmText="Disconnect">
|
||||
<Button size="sm" fill="text" variant="destructive" onClick={handleUnlinkSlackAccount}>
|
||||
Unlink Slack account
|
||||
<>
|
||||
<InlineField
|
||||
label="Slack"
|
||||
labelWidth={12}
|
||||
tooltip={'Connected Slack user will receive mentions during escalations'}
|
||||
>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Input
|
||||
disabled={true}
|
||||
value={
|
||||
storeUser.slack_user_identity?.slack_login ? '@' + storeUser.slack_user_identity?.slack_login : ''
|
||||
}
|
||||
/>
|
||||
<WithConfirm title="Are you sure to disconnect your Slack account?" confirmText="Disconnect">
|
||||
<Button
|
||||
variant="destructive"
|
||||
icon="times"
|
||||
onClick={handleUnlinkSlackAccount}
|
||||
disabled={!isCurrentUser}
|
||||
/>
|
||||
</WithConfirm>
|
||||
</HorizontalGroup>
|
||||
</InlineField>
|
||||
</>
|
||||
) : teamStore.currentTeam?.slack_team_identity ? (
|
||||
<>
|
||||
<InlineField
|
||||
label="Slack"
|
||||
labelWidth={12}
|
||||
disabled={!isCurrentUser}
|
||||
tooltip={'To receive mentions for alert groups posted on Slack, connect your Slack profile.'}
|
||||
>
|
||||
<Button onClick={handleConnectButtonClick}>Connect account</Button>
|
||||
</InlineField>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<InlineField
|
||||
label="Slack"
|
||||
labelWidth={12}
|
||||
tooltip={'To receive mentions for alert groups posted on Slack, connect your Slack profile.'}
|
||||
>
|
||||
<WithConfirm
|
||||
title="Leave personal profile settings?"
|
||||
confirmText="Continue"
|
||||
description="OnCall Slack Application is not installed globally for this Grafana Organization Stack. Please install it in Organization Settings before connecting personal Slack Profile."
|
||||
>
|
||||
<Button onClick={() => window.open(chatOpsPath, '_blank')} icon="external-link-alt">
|
||||
Install Slack App
|
||||
</Button>
|
||||
</WithConfirm>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
) : teamStore.currentTeam?.slack_team_identity ? (
|
||||
<div>
|
||||
<Text type="warning">Slack account is not connected</Text>
|
||||
{isCurrent && (
|
||||
<Button size="sm" fill="text" onClick={handleConnectButtonClick}>
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Text type="warning">Slack Integration is not installed</Text>
|
||||
<PluginLink query={{ page: 'chat-ops' }}>
|
||||
<Button size="sm" fill="text">
|
||||
Install
|
||||
</Button>
|
||||
</PluginLink>
|
||||
</div>
|
||||
</InlineField>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Button, Label } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { Button, HorizontalGroup, InlineField, Input } from '@grafana/ui';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './index.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface TelegramConnectorProps {
|
||||
id: User['pk'];
|
||||
onTabChange: (tab: UserSettingsTab) => void;
|
||||
|
|
@ -26,7 +20,7 @@ const TelegramConnector = (props: TelegramConnectorProps) => {
|
|||
|
||||
const storeUser = userStore.items[id];
|
||||
|
||||
const isCurrent = id === store.userStore.currentUserPk;
|
||||
const isCurrentUser = id === store.userStore.currentUserPk;
|
||||
|
||||
const handleConnectButtonClick = useCallback(() => {
|
||||
onTabChange(UserSettingsTab.TelegramInfo);
|
||||
|
|
@ -37,32 +31,31 @@ const TelegramConnector = (props: TelegramConnectorProps) => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cx('user-item')}>
|
||||
<Label>Telegram username:</Label>
|
||||
<span className={cx('user-value')}>{storeUser.telegram_configuration?.telegram_nick_name || '—'}</span>
|
||||
{storeUser.telegram_configuration && storeUser.pk === userStore.currentUserPk ? (
|
||||
<div>
|
||||
<Text type="secondary"> Telegram account is connected</Text>
|
||||
{storeUser.pk === userStore.currentUserPk ? (
|
||||
<div>
|
||||
<InlineField label="Telegram" labelWidth={12} disabled={!isCurrentUser}>
|
||||
{storeUser.telegram_configuration ? (
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Input
|
||||
disabled={true}
|
||||
value={
|
||||
storeUser.telegram_configuration?.telegram_nick_name
|
||||
? '@' + storeUser.telegram_configuration?.telegram_nick_name
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<WithConfirm title="Are you sure to disconnect your Telegram account?" confirmText="Disconnect">
|
||||
<Button size="sm" fill="text" variant="destructive" onClick={handleUnlinkTelegramAccount}>
|
||||
Unlink Telegram account
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUnlinkTelegramAccount}
|
||||
variant="destructive"
|
||||
icon="times"
|
||||
disabled={!isCurrentUser}
|
||||
/>
|
||||
</WithConfirm>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Text type="warning">Telegram account is not connected</Text>
|
||||
{isCurrent && (
|
||||
<Button size="sm" fill="text" onClick={handleConnectButtonClick}>
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
) : (
|
||||
<Button onClick={handleConnectButtonClick}>Connect account</Button>
|
||||
)}
|
||||
</InlineField>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Legend } from '@grafana/ui';
|
||||
|
||||
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
|
|
@ -19,12 +21,13 @@ interface ConnectorsProps {
|
|||
export const Connectors: FC<ConnectorsProps> = (props) => {
|
||||
const store = useStore();
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<PhoneConnector {...props} />
|
||||
<MobileAppConnector {...props} />
|
||||
<SlackConnector {...props} />
|
||||
{store.hasFeature(AppFeature.Telegram) && <TelegramConnector {...props} />}
|
||||
<Legend>Calendar export</Legend>
|
||||
<ICalConnector {...props} />
|
||||
<MobileAppConnector {...props} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -116,10 +116,10 @@ export const TabsContent = observer(({ id, activeTab, onTabChange, isDesktopOrLa
|
|||
{activeTab === UserSettingsTab.UserInfo &&
|
||||
(isDesktopOrLaptop ? (
|
||||
<div className={cx('columns')}>
|
||||
<Block shadowed bordered style={{ width: '30%' }} className={cx('col', 'left')}>
|
||||
<Block shadowed bordered style={{ width: '40%' }} className={cx('col', 'left')}>
|
||||
<UserInfoTab id={id} onTabChange={onTabChange} />
|
||||
</Block>
|
||||
<Block shadowed bordered style={{ width: '70%' }} className={cx('col', 'right')}>
|
||||
<Block shadowed bordered style={{ width: '60%' }} className={cx('col', 'right')}>
|
||||
<NotificationSettingsTab id={id} />
|
||||
</Block>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, VerticalGroup, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { Button, VerticalGroup, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
|
|
@ -23,6 +23,8 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => {
|
|||
const [userStatus, setUserStatus] = useState<number>(0);
|
||||
const [userLink, setUserLink] = useState<string>(null);
|
||||
|
||||
const email = store.userStore.items[userPk].email;
|
||||
|
||||
useEffect(() => {
|
||||
getCloudUserInfo();
|
||||
}, []);
|
||||
|
|
@ -63,9 +65,8 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => {
|
|||
return (
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text>
|
||||
{
|
||||
'We can’t find a matching account in the connected Grafana Cloud instance (matching happens by e-mail). '
|
||||
}
|
||||
We can’t find a matching account in the connected Grafana Cloud instance (matching by e-mail
|
||||
{email && ': ' + email}).
|
||||
</Text>
|
||||
<Button variant="primary" onClick={() => handleLinkClick(userLink)}>
|
||||
Sign up in Grafana Cloud
|
||||
|
|
@ -98,9 +99,8 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => {
|
|||
return (
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text>
|
||||
{
|
||||
'We can’t find a matching account in the connected Grafana Cloud instance (matching happens by e-mail). '
|
||||
}
|
||||
We can’t find a matching account in the connected Grafana Cloud instance (matching by e-mail
|
||||
{email && ': ' + email}).
|
||||
</Text>
|
||||
<Button variant="primary" onClick={() => handleLinkClick(userLink)}>
|
||||
Sign up in Grafana Cloud
|
||||
|
|
@ -118,18 +118,16 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => {
|
|||
message="You do not have permission to perform this action. Ask an admin to upgrade your permissions."
|
||||
>
|
||||
<VerticalGroup spacing="lg">
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text.Title level={3}>OnCall uses Grafana Cloud for SMS and phone call notifications</Text.Title>
|
||||
{syncing ? (
|
||||
<Button variant="secondary" icon="sync" disabled>
|
||||
Updating...
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="secondary" icon="sync" onClick={syncUser} disabled={userStatus === 0}>
|
||||
Update
|
||||
</Button>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<Text.Title level={3}>OnCall uses Grafana Cloud for SMS and phone call notifications</Text.Title>
|
||||
{syncing ? (
|
||||
<Button icon="sync" variant="secondary" disabled>
|
||||
Updating...
|
||||
</Button>
|
||||
) : (
|
||||
<Button icon="sync" variant="secondary" onClick={syncUser} disabled={userStatus === 0}>
|
||||
Reload from Cloud
|
||||
</Button>
|
||||
)}
|
||||
{!syncing ? <UserCloudStatus /> : <LoadingPlaceholder text="Loading..." />}
|
||||
</VerticalGroup>
|
||||
</WithPermissionControlDisplay>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Label } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { InlineField, Input, Legend } from '@grafana/ui';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
|
||||
import { Connectors } from 'containers/UserSettings/parts/connectors';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './UserInfoTab.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface UserInfoTabProps {
|
||||
id: User['pk'];
|
||||
onTabChange: (tab: UserSettingsTab) => void;
|
||||
|
|
@ -25,23 +19,27 @@ export const UserInfoTab = (props: UserInfoTabProps) => {
|
|||
const { userStore } = store;
|
||||
|
||||
const storeUser = userStore.items[id];
|
||||
let width = 12;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('user-item')}>
|
||||
<Text type="secondary">
|
||||
To edit user details such as Username, email, and roles, please visit{' '}
|
||||
<a href="/org/users"> Grafana User settings</a>.
|
||||
</Text>
|
||||
</div>
|
||||
<div className={cx('user-item')}>
|
||||
<Label>Username:</Label>
|
||||
<span className={cx('user-value')}>{storeUser.username || '—'}</span>
|
||||
</div>
|
||||
<div className={cx('user-item')}>
|
||||
<Label>Email:</Label>
|
||||
<span className={cx('user-value')}>{storeUser.email || '—'}</span>
|
||||
</div>
|
||||
<Legend>User information</Legend>
|
||||
<InlineField
|
||||
label="Username"
|
||||
labelWidth={width}
|
||||
grow
|
||||
disabled
|
||||
tooltip="To edit username go to Grafana user management"
|
||||
>
|
||||
<Input value={storeUser.username || ''} />
|
||||
</InlineField>
|
||||
<InlineField label="Email" labelWidth={width} grow disabled tooltip="To edit email go to Grafana user management">
|
||||
<Input value={storeUser.email || ''} />
|
||||
</InlineField>
|
||||
<InlineField label="Timezone" labelWidth={width} grow disabled>
|
||||
<Input value={storeUser.timezone || ''} />
|
||||
</InlineField>
|
||||
<Legend>Notification channels</Legend>
|
||||
<Connectors {...props} />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { Alert, Field, HorizontalGroup, LoadingPlaceholder, VerticalGroup, Icon, Button } from '@grafana/ui';
|
||||
import {
|
||||
Alert,
|
||||
HorizontalGroup,
|
||||
LoadingPlaceholder,
|
||||
VerticalGroup,
|
||||
Icon,
|
||||
Button,
|
||||
InlineField,
|
||||
Input,
|
||||
Legend,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -98,95 +108,87 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('title')}>
|
||||
<Text.Title level={3}>Slack</Text.Title>
|
||||
</div>
|
||||
<div className={cx('slack-settings')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup align="center">
|
||||
<Field label="Slack Workspace">
|
||||
<div className={cx('select', 'control', 'team_workspace')}>
|
||||
<Text>{store.teamStore.currentTeam.slack_team_identity?.cached_name}</Text>
|
||||
</div>
|
||||
</Field>
|
||||
<Field
|
||||
label="Default channel for Slack notifications"
|
||||
description="The selected channel will be used as a fallback in the event that a schedule or integration does not have a configured channel"
|
||||
>
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsUpdateSettings}>
|
||||
<GSelect
|
||||
showSearch
|
||||
className={cx('select', 'control')}
|
||||
modelName="slackChannelStore"
|
||||
displayField="display_name"
|
||||
valueField="id"
|
||||
placeholder="Select Slack Channel"
|
||||
value={teamStore.currentTeam?.slack_channel?.id}
|
||||
onChange={this.handleSlackChannelChange}
|
||||
nullItemName={PRIVATE_CHANNEL_NAME}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</Field>
|
||||
</HorizontalGroup>
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsUpdateSettings}>
|
||||
<WithConfirm
|
||||
title="Remove Slack Integration for all of OnCall"
|
||||
description={
|
||||
<Alert severity="error" title="WARNING">
|
||||
<p>Are you sure to delete this Slack Integration?</p>
|
||||
<p>
|
||||
Removing the integration will also irreverisbly remove the following data for your OnCall plugin:
|
||||
</p>
|
||||
<ul style={{ marginLeft: '20px' }}>
|
||||
<li>default organization Slack channel</li>
|
||||
<li>default Slack channels for OnCall Integrations</li>
|
||||
<li>Slack channels & Slack user groups for OnCall Schedules</li>
|
||||
<li>linked Slack usernames for OnCall Users</li>
|
||||
</ul>
|
||||
<br />
|
||||
<p>
|
||||
If you would like to instead remove your linked Slack username, please head{' '}
|
||||
<PluginLink query={{ page: 'users/me' }}>here</PluginLink>.
|
||||
</p>
|
||||
</Alert>
|
||||
}
|
||||
confirmationText="DELETE"
|
||||
>
|
||||
<Button variant="destructive" size="sm" onClick={() => this.removeSlackIntegration()}>
|
||||
Disconnect
|
||||
</Button>
|
||||
</WithConfirm>
|
||||
<Legend>Slack App settings</Legend>
|
||||
<InlineField label="Slack Workspace" grow disabled>
|
||||
<Input value={store.teamStore.currentTeam.slack_team_identity?.cached_name} />
|
||||
</InlineField>
|
||||
<InlineField
|
||||
label="Default channel for Slack notifications"
|
||||
tooltip="The selected channel will be used as a fallback in the event that a schedule or integration does not have a configured channel"
|
||||
>
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsUpdateSettings}>
|
||||
<GSelect
|
||||
showSearch
|
||||
modelName="slackChannelStore"
|
||||
displayField="display_name"
|
||||
valueField="id"
|
||||
placeholder="Select Slack Channel"
|
||||
value={teamStore.currentTeam?.slack_channel?.id}
|
||||
onChange={this.handleSlackChannelChange}
|
||||
nullItemName={PRIVATE_CHANNEL_NAME}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</InlineField>
|
||||
<Alert
|
||||
severity="info"
|
||||
title="Tip: Create a separate channel for OnCall Slack App notifications (catch-all). Avoid using #general, etc."
|
||||
/>
|
||||
<InlineField>
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsUpdateSettings}>
|
||||
<WithConfirm
|
||||
title="Remove Slack Integration for all of OnCall"
|
||||
description={
|
||||
<Alert severity="error" title="WARNING">
|
||||
<p>Are you sure to delete this Slack Integration?</p>
|
||||
<p>
|
||||
Removing the integration will also irreverisbly remove the following data for your OnCall plugin:
|
||||
</p>
|
||||
<ul style={{ marginLeft: '20px' }}>
|
||||
<li>default organization Slack channel</li>
|
||||
<li>default Slack channels for OnCall Integrations</li>
|
||||
<li>Slack channels & Slack user groups for OnCall Schedules</li>
|
||||
<li>linked Slack usernames for OnCall Users</li>
|
||||
</ul>
|
||||
<br />
|
||||
<p>
|
||||
If you would like to instead remove your linked Slack username, please head{' '}
|
||||
<PluginLink query={{ page: 'users/me' }}>here</PluginLink>.
|
||||
</p>
|
||||
</Alert>
|
||||
}
|
||||
confirmationText="DELETE"
|
||||
>
|
||||
<Button variant="destructive" onClick={() => this.removeSlackIntegration()}>
|
||||
Disconnect Slack App
|
||||
</Button>
|
||||
</WithConfirm>
|
||||
</WithPermissionControlTooltip>
|
||||
</InlineField>
|
||||
<Legend>Additional settings</Legend>
|
||||
<InlineField
|
||||
label="Timeout for acknowledged alerts"
|
||||
tooltip="Slack app will send reminders into alert group slack thread and unacknowledge alert group if no confirmation is received."
|
||||
>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsWrite}>
|
||||
<RemoteSelect
|
||||
showSearch={false}
|
||||
href={'/slack_settings/acknowledge_remind_options/'}
|
||||
value={slackStore.slackSettings?.acknowledge_remind_timeout}
|
||||
onChange={this.getSlackSettingsChangeHandler('acknowledge_remind_timeout')}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsWrite}>
|
||||
<RemoteSelect
|
||||
disabled={slackStore.slackSettings?.acknowledge_remind_timeout === 0}
|
||||
showSearch={false}
|
||||
href={'/slack_settings/unacknowledge_timeout_options/'}
|
||||
value={slackStore.slackSettings?.unacknowledge_timeout}
|
||||
onChange={this.getSlackSettingsChangeHandler('unacknowledge_timeout')}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('slack-settings')}>
|
||||
<Text.Title level={5} className={cx('title')}>
|
||||
Additional settings
|
||||
</Text.Title>
|
||||
<Field label="Timeout for acknowledged alerts">
|
||||
<HorizontalGroup>
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsWrite}>
|
||||
<RemoteSelect
|
||||
className={cx('select')}
|
||||
showSearch={false}
|
||||
href={'/slack_settings/acknowledge_remind_options/'}
|
||||
value={slackStore.slackSettings?.acknowledge_remind_timeout}
|
||||
onChange={this.getSlackSettingsChangeHandler('acknowledge_remind_timeout')}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.ChatOpsWrite}>
|
||||
<RemoteSelect
|
||||
className={cx('select')}
|
||||
disabled={slackStore.slackSettings?.acknowledge_remind_timeout === 0}
|
||||
showSearch={false}
|
||||
href={'/slack_settings/unacknowledge_timeout_options/'}
|
||||
value={slackStore.slackSettings?.unacknowledge_timeout}
|
||||
onChange={this.getSlackSettingsChangeHandler('unacknowledge_timeout')}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</Field>
|
||||
</div>
|
||||
</InlineField>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -257,7 +259,7 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
<SlackNewIcon />
|
||||
</div>
|
||||
<Text className={cx('infoblock-text')}>
|
||||
Slack connection will allow you to manage alert groups in your team Slack workspace.
|
||||
Connecting Slack App will allow you to manage alert groups in your team Slack workspace.
|
||||
</Text>
|
||||
<Text className={cx('infoblock-text')}>
|
||||
After a basic workspace connection your team members need to connect their personal Slack accounts in
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
width: '20%',
|
||||
title: 'Status',
|
||||
key: 'note',
|
||||
render: this.renderNote,
|
||||
render: this.renderStatus,
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
|
|
@ -323,54 +323,68 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
);
|
||||
};
|
||||
|
||||
renderNote = (user: UserType) => {
|
||||
renderStatus = (user: UserType) => {
|
||||
const { store } = this.props;
|
||||
if (user.hidden_fields === true) {
|
||||
return null;
|
||||
}
|
||||
let phone_verified = user.verified_phone_number !== null;
|
||||
let phone_not_verified_message = 'Phone not verified';
|
||||
|
||||
let warnings = [];
|
||||
|
||||
// Show warnining if no notifications are set
|
||||
if (!this.renderNotificationsChain(user)) {
|
||||
warnings.push('No Default Notifications');
|
||||
}
|
||||
|
||||
if (!this.renderImportantNotificationsChain(user)) {
|
||||
warnings.push('No Important Notifications');
|
||||
}
|
||||
|
||||
let phone_verified = user.verified_phone_number !== null;
|
||||
if (user.cloud_connection_status !== null) {
|
||||
phone_verified = false;
|
||||
switch (user.cloud_connection_status) {
|
||||
case 0:
|
||||
phone_not_verified_message = 'Cloud is not synced';
|
||||
// Cloud is not connected, no need to show warning to the user
|
||||
break;
|
||||
case 1:
|
||||
phone_not_verified_message = 'User not matched with cloud';
|
||||
warnings.push('User not matched with cloud');
|
||||
break;
|
||||
case 2:
|
||||
phone_not_verified_message = 'Phone number is not verified in Grafana Cloud';
|
||||
warnings.push('Phone number is not verified in Grafana Cloud');
|
||||
break;
|
||||
case 3:
|
||||
// Phone is verified in Grafana Cloud, no need to show warning to the user
|
||||
phone_verified = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (!phone_verified) {
|
||||
warnings.push('Phone not verified');
|
||||
}
|
||||
}
|
||||
|
||||
if (!phone_verified || !user.slack_user_identity || !user.telegram_configuration) {
|
||||
let texts = [];
|
||||
if (!phone_verified) {
|
||||
texts.push(phone_not_verified_message);
|
||||
}
|
||||
if (!user.slack_user_identity) {
|
||||
texts.push('Slack not verified');
|
||||
}
|
||||
if (store.hasFeature(AppFeature.Telegram) && !user.telegram_configuration) {
|
||||
texts.push('Telegram not verified');
|
||||
}
|
||||
if (store.teamStore.currentTeam.slack_team_identity && !user.slack_user_identity) {
|
||||
warnings.push('Slack profile is not connected');
|
||||
}
|
||||
|
||||
return (
|
||||
let telegramChannelsExist = store.telegramChannelStore.currentTeamToTelegramChannel?.length > 0;
|
||||
|
||||
if (store.hasFeature(AppFeature.Telegram) && telegramChannelsExist && !user.telegram_configuration) {
|
||||
warnings.push('Telegram profile is not connected');
|
||||
}
|
||||
|
||||
return (
|
||||
warnings.length > 0 && (
|
||||
<HorizontalGroup>
|
||||
<TooltipBadge
|
||||
borderType="warning"
|
||||
icon="exclamation-triangle"
|
||||
text={texts.length}
|
||||
text={warnings.length}
|
||||
tooltipTitle="Warnings"
|
||||
tooltipContent={
|
||||
<VerticalGroup spacing="none">
|
||||
{texts.map((warning, index) => (
|
||||
{warnings.map((warning, index) => (
|
||||
<Text type="primary" key={index}>
|
||||
{warning}
|
||||
</Text>
|
||||
|
|
@ -379,10 +393,8 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return 'All contacts verified';
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
debouncedUpdateUsers = debounce(this.updateUsers, 500);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue