diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a660e7..d917d267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index 2c5588ab..eff8074b 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -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) diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index caa19d82..b8249663 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -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"), diff --git a/grafana-plugin/src/components/Policy/NotificationPolicy.module.css b/grafana-plugin/src/components/Policy/NotificationPolicy.module.css index b8f4eff8..561827ef 100644 --- a/grafana-plugin/src/components/Policy/NotificationPolicy.module.css +++ b/grafana-plugin/src/components/Policy/NotificationPolicy.module.css @@ -13,6 +13,6 @@ } .step .select { - width: 250px !important; + width: 200px !important; flex-shrink: 0; } diff --git a/grafana-plugin/src/containers/Alerts/Alerts.tsx b/grafana-plugin/src/containers/Alerts/Alerts.tsx index 5ca40d88..21a23a47 100644 --- a/grafana-plugin/src/containers/Alerts/Alerts.tsx +++ b/grafana-plugin/src/containers/Alerts/Alerts.tsx @@ -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{' '} - User profile settings to receive all - notifications. - - )} - {!isPhoneVerified && ( - <> - Your phone number is not verified. You can change your configuration in{' '} - User profile settings - - )} + {!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{' '} + User profile settings + } diff --git a/grafana-plugin/src/containers/UserSettings/UserSettings.tsx b/grafana-plugin/src/containers/UserSettings/UserSettings.tsx index 55d162ca..5d3da0ac 100644 --- a/grafana-plugin/src/containers/UserSettings/UserSettings.tsx +++ b/grafana-plugin/src/containers/UserSettings/UserSettings.tsx @@ -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 = ( + +

{storeUser.username}

