diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a27dde..b359f5cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow persisting mobile app's timezone, to allow for more accurate datetime related notifications by @joeyorlando ([#2601](https://github.com/grafana/oncall/pull/2601)) - Add filter integrations by type ([2609](https://github.com/grafana/oncall/pull/2609)) +- Add `rbac_enabled` to `GET /api/internal/v1/current_team` response schema + `rbac_permissions` to `GET /api/internal/v1/user` + response schema by @joeyorlando ([#2611](https://github.com/grafana/oncall/pull/2611)) ### Changed diff --git a/engine/apps/api/serializers/organization.py b/engine/apps/api/serializers/organization.py index 67c8e34b..96c36169 100644 --- a/engine/apps/api/serializers/organization.py +++ b/engine/apps/api/serializers/organization.py @@ -23,6 +23,8 @@ class OrganizationSerializer(EagerLoadingMixin, serializers.ModelSerializer): name = serializers.CharField(required=False, allow_null=True, allow_blank=True, source="org_title") slack_channel = serializers.SerializerMethodField() + rbac_enabled = serializers.BooleanField(source="is_rbac_permissions_enabled") + SELECT_RELATED = ["slack_team_identity"] class Meta: @@ -32,9 +34,11 @@ class OrganizationSerializer(EagerLoadingMixin, serializers.ModelSerializer): "name", "slack_team_identity", "slack_channel", + "rbac_enabled", ] read_only_fields = [ "slack_team_identity", + "rbac_enabled", ] def get_slack_channel(self, obj): diff --git a/engine/apps/api/serializers/user.py b/engine/apps/api/serializers/user.py index 1164954f..8acaa7f3 100644 --- a/engine/apps/api/serializers/user.py +++ b/engine/apps/api/serializers/user.py @@ -21,6 +21,10 @@ from .organization import FastOrganizationSerializer from .slack_user_identity import SlackUserIdentitySerializer +class UserPermissionSerializer(serializers.Serializer): + action = serializers.CharField(read_only=True) + + class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin): pk = serializers.CharField(read_only=True, source="public_primary_key") slack_user_identity = SlackUserIdentitySerializer(read_only=True) @@ -156,6 +160,17 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin): return f"{HIDE_SYMBOL * (len(number) - SHOW_LAST_SYMBOLS)}{number[-SHOW_LAST_SYMBOLS:]}" +class CurrentUserSerializer(UserSerializer): + rbac_permissions = UserPermissionSerializer(read_only=True, many=True, source="permissions") + + class Meta: + model = User + fields = UserSerializer.Meta.fields + [ + "rbac_permissions", + ] + read_only_fields = UserSerializer.Meta.read_only_fields + + class UserHiddenFieldsSerializer(UserSerializer): fields_available_for_all_users = [ "pk", diff --git a/engine/apps/api/tests/test_organization.py b/engine/apps/api/tests/test_organization.py index 518c9f5a..9b342d59 100644 --- a/engine/apps/api/tests/test_organization.py +++ b/engine/apps/api/tests/test_organization.py @@ -9,6 +9,23 @@ from rest_framework.test import APIClient from apps.api.permissions import LegacyAccessControlRole +@pytest.mark.django_db +@pytest.mark.parametrize("rbac_enabled", [True, False]) +def test_get_organization_rbac_enabled( + make_organization_and_user_with_plugin_token, make_user_auth_headers, rbac_enabled +): + organization, user, token = make_organization_and_user_with_plugin_token() + organization.is_rbac_permissions_enabled = rbac_enabled + organization.save() + + client = APIClient() + url = reverse("api-internal:api-organization") + + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert response.json()["rbac_enabled"] == rbac_enabled + + @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", @@ -18,7 +35,7 @@ from apps.api.permissions import LegacyAccessControlRole (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) -def test_current_team_retrieve_permissions( +def test_organization_retrieve_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, role, @@ -27,7 +44,7 @@ def test_current_team_retrieve_permissions( _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() - url = reverse("api-internal:api-current-team") + url = reverse("api-internal:api-organization") with patch( "apps.api.views.organization.CurrentOrganizationView.get", return_value=Response( @@ -48,7 +65,7 @@ def test_current_team_retrieve_permissions( (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) -def test_current_team_update_permissions( +def test_organization_update_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, role, @@ -57,7 +74,7 @@ def test_current_team_update_permissions( _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() - url = reverse("api-internal:api-current-team") + url = reverse("api-internal:api-organization") with patch( "apps.api.views.organization.CurrentOrganizationView.put", @@ -79,7 +96,7 @@ def test_current_team_update_permissions( (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) -def test_current_team_get_telegram_verification_code_permissions( +def test_organization_get_telegram_verification_code_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, role, @@ -103,7 +120,7 @@ def test_current_team_get_telegram_verification_code_permissions( (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) -def test_current_team_get_channel_verification_code_permissions( +def test_organization_get_channel_verification_code_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, role, @@ -119,7 +136,7 @@ def test_current_team_get_channel_verification_code_permissions( @pytest.mark.django_db -def test_current_team_get_channel_verification_code_ok( +def test_organization_get_channel_verification_code_ok( make_organization_and_user_with_plugin_token, make_user_auth_headers, ): @@ -139,7 +156,7 @@ def test_current_team_get_channel_verification_code_ok( @pytest.mark.django_db -def test_current_team_get_channel_verification_code_invalid( +def test_organization_get_channel_verification_code_invalid( make_organization_and_user_with_plugin_token, make_user_auth_headers, ): diff --git a/engine/apps/api/tests/test_user.py b/engine/apps/api/tests/test_user.py index 8c9cd843..47a6ece9 100644 --- a/engine/apps/api/tests/test_user.py +++ b/engine/apps/api/tests/test_user.py @@ -23,6 +23,51 @@ def clear_cache(): cache.clear() +@pytest.mark.django_db +def test_current_user(make_organization_and_user_with_plugin_token, make_user_auth_headers): + organization, user, token = make_organization_and_user_with_plugin_token() + + client = APIClient() + url = reverse("api-internal:api-user") + + expected_response = { + "pk": user.public_primary_key, + "organization": {"pk": organization.public_primary_key, "name": organization.org_title}, + "current_team": None, + "email": user.email, + "hide_phone_number": False, + "username": user.username, + "name": user.name, + "role": user.role, + "rbac_permissions": user.permissions, + "timezone": None, + "working_hours": default_working_hours(), + "unverified_phone_number": None, + "verified_phone_number": None, + "telegram_configuration": None, + "messaging_backends": { + "TESTONLY": { + "user": user.username, + } + }, + "cloud_connection_status": 0, + "notification_chain_verbal": {"default": "", "important": ""}, + "slack_user_identity": None, + "avatar": user.avatar_url, + "avatar_full": user.avatar_full_url, + } + + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert response.json() == expected_response + + data_to_update = {"hide_phone_number": True} + + response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert response.json() == expected_response | data_to_update + + @pytest.mark.django_db def test_update_user( make_organization, diff --git a/engine/apps/api/urls.py b/engine/apps/api/urls.py index aa971b27..1ce3e260 100644 --- a/engine/apps/api/urls.py +++ b/engine/apps/api/urls.py @@ -69,6 +69,8 @@ urlpatterns = [ path("", include(router.urls)), optional_slash_path("user", CurrentUserView.as_view(), name="api-user"), optional_slash_path("set_general_channel", SetGeneralChannel.as_view(), name="api-set-general-log-channel"), + optional_slash_path("organization", CurrentOrganizationView.as_view(), name="api-organization"), + # TODO: remove current_team routes in future release optional_slash_path("current_team", CurrentOrganizationView.as_view(), name="api-current-team"), optional_slash_path( "current_team/get_telegram_verification_code", @@ -106,5 +108,3 @@ urlpatterns += [ path(r"login//", auth.overridden_login_slack_auth, name="slack-auth"), path(r"complete//", auth.overridden_complete_slack_auth, name="complete-slack-auth"), ] - -urlpatterns += router.urls diff --git a/engine/apps/api/views/organization.py b/engine/apps/api/views/organization.py index bce742b1..4da8396a 100644 --- a/engine/apps/api/views/organization.py +++ b/engine/apps/api/views/organization.py @@ -10,12 +10,16 @@ from apps.api.permissions import RBACPermission from apps.api.serializers.organization import CurrentOrganizationSerializer from apps.auth_token.auth import PluginAuthentication from apps.base.messaging import get_messaging_backend_from_id +from apps.mobile_app.auth import MobileAppAuthTokenAuthentication from apps.telegram.client import TelegramClient from common.insight_log import EntityEvent, write_resource_insight_log class CurrentOrganizationView(APIView): - authentication_classes = (PluginAuthentication,) + authentication_classes = ( + MobileAppAuthTokenAuthentication, + PluginAuthentication, + ) permission_classes = (IsAuthenticated, RBACPermission) rbac_permissions = { diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index 629990a6..2adbb6d6 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -25,7 +25,12 @@ from apps.api.permissions import ( user_is_authorized, ) from apps.api.serializers.team import TeamSerializer -from apps.api.serializers.user import FilterUserSerializer, UserHiddenFieldsSerializer, UserSerializer +from apps.api.serializers.user import ( + CurrentUserSerializer, + FilterUserSerializer, + UserHiddenFieldsSerializer, + UserSerializer, +) from apps.api.throttlers import ( GetPhoneVerificationCodeThrottlerPerOrg, GetPhoneVerificationCodeThrottlerPerUser, @@ -79,10 +84,7 @@ UPCOMING_SHIFTS_MAX_DAYS = 65 class CurrentUserView(APIView): - authentication_classes = ( - MobileAppAuthTokenAuthentication, - PluginAuthentication, - ) + authentication_classes = (MobileAppAuthTokenAuthentication, PluginAuthentication) permission_classes = (IsAuthenticated,) def get(self, request): @@ -98,12 +100,12 @@ class CurrentUserView(APIView): context["cloud_identities"] = cloud_identities context["connector"] = connector - serializer = UserSerializer(request.user, context=context) + serializer = CurrentUserSerializer(request.user, context=context) return Response(serializer.data) def put(self, request): data = self.request.data - serializer = UserSerializer(request.user, data=data, context={"request": self.request}) + serializer = CurrentUserSerializer(request.user, data=data, context={"request": self.request}) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) diff --git a/grafana-plugin/src/components/ScheduleUserDetails/ScheduleUserDetails.tsx b/grafana-plugin/src/components/ScheduleUserDetails/ScheduleUserDetails.tsx index e5a11b01..759760c4 100644 --- a/grafana-plugin/src/components/ScheduleUserDetails/ScheduleUserDetails.tsx +++ b/grafana-plugin/src/components/ScheduleUserDetails/ScheduleUserDetails.tsx @@ -36,8 +36,9 @@ const ScheduleUserDetails: FC = (props) => { const colorSchemeMapping = getColorSchemeMappingForUsers(store, scheduleId, startMoment); const colorSchemeList = Array.from(colorSchemeMapping[user.pk] || []); - const { teamStore } = store; - const slackWorkspaceName = teamStore.currentTeam.slack_team_identity?.cached_name?.replace(/[^0-9a-z]/gi, '') || ''; + const { organizationStore } = store; + const slackWorkspaceName = + organizationStore.currentOrganization.slack_team_identity?.cached_name?.replace(/[^0-9a-z]/gi, '') || ''; return (
diff --git a/grafana-plugin/src/containers/AlertRules/parts/connectors/SlackConnector.tsx b/grafana-plugin/src/containers/AlertRules/parts/connectors/SlackConnector.tsx index 403df04e..f1beb3f4 100644 --- a/grafana-plugin/src/containers/AlertRules/parts/connectors/SlackConnector.tsx +++ b/grafana-plugin/src/containers/AlertRules/parts/connectors/SlackConnector.tsx @@ -26,7 +26,10 @@ const SlackConnector = (props: SlackConnectorProps) => { const { channelFilterId } = props; const store = useStore(); - const { teamStore, alertReceiveChannelStore } = store; + const { + organizationStore: { currentOrganization }, + alertReceiveChannelStore, + } = store; const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId]; @@ -61,7 +64,7 @@ const SlackConnector = (props: SlackConnectorProps) => { displayField="display_name" valueField="id" placeholder="Select Slack Channel" - value={channelFilter.slack_channel?.id || teamStore.currentTeam?.slack_channel?.id} + value={channelFilter.slack_channel?.id || currentOrganization?.slack_channel?.id} onChange={handleSlackChannelChange} nullItemName={PRIVATE_CHANNEL_NAME} /> @@ -69,12 +72,11 @@ const SlackConnector = (props: SlackConnectorProps) => { {Boolean( channelFilter.slack_channel?.id && - teamStore.currentTeam?.slack_channel?.id && - channelFilter.slack_channel?.id !== teamStore.currentTeam?.slack_channel?.id + currentOrganization?.slack_channel?.id && + channelFilter.slack_channel?.id !== currentOrganization?.slack_channel?.id ) ? ( - default slack channel is{' '} - #{getSlackChannelName(store.teamStore.currentTeam?.slack_channel)}{' '} + default slack channel is #{getSlackChannelName(currentOrganization?.slack_channel)}{' '} - ) : teamStore.currentTeam?.slack_channel?.id ? ( + ) : currentOrganization?.slack_channel?.id ? ( This is the default slack channel{' '} diff --git a/grafana-plugin/src/containers/AlertRules/parts/index.tsx b/grafana-plugin/src/containers/AlertRules/parts/index.tsx index 13268e44..00f9c97d 100644 --- a/grafana-plugin/src/containers/AlertRules/parts/index.tsx +++ b/grafana-plugin/src/containers/AlertRules/parts/index.tsx @@ -19,9 +19,9 @@ export const ChatOpsConnectors = (props: ChatOpsConnectorsProps) => { const { channelFilterId, showLineNumber = true } = props; const store = useStore(); - const { telegramChannelStore } = store; + const { telegramChannelStore, organizationStore } = store; - const isSlackInstalled = Boolean(store.teamStore.currentTeam?.slack_team_identity); + const isSlackInstalled = Boolean(organizationStore.currentOrganization?.slack_team_identity); const isTelegramInstalled = store.hasFeature(AppFeature.Telegram) && telegramChannelStore.currentTeamToTelegramChannel?.length > 0; diff --git a/grafana-plugin/src/containers/Alerts/Alerts.tsx b/grafana-plugin/src/containers/Alerts/Alerts.tsx index e8b19d66..f14bdecc 100644 --- a/grafana-plugin/src/containers/Alerts/Alerts.tsx +++ b/grafana-plugin/src/containers/Alerts/Alerts.tsx @@ -53,11 +53,10 @@ export default function Alerts() { }, []); const store = useStore(); - - const { userStore, teamStore } = store; - - const { currentTeam } = teamStore; - const { currentUser } = userStore; + const { + userStore: { currentUser }, + organizationStore: { currentOrganization }, + } = store; const isChatOpsConnected = getIfChatOpsConnected(currentUser); const isPhoneVerified = currentUser?.cloud_connection_status === 3 || currentUser?.verified_phone_number; @@ -77,23 +76,19 @@ export default function Alerts() { severity="error" title="Slack integration error" > - {getSlackMessage( - showSlackInstallAlert, - store.teamStore.currentTeam, - store.hasFeature(AppFeature.LiveSettings) - )} + {getSlackMessage(showSlackInstallAlert, currentOrganization, store.hasFeature(AppFeature.LiveSettings))} )} {showBannerTeam() && (
@@ -147,7 +142,7 @@ export default function Alerts() { ); function showBannerTeam(): boolean { - return currentTeam?.banner.title != null && !getItem(currentTeam?.banner.title); + return currentOrganization?.banner.title != null && !getItem(currentOrganization?.banner.title); } function showMismatchWarning(): boolean { @@ -162,7 +157,7 @@ export default function Alerts() { function showChannelWarnings(): boolean { return Boolean( - currentTeam && + currentOrganization && currentUser && isUserActionAllowed(UserActions.UserSettingsWrite) && (!isPhoneVerified || !isChatOpsConnected) && diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.helpers.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.helpers.tsx index fffca1f8..133251bc 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.helpers.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.helpers.tsx @@ -1,19 +1,19 @@ import React from 'react'; import PluginLink from 'components/PluginLink/PluginLink'; -import { Team } from 'models/team/team.types'; +import { Organization } from 'models/organization/organization.types'; import { SlackError } from './DefaultPageLayout.types'; -export function getSlackMessage(slackError: SlackError, team: Team, hasLiveSettingsFeature: boolean) { +export function getSlackMessage(slackError: SlackError, organization: Organization, hasLiveSettingsFeature: boolean) { if (slackError === SlackError.WRONG_WORKSPACE) { return ( <> Couldn't connect Slack. - {Boolean(team?.slack_team_identity) && ( + {Boolean(organization?.slack_team_identity) && ( <> {' '} - Select {team.slack_team_identity.cached_name} workspace when connecting please + Select {organization.slack_team_identity.cached_name} workspace when connecting please )} diff --git a/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx b/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx index 7af65132..2a826ad0 100644 --- a/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx +++ b/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx @@ -56,7 +56,7 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => { ); const escalationPolicyIds = escalationPolicyStore.escalationChainToEscalationPolicy[id]; - const isSlackInstalled = Boolean(store.teamStore.currentTeam?.slack_team_identity); + const isSlackInstalled = Boolean(store.organizationStore.currentOrganization?.slack_team_identity); return ( // @ts-ignore diff --git a/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx b/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx index 636d1192..6133730f 100644 --- a/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx +++ b/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx @@ -141,7 +141,7 @@ const PersonalNotificationSettings = observer((props: PersonalNotificationSettin phoneStatus={getPhoneStatus()} isMobileAppConnected={isMobileAppConnected} showCloudConnectionWarning={showCloudConnectionWarning} - slackTeamIdentity={store.teamStore.currentTeam?.slack_team_identity} + slackTeamIdentity={store.organizationStore.currentOrganization?.slack_team_identity} slackUserIdentity={user.slack_user_identity} data={notificationPolicy} onChange={getNotificationPolicyUpdateHandler} diff --git a/grafana-plugin/src/containers/UserSettings/UserSettings.tsx b/grafana-plugin/src/containers/UserSettings/UserSettings.tsx index 5d3da0ac..96cf5413 100644 --- a/grafana-plugin/src/containers/UserSettings/UserSettings.tsx +++ b/grafana-plugin/src/containers/UserSettings/UserSettings.tsx @@ -28,7 +28,7 @@ interface UserFormProps { const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserInfo }: UserFormProps) => { const store = useStore(); - const { userStore, teamStore } = store; + const { userStore, organizationStore } = store; const storeUser = userStore.items[id]; const isCurrent = id === store.userStore.currentUserPk; @@ -51,7 +51,7 @@ const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserInfo }: U const [showNotificationSettingsTab, showSlackConnectionTab, showTelegramConnectionTab, showMobileAppConnectionTab] = [ !isDesktopOrLaptop, - isCurrent && teamStore.currentTeam?.slack_team_identity && !storeUser.slack_user_identity, + isCurrent && organizationStore.currentOrganization?.slack_team_identity && !storeUser.slack_user_identity, isCurrent && store.hasFeature(AppFeature.Telegram) && !storeUser.telegram_configuration, isCurrent, ]; diff --git a/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx b/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx index 67d9ab7d..b52c39e1 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/connectors/SlackConnector.tsx @@ -17,7 +17,7 @@ const SlackConnector = (props: SlackConnectorProps) => { const { id, onTabChange } = props; const store = useStore(); - const { userStore, teamStore } = store; + const { userStore, organizationStore } = store; const storeUser = userStore.items[id]; @@ -61,7 +61,7 @@ const SlackConnector = (props: SlackConnectorProps) => { - ) : teamStore.currentTeam?.slack_team_identity ? ( + ) : organizationStore.currentOrganization?.slack_team_identity ? ( <> { const { userPk: propsUserPk } = props; const store = useStore(); - const { userStore, teamStore } = store; + const { userStore, organizationStore } = store; const userPk = (propsUserPk || userStore.currentUserPk) as User['pk']; const user = userStore.items[userPk]; @@ -158,8 +158,8 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => { }); }, [code, userPk, userStore.verifyPhone, userStore.loadUser]); - const isPhoneProviderConfigured = teamStore.currentTeam?.env_status.phone_provider?.configured; - const providerConfiguration = teamStore.currentTeam?.env_status.phone_provider; + const providerConfiguration = organizationStore.currentOrganization?.env_status.phone_provider; + const isPhoneProviderConfigured = providerConfiguration?.configured; const phoneHasMinimumLength = phone?.length > 8; diff --git a/grafana-plugin/src/containers/UserSettings/parts/tabs/TelegramInfo/TelegramInfo.tsx b/grafana-plugin/src/containers/UserSettings/parts/tabs/TelegramInfo/TelegramInfo.tsx index 9eb68b46..278961ed 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/tabs/TelegramInfo/TelegramInfo.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/tabs/TelegramInfo/TelegramInfo.tsx @@ -24,12 +24,12 @@ interface TelegramInfoProps extends HTMLAttributes {} const TelegramInfo = observer((_props: TelegramInfoProps) => { const store = useStore(); - const { userStore, teamStore } = store; + const { userStore, organizationStore } = store; const [verificationCode, setVerificationCode] = useState(); const [botLink, setBotLink] = useState(); - const telegramConfigured = teamStore.currentTeam?.env_status.telegram_configured; + const telegramConfigured = organizationStore.currentOrganization?.env_status.telegram_configured; useEffect(() => { userStore.sendTelegramConfirmationCode(userStore.currentUserPk).then((res) => { diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index fb81d578..c7149193 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -6,9 +6,9 @@ import { AlertTemplatesDTO } from 'models/alert_templates'; import { Alert } from 'models/alertgroup/alertgroup.types'; import BaseStore from 'models/base_store'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; +import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; import { Heartbeat } from 'models/heartbeat/heartbeat.types'; import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; -import { Team } from 'models/team/team.types'; import { makeRequest } from 'network'; import { Mixpanel } from 'services/mixpanel'; import { RootStore } from 'state'; @@ -443,7 +443,7 @@ export class AlertReceiveChannelStore extends BaseStore { }); } - async changeTeam(id: AlertReceiveChannel['id'], teamId: Team['pk']) { + async changeTeam(id: AlertReceiveChannel['id'], teamId: GrafanaTeam['id']) { return await makeRequest(`${this.path}${id}/change_team`, { params: { team_id: String(teamId) }, method: 'PUT', diff --git a/grafana-plugin/src/models/base_store.ts b/grafana-plugin/src/models/base_store.ts index 766cb698..1a59ae8b 100644 --- a/grafana-plugin/src/models/base_store.ts +++ b/grafana-plugin/src/models/base_store.ts @@ -65,7 +65,7 @@ export default class BaseStore { }).catch(this.onApiError); // Update env_status field for current team - await this.rootStore.teamStore.loadCurrentTeam(); + await this.rootStore.organizationStore.loadCurrentOrganization(); return result; } @@ -76,7 +76,7 @@ export default class BaseStore { }).catch(this.onApiError); // Update env_status field for current team - await this.rootStore.teamStore.loadCurrentTeam(); + await this.rootStore.organizationStore.loadCurrentOrganization(); return result; } } diff --git a/grafana-plugin/src/models/organization/organization.ts b/grafana-plugin/src/models/organization/organization.ts new file mode 100644 index 00000000..0c2ac3dd --- /dev/null +++ b/grafana-plugin/src/models/organization/organization.ts @@ -0,0 +1,30 @@ +import { action, observable } from 'mobx'; + +import BaseStore from 'models/base_store'; +import { makeRequest } from 'network'; +import { RootStore } from 'state'; + +import { Organization } from './organization.types'; + +export class OrganizationStore extends BaseStore { + @observable + currentOrganization?: Organization; + + constructor(rootStore: RootStore) { + super(rootStore); + this.path = '/organization/'; + } + + @action + async loadCurrentOrganization() { + this.currentOrganization = await makeRequest(this.path, {}); + } + + @action + async saveCurrentOrganization(data: Partial) { + this.currentOrganization = await makeRequest(this.path, { + method: 'PUT', + data, + }); + } +} diff --git a/grafana-plugin/src/models/team/team.types.ts b/grafana-plugin/src/models/organization/organization.types.ts similarity index 83% rename from grafana-plugin/src/models/team/team.types.ts rename to grafana-plugin/src/models/organization/organization.types.ts index 315a53ae..0c6a158e 100644 --- a/grafana-plugin/src/models/team/team.types.ts +++ b/grafana-plugin/src/models/organization/organization.types.ts @@ -1,15 +1,15 @@ import { SlackChannel } from 'models/slack_channel/slack_channel.types'; -export interface Team { +export interface Organization { pk: string; banner: { title: string; body: string; }; telegram_configuration: { - channel_chat_id: number; + channel_chat_id: string; channel_name: string; - discussion_group_chat_id: number; // TODO check if string + discussion_group_chat_id: string; discussion_group_name: string; }; name: string; @@ -18,12 +18,8 @@ export interface Team { general_log_channel_pk: string; cached_name: string; }; - slack_channel: SlackChannel | null; - - // ex team settings is_resolution_note_required: boolean; - env_status: { telegram_configured: boolean; phone_provider: { diff --git a/grafana-plugin/src/models/team/team.ts b/grafana-plugin/src/models/team/team.ts deleted file mode 100644 index 22abcbac..00000000 --- a/grafana-plugin/src/models/team/team.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { action, observable } from 'mobx'; - -import BaseStore from 'models/base_store'; -import { makeRequest } from 'network'; -import { RootStore } from 'state'; - -import { Team } from './team.types'; - -export class TeamStore extends BaseStore { - @observable - redirectingToProperTeam = false; - - @observable.shallow - teams: { [id: number]: Team[] } = {}; - - @observable - currentTeam?: Team; - - constructor(rootStore: RootStore) { - super(rootStore); - - this.path = '/team/'; - } - - @action - async loadCurrentTeam() { - this.currentTeam = await makeRequest('/current_team/', {}); - } - - @action - async saveCurrentTeam(data: any) { - this.currentTeam = await makeRequest('/current_team/', { - method: 'PUT', - data, - }); - } -} diff --git a/grafana-plugin/src/models/user/user.types.ts b/grafana-plugin/src/models/user/user.types.ts index 58de40b2..87ab5be3 100644 --- a/grafana-plugin/src/models/user/user.types.ts +++ b/grafana-plugin/src/models/user/user.types.ts @@ -1,4 +1,3 @@ -import { Team } from 'models/team/team.types'; import { Timezone } from 'models/timezone/timezone.types'; export interface MessagingBackends { @@ -36,7 +35,6 @@ export interface User { slack_login: string; } | null; post_onboarding_entry_allowed: any; - teams: Team[]; current_team: string | null; onboarding_conversation_data: { image_link: string | null; diff --git a/grafana-plugin/src/pages/integration/Integration.helper.ts b/grafana-plugin/src/pages/integration/Integration.helper.ts index 7fb9688b..09ae8b58 100644 --- a/grafana-plugin/src/pages/integration/Integration.helper.ts +++ b/grafana-plugin/src/pages/integration/Integration.helper.ts @@ -60,7 +60,7 @@ const IntegrationHelper = { }, hasChatopsInstalled(store: RootStore) { - const hasSlack = Boolean(store.teamStore.currentTeam?.slack_team_identity); + const hasSlack = Boolean(store.organizationStore.currentOrganization?.slack_team_identity); const hasTelegram = store.hasFeature(AppFeature.Telegram) && store.telegramChannelStore.currentTeamToTelegramChannel?.length > 0; return hasSlack || hasTelegram; @@ -70,8 +70,8 @@ const IntegrationHelper = { const channels: Array<{ name: string; icon: IconName }> = []; if (store.hasFeature(AppFeature.Slack) && channelFilter.notify_in_slack) { - const matchingSlackChannel = store.teamStore.currentTeam?.slack_channel?.id - ? store.slackChannelStore.items[store.teamStore.currentTeam.slack_channel?.id] + const matchingSlackChannel = store.organizationStore.currentOrganization?.slack_channel?.id + ? store.slackChannelStore.items[store.organizationStore.currentOrganization.slack_channel?.id] : undefined; if (channelFilter.slack_channel?.display_name || matchingSlackChannel?.display_name) { channels.push({ 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 65a60462..db046d78 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 @@ -92,25 +92,27 @@ class SlackSettings extends Component { }; render() { - const { store } = this.props; - const { teamStore } = store; + const { currentOrganization } = this.props.store.organizationStore; - if (!teamStore.currentTeam) { + if (!currentOrganization) { return ; } - return teamStore.currentTeam?.slack_team_identity ? this.renderSlackIntegration() : this.renderSlackStub(); + return currentOrganization?.slack_team_identity ? this.renderSlackIntegration() : this.renderSlackStub(); } renderSlackIntegration = () => { const { store } = this.props; - const { teamStore, slackStore } = store; + const { + organizationStore: { currentOrganization }, + slackStore, + } = store; return (
Slack App settings - + { displayField="display_name" valueField="id" placeholder="Select Slack Channel" - value={teamStore.currentTeam?.slack_channel?.id} + value={currentOrganization?.slack_channel?.id} onChange={this.handleSlackChannelChange} nullItemName={PRIVATE_CHANNEL_NAME} /> @@ -195,7 +197,7 @@ class SlackSettings extends Component { renderSlackWorkspace = () => { const { store } = this.props; - return {store.teamStore.currentTeam.slack_team_identity?.cached_name}; + return {store.organizationStore.currentOrganization.slack_team_identity?.cached_name}; }; renderSlackChannels = () => { @@ -209,7 +211,7 @@ class SlackSettings extends Component { displayField="display_name" valueField="id" placeholder="Select Slack Channel" - value={store.teamStore.currentTeam?.slack_channel?.id} + value={store.organizationStore.currentOrganization?.slack_channel?.id} onChange={this.handleSlackChannelChange} nullItemName={PRIVATE_CHANNEL_NAME} /> @@ -222,7 +224,7 @@ class SlackSettings extends Component { store.slackStore .removeSlackIntegration() .then(() => { - store.teamStore.loadCurrentTeam(); + store.organizationStore.loadCurrentOrganization(); }) .catch(showApiError); }; @@ -242,7 +244,7 @@ class SlackSettings extends Component { await slackStore.setGeneralLogChannelId(value); - store.teamStore.loadCurrentTeam(); + store.organizationStore.loadCurrentOrganization(); }; renderSlackStub = () => { diff --git a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/TelegramSettings/TelegramSettings.tsx b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/TelegramSettings/TelegramSettings.tsx index 707a58af..0cd4ab58 100644 --- a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/TelegramSettings/TelegramSettings.tsx +++ b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/TelegramSettings/TelegramSettings.tsx @@ -41,10 +41,10 @@ class TelegramSettings extends Component { render() { const { store } = this.props; - const { telegramChannelStore, teamStore } = store; + const { telegramChannelStore, organizationStore } = store; const connectedChannels = telegramChannelStore.getSearchResult(); - const telegramConfigured = teamStore.currentTeam?.env_status.telegram_configured; + const telegramConfigured = organizationStore.currentOrganization?.env_status.telegram_configured; if (!telegramConfigured && store.hasFeature(AppFeature.LiveSettings)) { return ( diff --git a/grafana-plugin/src/pages/settings/tabs/MainSettings/MainSettings.tsx b/grafana-plugin/src/pages/settings/tabs/MainSettings/MainSettings.tsx index e7ec42ab..ecad3f36 100644 --- a/grafana-plugin/src/pages/settings/tabs/MainSettings/MainSettings.tsx +++ b/grafana-plugin/src/pages/settings/tabs/MainSettings/MainSettings.tsx @@ -37,8 +37,8 @@ class SettingsPage extends React.Component } render() { - const { store } = this.props; - const { teamStore } = store; + const { organizationStore } = this.props.store; + const { currentOrganization } = organizationStore; const { apiUrl } = this.state; return ( @@ -54,15 +54,15 @@ class SettingsPage extends React.Component Resolution Note { - teamStore.saveCurrentTeam({ + organizationStore.saveCurrentOrganization({ is_resolution_note_required: event.currentTarget.checked, }); }} diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index 31df49ca..c04f5804 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -364,7 +364,7 @@ class Users extends React.Component { } } - if (store.teamStore.currentTeam.slack_team_identity && !user.slack_user_identity) { + if (store.organizationStore.currentOrganization.slack_team_identity && !user.slack_user_identity) { warnings.push('Slack profile is not connected'); } diff --git a/grafana-plugin/src/state/rootBaseStore/index.ts b/grafana-plugin/src/state/rootBaseStore/index.ts index 3cb525f7..cf9cebc9 100644 --- a/grafana-plugin/src/state/rootBaseStore/index.ts +++ b/grafana-plugin/src/state/rootBaseStore/index.ts @@ -18,13 +18,13 @@ import { FiltersStore } from 'models/filters/filters'; import { GlobalSettingStore } from 'models/global_setting/global_setting'; import { GrafanaTeamStore } from 'models/grafana_team/grafana_team'; import { HeartbeatStore } from 'models/heartbeat/heartbeat'; +import { OrganizationStore } from 'models/organization/organization'; import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook'; import { OutgoingWebhook2Store } from 'models/outgoing_webhook_2/outgoing_webhook_2'; import { ResolutionNotesStore } from 'models/resolution_note/resolution_note'; import { ScheduleStore } from 'models/schedule/schedule'; import { SlackStore } from 'models/slack/slack'; import { SlackChannelStore } from 'models/slack_channel/slack_channel'; -import { TeamStore } from 'models/team/team'; import { TelegramChannelStore } from 'models/telegram_channel/telegram_channel'; import { Timezone } from 'models/timezone/timezone.types'; import { UserStore } from 'models/user/user'; @@ -80,29 +80,29 @@ export class RootBaseStore { // -------------------------- - userStore: UserStore = new UserStore(this); - cloudStore: CloudStore = new CloudStore(this); - directPagingStore: DirectPagingStore = new DirectPagingStore(this); - grafanaTeamStore: GrafanaTeamStore = new GrafanaTeamStore(this); - alertReceiveChannelStore: AlertReceiveChannelStore = new AlertReceiveChannelStore(this); - outgoingWebhookStore: OutgoingWebhookStore = new OutgoingWebhookStore(this); + userStore = new UserStore(this); + cloudStore = new CloudStore(this); + directPagingStore = new DirectPagingStore(this); + grafanaTeamStore = new GrafanaTeamStore(this); + alertReceiveChannelStore = new AlertReceiveChannelStore(this); + outgoingWebhookStore = new OutgoingWebhookStore(this); - outgoingWebhook2Store: OutgoingWebhook2Store = new OutgoingWebhook2Store(this); - alertReceiveChannelFiltersStore: AlertReceiveChannelFiltersStore = new AlertReceiveChannelFiltersStore(this); - escalationChainStore: EscalationChainStore = new EscalationChainStore(this); - escalationPolicyStore: EscalationPolicyStore = new EscalationPolicyStore(this); - teamStore: TeamStore = new TeamStore(this); - telegramChannelStore: TelegramChannelStore = new TelegramChannelStore(this); - slackStore: SlackStore = new SlackStore(this); - slackChannelStore: SlackChannelStore = new SlackChannelStore(this); - heartbeatStore: HeartbeatStore = new HeartbeatStore(this); - scheduleStore: ScheduleStore = new ScheduleStore(this); - userGroupStore: UserGroupStore = new UserGroupStore(this); - alertGroupStore: AlertGroupStore = new AlertGroupStore(this); - resolutionNotesStore: ResolutionNotesStore = new ResolutionNotesStore(this); - apiTokenStore: ApiTokenStore = new ApiTokenStore(this); - globalSettingStore: GlobalSettingStore = new GlobalSettingStore(this); - filtersStore: FiltersStore = new FiltersStore(this); + outgoingWebhook2Store = new OutgoingWebhook2Store(this); + alertReceiveChannelFiltersStore = new AlertReceiveChannelFiltersStore(this); + escalationChainStore = new EscalationChainStore(this); + escalationPolicyStore = new EscalationPolicyStore(this); + organizationStore = new OrganizationStore(this); + telegramChannelStore = new TelegramChannelStore(this); + slackStore = new SlackStore(this); + slackChannelStore = new SlackChannelStore(this); + heartbeatStore = new HeartbeatStore(this); + scheduleStore = new ScheduleStore(this); + userGroupStore = new UserGroupStore(this); + alertGroupStore = new AlertGroupStore(this); + resolutionNotesStore = new ResolutionNotesStore(this); + apiTokenStore = new ApiTokenStore(this); + globalSettingStore = new GlobalSettingStore(this); + filtersStore = new FiltersStore(this); // stores async updateBasicData() { @@ -117,7 +117,7 @@ export class RootBaseStore { }; return Promise.all([ - this.teamStore.loadCurrentTeam(), + this.organizationStore.loadCurrentOrganization(), this.grafanaTeamStore.updateItems(), updateFeatures(), this.userStore.updateNotificationPolicyOptions(),