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:
Joey Orlando 2023-07-21 17:38:58 +02:00 committed by GitHub
parent 2b2483981f
commit 3d708767dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 233 additions and 157 deletions

View file

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

View file

@ -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):

View file

@ -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",

View file

@ -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,
):

View file

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

View file

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

View file

@ -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 = {

View file

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

View file

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

View file

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

View file

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

View file

@ -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) &&

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) => {

View file

@ -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',

View file

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

View 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,
});
}
}

View file

@ -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: {

View file

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

View file

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

View file

@ -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({

View file

@ -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 = () => {

View file

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

View file

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

View file

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

View file

@ -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(),