+
+ ); + return ( <> - +
{ await userStore.deleteiCalLink(id); }; + const isCurrentUser = id === store.userStore.currentUserPk; + return ( -
- - - Secret iCal export link to add your assigned on call shifts to your calendar. -
- NOTE: We do not have control over when a client refreshes an imported calendar. -
-
- {iCalLoading ? ( - - ) : ( - <> - {isiCalLinkExisting ? ( - <> - {showiCalLink !== undefined ? ( - <> -
- {' '} - Make sure you copy it - you won't be able to access it again. -
{showiCalLink}
-
- { - openNotification('iCal link is copied'); - }} - > - - - - ) : ( - <> - - In case you lost your iCal link you can revoke it and generate a new one. - - - - - - )} - - ) : ( - - + + + + + + ) : ( + <> + + + + + + + + + + + + + + )} + + ) : ( + + + - - )} - - )} -
-
+ + + )} + + )} + + ); }; diff --git a/grafana-plugin/src/containers/UserSettings/parts/connectors/MobileAppConnector.tsx b/grafana-plugin/src/containers/UserSettings/parts/connectors/MobileAppConnector.tsx index 9d036526..87c5ee30 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/connectors/MobileAppConnector.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/connectors/MobileAppConnector.tsx @@ -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 ( -
- -
- -
-
+ ) : ( + + )} + ); }; diff --git a/grafana-plugin/src/containers/UserSettings/parts/connectors/PhoneConnector.tsx b/grafana-plugin/src/containers/UserSettings/parts/connectors/PhoneConnector.tsx index 8b2e57c0..434a9ba9 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/connectors/PhoneConnector.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/connectors/PhoneConnector.tsx @@ -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 Cloud is not synced; + return ( + <> + + + + + + ); case 1: return ( - - User is not matched with cloud - - + <> + + + + + ); case 2: return ( - - Phone number is not verified in Grafana Cloud - - + <> + + + + + ); case 3: return ( - - Phone number verified - - + <> + + + + + ); default: return ( - - User is not matched with cloud - - + <> + + + + + ); } }; return ( -
+
{store.hasFeature(AppFeature.CloudNotifications) ? ( - <> - - {cloudVersionPhone(storeUser)} - + <>{cloudVersionPhone(storeUser)} ) : ( <> - - {storeUser.verified_phone_number || '—'} {storeUser.verified_phone_number ? (
- Phone number is verified - + + + + {isCurrentUser ? ( +
) : storeUser.unverified_phone_number ? (
- Phone number is not verified - + + + + {isCurrentUser ? ( + + ) : ( + + + + )} + + +
) : (
- Phone number is not added - + + {isCurrentUser ? ( + + ) : ( + + + + )} +
)} diff --git a/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx b/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx index 18bd27e8..67d9ab7d 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx @@ -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 ( -
- - {storeUser.slack_user_identity?.name || '—'} + <> {storeUser.slack_user_identity ? ( -
- Slack account is connected - {storeUser.pk === userStore.currentUserPk ? ( - - + + + ) : ( + <> + + + - ) : ( - '' - )} -
- ) : teamStore.currentTeam?.slack_team_identity ? ( -
- Slack account is not connected - {isCurrent && ( - - )} -
- ) : ( -
- Slack Integration is not installed - - - -
+ + )} -
+ ); }; diff --git a/grafana-plugin/src/containers/UserSettings/parts/connectors/TelegramConnector.tsx b/grafana-plugin/src/containers/UserSettings/parts/connectors/TelegramConnector.tsx index 40515cfa..08953a3a 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/connectors/TelegramConnector.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/connectors/TelegramConnector.tsx @@ -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 ( -
- - {storeUser.telegram_configuration?.telegram_nick_name || '—'} - {storeUser.telegram_configuration && storeUser.pk === userStore.currentUserPk ? ( -
- Telegram account is connected - {storeUser.pk === userStore.currentUserPk ? ( +
+ + {storeUser.telegram_configuration ? ( + + - +
- ) : ( -
- Telegram account is not connected - {isCurrent && ( - - )} -
- )} + + ) : ( + + )} +
); }; diff --git a/grafana-plugin/src/containers/UserSettings/parts/connectors/index.tsx b/grafana-plugin/src/containers/UserSettings/parts/connectors/index.tsx index 6ab5c0d1..5e65b3cd 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/connectors/index.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/connectors/index.tsx @@ -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 = (props) => { const store = useStore(); return ( -
+ <> + {store.hasFeature(AppFeature.Telegram) && } + Calendar export - -
+ ); }; diff --git a/grafana-plugin/src/containers/UserSettings/parts/index.tsx b/grafana-plugin/src/containers/UserSettings/parts/index.tsx index cf0eb994..b5723b39 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/index.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/index.tsx @@ -116,10 +116,10 @@ export const TabsContent = observer(({ id, activeTab, onTabChange, isDesktopOrLa {activeTab === UserSettingsTab.UserInfo && (isDesktopOrLaptop ? (
- + - +
diff --git a/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx b/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx index acc4cb25..e2259795 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx @@ -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(0); const [userLink, setUserLink] = useState(null); + const email = store.userStore.items[userPk].email; + useEffect(() => { getCloudUserInfo(); }, []); @@ -63,9 +65,8 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => { return ( - { - '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}). - ) : ( - - )} - + OnCall uses Grafana Cloud for SMS and phone call notifications + {syncing ? ( + + ) : ( + + )} {!syncing ? : } diff --git a/grafana-plugin/src/containers/UserSettings/parts/tabs/UserInfoTab/UserInfoTab.tsx b/grafana-plugin/src/containers/UserSettings/parts/tabs/UserInfoTab/UserInfoTab.tsx index a506af70..3b04d308 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/tabs/UserInfoTab/UserInfoTab.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/tabs/UserInfoTab/UserInfoTab.tsx @@ -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 ( <> -
- - To edit user details such as Username, email, and roles, please visit{' '} - Grafana User settings. - -
-
- - {storeUser.username || '—'} -
-
- - {storeUser.email || '—'} -
+ User information + + + + + + + + + + Notification channels ); diff --git a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx index 1f3950b9..65a60462 100644 --- a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx +++ b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx @@ -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 { return (
-
- Slack -
-
- - - -
- {store.teamStore.currentTeam.slack_team_identity?.cached_name} -
-
- - - - - -
- - -

Are you sure to delete this Slack Integration?

-

- Removing the integration will also irreverisbly remove the following data for your OnCall plugin: -

-
    -
  • default organization Slack channel
  • -
  • default Slack channels for OnCall Integrations
  • -
  • Slack channels & Slack user groups for OnCall Schedules
  • -
  • linked Slack usernames for OnCall Users
  • -
-
-

- If you would like to instead remove your linked Slack username, please head{' '} - here. -

- - } - confirmationText="DELETE" - > - -
+ Slack App settings + + + + + + + + + + + + +

Are you sure to delete this Slack Integration?

+

+ Removing the integration will also irreverisbly remove the following data for your OnCall plugin: +

+
    +
  • default organization Slack channel
  • +
  • default Slack channels for OnCall Integrations
  • +
  • Slack channels & Slack user groups for OnCall Schedules
  • +
  • linked Slack usernames for OnCall Users
  • +
+
+

+ If you would like to instead remove your linked Slack username, please head{' '} + here. +

+
+ } + confirmationText="DELETE" + > + + +
+ + Additional settings + + + + + + + -
-
- - Additional settings - - - - - - - - - - - -
+
); }; @@ -257,7 +259,7 @@ class SlackSettings extends Component {
- 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. After a basic workspace connection your team members need to connect their personal Slack accounts in diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index b44bed61..2284e9a5 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -141,7 +141,7 @@ class Users extends React.Component { width: '20%', title: 'Status', key: 'note', - render: this.renderNote, + render: this.renderStatus, }, { width: '20%', @@ -323,54 +323,68 @@ class Users extends React.Component { ); }; - 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 && ( - {texts.map((warning, index) => ( + {warnings.map((warning, index) => ( {warning} @@ -379,10 +393,8 @@ class Users extends React.Component { } /> - ); - } - - return 'All contacts verified'; + ) + ); }; debouncedUpdateUsers = debounce(this.updateUsers, 500);