add rbac_permissions to current-user response + rbac_enabled to current-org response (#2611)
# What this PR does unblocks https://github.com/grafana/oncall-mobile-app/issues/152 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
This commit is contained in:
parent
2b2483981f
commit
3d708767dc
31 changed files with 233 additions and 157 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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/<backend>/", auth.overridden_login_slack_auth, name="slack-auth"),
|
||||
path(r"complete/<backend>/", auth.overridden_complete_slack_auth, name="complete-slack-auth"),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ const ScheduleUserDetails: FC<ScheduleUserDetailsProps> = (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 (
|
||||
<div className={cx('root')}>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
<HorizontalGroup>
|
||||
{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
|
||||
) ? (
|
||||
<Text type="secondary">
|
||||
default slack channel is{' '}
|
||||
<Text strong>#{getSlackChannelName(store.teamStore.currentTeam?.slack_channel)}</Text>{' '}
|
||||
default slack channel is <Text strong>#{getSlackChannelName(currentOrganization?.slack_channel)}</Text>{' '}
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
|
@ -82,8 +84,8 @@ const SlackConnector = (props: SlackConnectorProps) => {
|
|||
fill="text"
|
||||
onClick={() => {
|
||||
handleSlackChannelChange(
|
||||
teamStore.currentTeam?.slack_channel?.id,
|
||||
teamStore.currentTeam?.slack_channel
|
||||
currentOrganization?.slack_channel?.id,
|
||||
currentOrganization?.slack_channel
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
|
@ -91,7 +93,7 @@ const SlackConnector = (props: SlackConnectorProps) => {
|
|||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</Text>
|
||||
) : teamStore.currentTeam?.slack_channel?.id ? (
|
||||
) : currentOrganization?.slack_channel?.id ? (
|
||||
<Text type="secondary">
|
||||
This is the default slack channel{' '}
|
||||
<PluginLink query={{ page: 'chat-ops' }} disabled={!isUserActionAllowed(UserActions.ChatOpsWrite)}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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))}
|
||||
</Alert>
|
||||
)}
|
||||
{showBannerTeam() && (
|
||||
<Alert
|
||||
className={cx('alert')}
|
||||
severity="success"
|
||||
title={currentTeam.banner.title}
|
||||
onRemove={getRemoveAlertHandler(currentTeam?.banner.title)}
|
||||
title={currentOrganization.banner.title}
|
||||
onRemove={getRemoveAlertHandler(currentOrganization?.banner.title)}
|
||||
>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(currentTeam?.banner.body),
|
||||
__html: sanitize(currentOrganization?.banner.body),
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
|
|
@ -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) &&
|
||||
|
|
|
|||
|
|
@ -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 <b>{team.slack_team_identity.cached_name}</b> workspace when connecting please
|
||||
Select <b>{organization.slack_team_identity.cached_name}</b> workspace when connecting please
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
</HorizontalGroup>
|
||||
</InlineField>
|
||||
</>
|
||||
) : teamStore.currentTeam?.slack_team_identity ? (
|
||||
) : organizationStore.currentOrganization?.slack_team_identity ? (
|
||||
<>
|
||||
<InlineField
|
||||
label="Slack"
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const PHONE_REGEX = /^\+\d{8,15}$/;
|
|||
const PhoneVerification = observer((props: PhoneVerificationProps) => {
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ interface TelegramInfoProps extends HTMLAttributes<HTMLElement> {}
|
|||
|
||||
const TelegramInfo = observer((_props: TelegramInfoProps) => {
|
||||
const store = useStore();
|
||||
const { userStore, teamStore } = store;
|
||||
const { userStore, organizationStore } = store;
|
||||
|
||||
const [verificationCode, setVerificationCode] = useState<string>();
|
||||
const [botLink, setBotLink] = useState<string>();
|
||||
|
||||
const telegramConfigured = teamStore.currentTeam?.env_status.telegram_configured;
|
||||
const telegramConfigured = organizationStore.currentOrganization?.env_status.telegram_configured;
|
||||
|
||||
useEffect(() => {
|
||||
userStore.sendTelegramConfirmationCode(userStore.currentUserPk).then((res) => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
grafana-plugin/src/models/organization/organization.ts
Normal file
30
grafana-plugin/src/models/organization/organization.ts
Normal file
|
|
@ -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<Organization>) {
|
||||
this.currentOrganization = await makeRequest(this.path, {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -92,25 +92,27 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { store } = this.props;
|
||||
const { teamStore } = store;
|
||||
const { currentOrganization } = this.props.store.organizationStore;
|
||||
|
||||
if (!teamStore.currentTeam) {
|
||||
if (!currentOrganization) {
|
||||
return <LoadingPlaceholder text="Loading..." />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={cx('root')}>
|
||||
<Legend>Slack App settings</Legend>
|
||||
<InlineField label="Slack Workspace" grow disabled>
|
||||
<Input value={store.teamStore.currentTeam.slack_team_identity?.cached_name} />
|
||||
<Input value={currentOrganization.slack_team_identity?.cached_name} />
|
||||
</InlineField>
|
||||
<InlineField
|
||||
label="Default channel for Slack notifications"
|
||||
|
|
@ -123,7 +125,7 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
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<SlackProps, SlackState> {
|
|||
|
||||
renderSlackWorkspace = () => {
|
||||
const { store } = this.props;
|
||||
return <Text>{store.teamStore.currentTeam.slack_team_identity?.cached_name}</Text>;
|
||||
return <Text>{store.organizationStore.currentOrganization.slack_team_identity?.cached_name}</Text>;
|
||||
};
|
||||
|
||||
renderSlackChannels = () => {
|
||||
|
|
@ -209,7 +211,7 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
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<SlackProps, SlackState> {
|
|||
store.slackStore
|
||||
.removeSlackIntegration()
|
||||
.then(() => {
|
||||
store.teamStore.loadCurrentTeam();
|
||||
store.organizationStore.loadCurrentOrganization();
|
||||
})
|
||||
.catch(showApiError);
|
||||
};
|
||||
|
|
@ -242,7 +244,7 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
|
||||
await slackStore.setGeneralLogChannelId(value);
|
||||
|
||||
store.teamStore.loadCurrentTeam();
|
||||
store.organizationStore.loadCurrentOrganization();
|
||||
};
|
||||
|
||||
renderSlackStub = () => {
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ class TelegramSettings extends Component<TelegramProps, TelegramState> {
|
|||
|
||||
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 (
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ class SettingsPage extends React.Component<SettingsPageProps, SettingsPageState>
|
|||
}
|
||||
|
||||
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<SettingsPageProps, SettingsPageState>
|
|||
Resolution Note
|
||||
</Text.Title>
|
||||
<Field
|
||||
loading={!teamStore.currentTeam}
|
||||
loading={!currentOrganization}
|
||||
label="Require a resolution note when resolving Alert Groups"
|
||||
description={`Once user clicks "Resolve" for an Alert Group, they will be required to fill in a resolution note about the Alert Group`}
|
||||
>
|
||||
<WithPermissionControlTooltip userAction={UserActions.OtherSettingsWrite}>
|
||||
<Switch
|
||||
value={teamStore.currentTeam?.is_resolution_note_required}
|
||||
value={currentOrganization?.is_resolution_note_required}
|
||||
onChange={(event) => {
|
||||
teamStore.saveCurrentTeam({
|
||||
organizationStore.saveCurrentOrganization({
|
||||
is_resolution_note_required: event.currentTarget.checked,
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue