Skip to content

Commit

Permalink
Draft UI changes
Browse files Browse the repository at this point in the history
  • Loading branch information
ravishankar15 committed Jun 25, 2024
1 parent aebfc39 commit 6bb6ec8
Show file tree
Hide file tree
Showing 20 changed files with 327 additions and 36 deletions.
9 changes: 9 additions & 0 deletions engine/apps/api/serializers/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,23 @@ class Meta:
class CurrentOrganizationConfigChecksSerializer(serializers.ModelSerializer):
is_chatops_connected = serializers.SerializerMethodField()
is_integration_chatops_connected = serializers.SerializerMethodField()
mattermost = serializers.SerializerMethodField()

class Meta:
model = Organization
fields = [
"is_chatops_connected",
"is_integration_chatops_connected",
"mattermost",
]

def get_mattermost(self, obj):
env_status = not LiveSetting.objects.filter(name__startswith="MATTERMOST", error__isnull=False).exists()
return {
"env_status": env_status,
"is_integrated": False, # TODO: Add logic to verify if mattermost is integrated
}

def get_is_chatops_connected(self, obj):
msteams_backend = get_messaging_backend_from_id("MSTEAMS")
return bool(
Expand Down
12 changes: 12 additions & 0 deletions engine/apps/api/tests/test_organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ def test_get_organization_slack_config_checks(
expected_result = {
"is_chatops_connected": False,
"is_integration_chatops_connected": False,
"mattermost": {
"env_status": True,
"is_integrated": False,
},
}
response = client.get(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
Expand Down Expand Up @@ -310,6 +314,8 @@ def test_get_organization_slack_config_checks(
expected_result["is_integration_chatops_connected"] = True
assert response.json() == expected_result

# TODO: Add test to validate mattermost is integrated once integration PR changes are made


@pytest.mark.django_db
def test_get_organization_telegram_config_checks(
Expand All @@ -326,6 +332,10 @@ def test_get_organization_telegram_config_checks(
expected_result = {
"is_chatops_connected": False,
"is_integration_chatops_connected": False,
"mattermost": {
"env_status": True,
"is_integrated": False,
},
}
response = client.get(url, format="json", **make_user_auth_headers(user, token))
assert response.status_code == status.HTTP_200_OK
Expand Down Expand Up @@ -353,3 +363,5 @@ def test_get_organization_telegram_config_checks(
assert response.status_code == status.HTTP_200_OK
expected_result["is_integration_chatops_connected"] = True
assert response.json() == expected_result

# TODO: Add test to validate mattermost is integrated once integration PR changes are made
4 changes: 2 additions & 2 deletions engine/apps/api/views/organization.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from contextlib import suppress

from django.conf import settings
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
Expand All @@ -10,6 +9,7 @@
from apps.api.serializers.organization import CurrentOrganizationConfigChecksSerializer, CurrentOrganizationSerializer
from apps.auth_token.auth import PluginAuthentication
from apps.base.messaging import get_messaging_backend_from_id
from apps.base.utils import live_settings
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
from apps.telegram.client import TelegramClient
from common.insight_log import EntityEvent, write_resource_insight_log
Expand Down Expand Up @@ -118,7 +118,7 @@ class GetMattermostSetupDetails(APIView):
}

def _create_engine_url(self, auth_token) -> str:
return f"{settings.BASE_URL}/mattermost/manifest?auth_token={auth_token}"
return f"{live_settings.MATTERMOST_WEBHOOK_HOST}/mattermost/manifest?auth_token={auth_token}"

def get(self, request):
organization = request.auth.organization
Expand Down
4 changes: 4 additions & 0 deletions engine/apps/base/models/live_setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class LiveSetting(models.Model):
"TWILIO_VERIFY_SERVICE_SID",
"TELEGRAM_TOKEN",
"TELEGRAM_WEBHOOK_HOST",
"MATTERMOST_WEBHOOK_HOST",
"SLACK_CLIENT_OAUTH_ID",
"SLACK_CLIENT_OAUTH_SECRET",
"SLACK_SIGNING_SECRET",
Expand Down Expand Up @@ -151,6 +152,9 @@ class LiveSetting(models.Model):
"TELEGRAM_WEBHOOK_HOST": (
"Externally available URL for Telegram to make requests. Must use https and ports 80, 88, 443, 8443."
),
"MATTERMOST_WEBHOOK_HOST": (
"Externally available URL for Mattermost to make requests. Must use https and ports 80, 88, 443, 8443."
),
"SEND_ANONYMOUS_USAGE_STATS": (
"Grafana OnCall will send anonymous, but uniquely-identifiable usage analytics to Grafana Labs."
" These statistics are sent to https://stats.grafana.org/. For more information on what's sent, look at the "
Expand Down
56 changes: 25 additions & 31 deletions engine/apps/mattermost/tests/test_api_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from rest_framework import status
from rest_framework.test import APIClient

from apps.base.utils import live_settings

def _create_callback_request_body(token):
return { "context":{ "app": { "app_id": "oncall-app-id" } }, "state": { "auth_token": token } }
return {"context": {"app": {"app_id": "oncall-app-id"}}, "state": {"auth_token": token}}


def _create_manifest_data(auth_token, base_url):
return {
Expand All @@ -14,50 +16,47 @@ def _create_manifest_data(auth_token, base_url):
"display_name": "Grafana OnCall",
"description": "Grafana OnCall app for sending and receiving events from mattermost",
"homepage_url": "https://grafana.com/docs/oncall/latest/",
"requested_permissions": [ "act_as_bot" ],
"requested_locations": [ "/in_post", "/post_menu", "/command" ],
"requested_permissions": ["act_as_bot"],
"requested_locations": ["/in_post", "/post_menu", "/command"],
"on_install": {
"path": "/mattermost/install",
"expand": { "app": "summary", "acting_user": "summary" },
"state": { "auth_token": auth_token }
"expand": {"app": "summary", "acting_user": "summary"},
"state": {"auth_token": auth_token},
},
"bindings": { "path": "/mattermost/bindings", "state": { "auth_token": auth_token } },
"http": { "root_url": base_url }
"bindings": {"path": "/mattermost/bindings", "state": {"auth_token": auth_token}},
"http": {"root_url": base_url},
}


@pytest.mark.django_db
def test_get_manifest_data_success(
settings,
make_organization_and_user,
make_mattermost_app_verification_token_for_user
settings, make_organization_and_user, make_mattermost_app_verification_token_for_user
):
organization, user = make_organization_and_user()
_, token = make_mattermost_app_verification_token_for_user(user, organization)
url = reverse("mattermost:manifest")
live_settings.MATTERMOST_WEBHOOK_HOST = "https://oncallengine.com"

client = APIClient()
response = client.get(url + f"?auth_token={token}")
assert response.status_code == status.HTTP_200_OK

expected_manifest_data = _create_manifest_data(token, settings.BASE_URL)
expected_manifest_data = _create_manifest_data(token, live_settings.MATTERMOST_WEBHOOK_HOST)
assert response.json() == expected_manifest_data


@pytest.mark.django_db
def test_get_manifest_data_forbidden(
make_organization_and_user,
make_mattermost_app_verification_token_for_user
):
def test_get_manifest_data_forbidden(make_organization_and_user, make_mattermost_app_verification_token_for_user):
organization, user = make_organization_and_user()
_, _ = make_mattermost_app_verification_token_for_user(user, organization)
url = reverse("mattermost:manifest")
client = APIClient()
response = client.get(url + f"?auth_token=wrongtoken")
response = client.get(url + "?auth_token=wrongtoken")
assert response.status_code == status.HTTP_403_FORBIDDEN


@pytest.mark.django_db
def test_install_callback_success(
make_organization_and_user,
make_mattermost_app_verification_token_for_user
):
def test_install_callback_success(make_organization_and_user, make_mattermost_app_verification_token_for_user):
organization, user = make_organization_and_user()
_, token = make_mattermost_app_verification_token_for_user(user, organization)
url = reverse("mattermost:install")
Expand All @@ -67,11 +66,9 @@ def test_install_callback_success(
assert response.status_code == status.HTTP_200_OK
assert response.data["type"] == "ok"


@pytest.mark.django_db
def test_bindings_callback_success(
make_organization_and_user,
make_mattermost_app_verification_token_for_user
):
def test_bindings_callback_success(make_organization_and_user, make_mattermost_app_verification_token_for_user):
organization, user = make_organization_and_user()
_, token = make_mattermost_app_verification_token_for_user(user, organization)
url = reverse("mattermost:bindings")
Expand All @@ -81,17 +78,14 @@ def test_bindings_callback_success(
assert response.status_code == status.HTTP_200_OK
assert response.data["type"] == "ok"


@pytest.mark.django_db
@pytest.mark.parametrize("path",["install", "bindings"])
def test_install_callback_forbiden(
make_organization_and_user,
make_mattermost_app_verification_token_for_user,
path
):
@pytest.mark.parametrize("path", ["install", "bindings"])
def test_install_callback_forbiden(make_organization_and_user, make_mattermost_app_verification_token_for_user, path):
organization, user = make_organization_and_user()
_, _ = make_mattermost_app_verification_token_for_user(user, organization)
url = reverse(f"mattermost:{path}")
client = APIClient()
data = _create_callback_request_body("wrongtoken")
response = client.post(url, data, format="json")
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.status_code == status.HTTP_403_FORBIDDEN
3 changes: 2 additions & 1 deletion engine/apps/mattermost/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from apps.base.utils import live_settings
from apps.mattermost.auth import MattermostAuthTokenAuthentication, MattermostWebhookAuthTokenAuthentication

MATTERMOST_CONNECTED_TEXT = (
Expand Down Expand Up @@ -39,7 +40,7 @@ def _build_manifest(self, auth_token: str) -> dict:
"requested_locations": ["/in_post", "/post_menu", "/command"],
"on_install": self._build_on_install_callback(auth_token=auth_token),
"bindings": self._build_bindings_callback(auth_token=auth_token),
"http": {"root_url": settings.BASE_URL},
"http": {"root_url": live_settings.MATTERMOST_WEBHOOK_HOST},
}


Expand Down
2 changes: 2 additions & 0 deletions engine/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
TELEGRAM_WEBHOOK_HOST = os.environ.get("TELEGRAM_WEBHOOK_HOST", BASE_URL)
TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN")

MATTERMOST_WEBHOOK_HOST = os.environ.get("MATTERMOST_WEBHOOK_HOST", BASE_URL)

# For Grafana Cloud integration
GRAFANA_CLOUD_ONCALL_API_URL = os.environ.get(
"GRAFANA_CLOUD_ONCALL_API_URL", "https://oncall-prod-us-central-0.grafana.net/oncall"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.mattermost-block {
width: 100%;
}

.field-command {
margin-top: 8px;
width: 100%;
display: inline-block;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useCallback, useState, useEffect } from 'react';

import { Button, Modal, Icon, HorizontalGroup, VerticalGroup, Field, Input } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import CopyToClipboard from 'react-copy-to-clipboard';

import { Block } from 'components/GBlock/Block';
import { Text } from 'components/Text/Text';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { useStore } from 'state/useStore';
import { UserActions } from 'utils/authorization/authorization';
import { openNotification } from 'utils/utils';

import styles from './MattermostSetupButton.module.css'
const cx = cn.bind(styles)

interface MattermostSetupProps {
disabled?: boolean;
size?: 'md' | 'lg';
onUpdate: () => void;
}

export const MattermostSetupButton = observer((props: MattermostSetupProps) => {
const {disabled, size = 'md', onUpdate } = props;
const [showModal, setShowModal] = useState<boolean>(false);
const onSetupModalHideCallback = useCallback(() => {
setShowModal(false);
}, [])
const onSetupModalCallback = useCallback(() => {
setShowModal(true);
}, [])
const onModalUpdateCallback = useCallback(() => {
setShowModal(false)
onUpdate();
}, [onUpdate]);

return (
<>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<Button size={size} variant="primary" icon="plus" disabled={disabled} onClick={onSetupModalCallback}>
Setup Mattermost
</Button>
</WithPermissionControlTooltip>
{showModal && <MattermostModal onHide={onSetupModalHideCallback} onUpdate={onModalUpdateCallback} />}
</>
)
});

interface MattermostModalProps {
onHide: () => void;
onUpdate: () => void;
}

const MattermostModal = (props: MattermostModalProps) => {
const { onHide, onUpdate } = props;
const store = useStore();
const { mattermostStore } = store

const [manifestLink, setManifestLink] = useState<string>();

useEffect(() => {
(async () => {
const res = await mattermostStore.getMattermostSetupDetails();
setManifestLink(res.manifest_link);
})();
}, []);

return (
<Modal title="Setup Mattermost" closeOnEscape isOpen onDismiss={onUpdate}>
<VerticalGroup spacing="md">
<Block withBackground bordered className={cx('mattermost-block')}>
<Text type="secondary">
Use the following link for the manifest file
<Field className={cx('field-command')}>
<Input
id="mattermostManifestLink"
value={manifestLink}
suffix={
<CopyToClipboard
text={manifestLink}
onCopy={() => {
openNotification('Link is copied')
}}
>
<Icon name="copy"/>
</CopyToClipboard>
}
/>
</Field>
</Text>
</Block>
<HorizontalGroup justify="flex-end">
<Button variant="secondary" onClick={onHide}>
Cancel
</Button>
<Button variant="primary" onClick={onUpdate}>
Done
</Button>
</HorizontalGroup>
</VerticalGroup>
</Modal>
);
};
2 changes: 2 additions & 0 deletions grafana-plugin/src/models/base_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export class BaseStore {

// Update env_status field for current team
await this.rootStore.organizationStore.loadCurrentOrganization();
await this.rootStore.organizationStore.loadCurrentOrganizationConfigChecks();
return result;
} catch (error) {
this.onApiError(error, skipErrorHandling);
Expand All @@ -103,6 +104,7 @@ export class BaseStore {
});
// Update env_status field for current team
await this.rootStore.organizationStore.loadCurrentOrganization();
await this.rootStore.organizationStore.loadCurrentOrganizationConfigChecks();
return result;
} catch (error) {
this.onApiError(error);
Expand Down
18 changes: 18 additions & 0 deletions grafana-plugin/src/models/mattermost/mattermost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { makeObservable } from 'mobx';

import { BaseStore } from "models/base_store";
import { makeRequest } from 'network/network';
import { RootStore } from 'state/rootStore';

export class MattermostStore extends BaseStore {
constructor(rootStore: RootStore) {
super(rootStore);
makeObservable(this);
}

async getMattermostSetupDetails() {
return await makeRequest(`/mattermost/setup/`, {
withCredentials: true
})
}
}
Empty file.
Loading

0 comments on commit 6bb6ec8

Please sign in to comment.