diff --git a/grafana-plugin/src/containers/UserSettings/parts/index.tsx b/grafana-plugin/src/containers/UserSettings/parts/index.tsx index 7f966bfc..62f14daf 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/index.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/index.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react'; import { Tab, TabContent, TabsBar } from '@grafana/ui'; import cn from 'classnames/bind'; +import { observer } from 'mobx-react'; import Block from 'components/GBlock/Block'; import MobileAppVerification from 'containers/MobileAppVerification/MobileAppVerification'; @@ -13,6 +14,7 @@ import PhoneVerification from 'containers/UserSettings/parts/tabs/PhoneVerificat import TelegramInfo from 'containers/UserSettings/parts/tabs/TelegramInfo/TelegramInfo'; import { UserInfoTab } from 'containers/UserSettings/parts/tabs/UserInfoTab/UserInfoTab'; import { User } from 'models/user/user.types'; +import { AppFeature } from 'state/features'; import { useStore } from 'state/useStore'; import styles from 'containers/UserSettings/parts/index.module.css'; @@ -101,12 +103,11 @@ interface TabsContentProps { isDesktopOrLaptop: boolean; } -export const TabsContent = (props: TabsContentProps) => { +export const TabsContent = observer((props: TabsContentProps) => { const { id, activeTab, onTabChange, isDesktopOrLaptop } = props; const store = useStore(); const { userStore } = store; - const [isPhoneEnabled, setIsPhoneEnabled] = useState(false); const storeUser = userStore.items[id]; @@ -127,7 +128,7 @@ export const TabsContent = (props: TabsContentProps) => { ))} {activeTab === UserSettingsTab.NotificationSettings && } {activeTab === UserSettingsTab.PhoneVerification && - (isPhoneEnabled ? ( + (store.hasFeature(AppFeature.CloudNotifications) ? ( ) : ( @@ -139,4 +140,4 @@ export const TabsContent = (props: TabsContentProps) => { {activeTab === UserSettingsTab.TelegramInfo && } ); -}; +}); diff --git a/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx b/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx index dd833057..a5ea00f1 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx @@ -1,8 +1,20 @@ import React, { useCallback, useEffect, useState } from 'react'; import { getLocationSrv, LocationUpdate } from '@grafana/runtime'; -import { Field, Input, Button, Modal, HorizontalGroup, Alert, Icon, VerticalGroup, Table } from '@grafana/ui'; +import { + Field, + Input, + Button, + Modal, + HorizontalGroup, + Alert, + Icon, + VerticalGroup, + Table, + LoadingPlaceholder, +} from '@grafana/ui'; import cn from 'classnames/bind'; +import { observer } from 'mobx-react'; import Block from 'components/GBlock/Block'; import GTable from 'components/GTable/GTable'; @@ -20,66 +32,106 @@ const cx = cn.bind(styles); interface CloudPhoneSettingsProps extends WithStoreProps {} -const CloudPhoneSettings = (props: CloudPhoneSettingsProps) => { +const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => { const store = useStore(); const [isAccountMatched, setIsAccountMatched] = useState(true); const [isPhoneVerified, setIsPhoneVerified] = useState(true); + const [userStatus, setUserStatus] = useState(0); + const [userLink, setUserLink] = useState(null); - const signUpGrafanaCloud = () => { - console.log('Sign UP'); + useEffect(() => { + getCloudUserInfo(); + }, []); + + const handleLinkClick = (link: string) => { + getLocationSrv().update({ partial: false, path: link }); }; - const handleLinkClick = () => { + + const syncUser = () => { store.cloudStore.syncCloudUser(store.userStore.currentUserPk); }; + const getCloudUserInfo = async () => { + await store.cloudStore.updateItems(); + const { count, results } = await store.cloudStore.getSearchResult(); + console.log('RES', results); + const cloudUser = + results && (await results.find((element: { id: string }) => element.id === store.userStore.currentUserPk)); + console.log('CLOUD USER', cloudUser); + setUserStatus(cloudUser?.cloud_data?.status); + setUserLink(cloudUser?.cloud_data?.link); + }; + + const UserCloudStatus = () => { + switch (userStatus) { + case 0: + return ( + + Grafana Cloud is not synced + + ); + case 1: + return ( + + + { + 'We can’t find a matching account in the connected Grafana Cloud instance (matching happens by e-mail). ' + } + + + + ); + case 2: + return ( + + + Your account successfully matched with the Grafana Cloud account. Please verify your phone number.{' '} + + + + ); + case 3: + return ( + + + Your account successfully matched with the Grafana Cloud account. Your phone number is verified.{' '} + + + + ); + default: + return ( + + + { + 'We can’t find a matching account in the connected Grafana Cloud instance (matching happens by e-mail). ' + } + + + + ); + } + }; + return ( OnCall use Grafana Cloud for SMS and phone call notifications - - {isAccountMatched ? ( - isPhoneVerified ? ( - - - Your account successfully matched with the Grafana Cloud account. Please verify your phone number.{' '} - - - - ) : ( - - - Your account successfully matched with the Grafana Cloud account. Your phone number is verified. - - - - ) - ) : ( - - - {'We can’t find a matching account in the connected Grafana Cloud instance (matching happens by e-mail). '} - - - - )} + {userStatus ? : } ); -}; +}); export default withMobXProviderContext(CloudPhoneSettings); diff --git a/grafana-plugin/src/models/cloud/cloud.ts b/grafana-plugin/src/models/cloud/cloud.ts index f41512dd..f1dd54f2 100644 --- a/grafana-plugin/src/models/cloud/cloud.ts +++ b/grafana-plugin/src/models/cloud/cloud.ts @@ -59,18 +59,15 @@ export class CloudStore extends BaseStore { } async syncCloudUser(id: string) { - return await makeRequest(`${this.path}${id}/sync_with_cloud/`, { method: 'POST' }); + return await makeRequest(`${this.path}${id}/sync/`, { method: 'POST' }); } async getCloudConnectionStatus() { return await makeRequest(`/cloud_connection/`, { method: 'GET' }); } - @action - async connectToCloud(token: string) {} - @action async disconnectToCloud() { - return await makeRequest(`/live_settings/`, { method: 'DELETE' }); + return await makeRequest(`/cloud_connection/`, { method: 'DELETE' }); } } diff --git a/grafana-plugin/src/models/global_setting/global_setting.ts b/grafana-plugin/src/models/global_setting/global_setting.ts index a7e6deb0..edcb2986 100644 --- a/grafana-plugin/src/models/global_setting/global_setting.ts +++ b/grafana-plugin/src/models/global_setting/global_setting.ts @@ -60,4 +60,9 @@ export class GlobalSettingStore extends BaseStore { return this.searchResult[query].map((globalSettingId: GlobalSetting['id']) => this.items[globalSettingId]); } + + async getGlobalSettingItemByName(name: string) { + const results = await this.getAll(); + return results.find((element: { name: string }) => element.name === name); + } } diff --git a/grafana-plugin/src/pages/cloud/CloudPage.module.css b/grafana-plugin/src/pages/cloud/CloudPage.module.css index ba98f153..14f11ba5 100644 --- a/grafana-plugin/src/pages/cloud/CloudPage.module.css +++ b/grafana-plugin/src/pages/cloud/CloudPage.module.css @@ -37,6 +37,22 @@ color: var(--secondary-text-color); } +.error-icon { + display: inline-block; + white-space: break-spaces; + line-height: 20px; + color: var(--error-text-color); +} + +.error-icon svg { + vertical-align: middle; +} + +.heart-icon { + color: var(--secondary-text-color); + margin-right: 8px; +} + .block-button { margin-top: 24px; } diff --git a/grafana-plugin/src/pages/cloud/CloudPage.tsx b/grafana-plugin/src/pages/cloud/CloudPage.tsx index 5aa1b8fa..5c185597 100644 --- a/grafana-plugin/src/pages/cloud/CloudPage.tsx +++ b/grafana-plugin/src/pages/cloud/CloudPage.tsx @@ -1,8 +1,20 @@ import React, { useCallback, useEffect, useState } from 'react'; import { getLocationSrv, LocationUpdate } from '@grafana/runtime'; -import { Field, Input, Button, Modal, HorizontalGroup, Alert, Icon, VerticalGroup, Table } from '@grafana/ui'; +import { + Field, + Input, + Button, + Modal, + HorizontalGroup, + Alert, + Icon, + VerticalGroup, + Table, + LoadingPlaceholder, +} from '@grafana/ui'; import cn from 'classnames/bind'; +import { observer } from 'mobx-react'; import Block from 'components/GBlock/Block'; import GTable from 'components/GTable/GTable'; @@ -14,36 +26,45 @@ import { Cloud } from 'models/cloud/cloud.types'; import { WithStoreProps } from 'state/types'; import { useStore } from 'state/useStore'; import { withMobXProviderContext } from 'state/withStore'; +import { openErrorNotification } from 'utils'; import styles from './CloudPage.module.css'; const cx = cn.bind(styles); interface CloudPageProps extends WithStoreProps {} +const ITEMS_PER_PAGE = 1; -const CloudPage = (props: CloudPageProps) => { +const CloudPage = observer((props: CloudPageProps) => { const store = useStore(); + const [page, setPage] = useState(1); const [cloudApiKey, setCloudApiKey] = useState(''); - const [cloudIsConnected, setCloudIsConnected] = useState(true); + const [apiKeyError, setApiKeyError] = useState(false); + const [cloudIsConnected, setCloudIsConnected] = useState(undefined); + const [heartbitLink, setHeartbitLink] = useState(null); + const [heartbitStatus, setHeartbitStatus] = useState(false); const [showConfirmationModal, setShowConfirmationModal] = useState(false); + const [syncingUsers, setSyncingUsers] = useState(false); useEffect(() => { - store.cloudStore.updateItems(); + store.cloudStore.updateItems(page); store.cloudStore.getCloudConnectionStatus().then((cloudStatus) => { setCloudIsConnected(cloudStatus.cloud_connection_status); + setHeartbitStatus(cloudStatus.cloud_heartbeat_enabled); + setHeartbitLink(cloudStatus.cloud_heartbeat_link); }); }, []); - const data = [ - { id: 'yshanyrova', email: 'y.shanyrova@grafana.com', cloud_data: { status: 2, link: '/test/abc' } }, - { id: 'amixradmin', email: 'amixr-admin@grafana.com', cloud_data: { status: 1, link: '/test/abc' } }, - { id: 'amixr', email: 'amixr@grafana.com', cloud_data: { status: undefined, link: '/test/abc' } }, - ]; + const { count, results } = store.cloudStore.getSearchResult(); - // const { count, results } = store.cloudStore.getSearchResult(); + const handleChangePage = (page: number) => { + setPage(page); + store.cloudStore.updateItems(page); + }; const handleChangeCloudApiKey = useCallback((e) => { setCloudApiKey(e.target.value); + setApiKeyError(false); }, []); const saveKeyAndConnect = () => { @@ -51,20 +72,32 @@ const CloudPage = (props: CloudPageProps) => { }; const disconnectCloudOncall = () => { - console.log('disconnected'); setCloudIsConnected(false); store.cloudStore.disconnectToCloud(); }; - const connectToCloud = () => { - setCloudIsConnected(true); + const connectToCloud = async () => { setShowConfirmationModal(false); - // store.cloudStore.update('') - store.cloudStore.connectToCloud(cloudApiKey); + const globalSettingItem = await store.globalSettingStore.getGlobalSettingItemByName('GRAFANA_CLOUD_ONCALL_TOKEN'); + store.globalSettingStore + .update(globalSettingItem?.id, { name: 'GRAFANA_CLOUD_ONCALL_TOKEN', value: cloudApiKey }) + .then((response) => { + if (response.error) { + setCloudIsConnected(false); + setApiKeyError(true); + openErrorNotification(response.error); + } else { + setCloudIsConnected(true); + syncUsers(); + } + }); }; - const syncUsers = () => { - store.cloudStore.syncCloudUsers(); + const syncUsers = async () => { + setSyncingUsers(true); + await store.cloudStore.syncCloudUsers(); + await store.cloudStore.updateItems(); + setSyncingUsers(false); }; const handleLinkClick = (link: string) => { @@ -76,17 +109,7 @@ const CloudPage = (props: CloudPageProps) => { case 0: return null; case 1: - return ( - - ); + return null; case 2: return ( ); + case 3: + return ( + + ); default: return null; } @@ -107,12 +142,14 @@ const CloudPage = (props: CloudPageProps) => { const renderStatus = (user: Cloud) => { switch (user?.cloud_data?.status) { case 0: - return User not found in the Grafana Cloud; + return Grafana Cloud is not synced; case 1: - return Phone number verified; - + return User not found in Grafana Cloud; case 2: return Phone number is not verified in Grafana Cloud; + case 3: + return Phone number verified; + default: return User not found in Grafana Cloud; } @@ -122,20 +159,26 @@ const CloudPage = (props: CloudPageProps) => { switch (user?.cloud_data?.status) { case 0: return ( - +
- +
); case 1: - return ; + return ( +
+ +
+ ); case 2: return ; + case 3: + return ; default: return ( - +
- +
); } }; @@ -168,40 +211,158 @@ const CloudPage = (props: CloudPageProps) => { }, ]; + const ConnectedBlock = ( + + + + + Cloud OnCall API key + + Cloud OnCall is sucessfully connected. + + + + + + + + + + + + + Monitor cloud instance with heartbeat + + + Once connected, current OnCall instance will send heartbeats every 3 minutes to the cloud Instance. If no + heartbeat will be received in 10 minutes, cloud instance will issue an alert. + + {heartbitStatus && heartbitLink && ( + + )} + + + + + + SMS and phone call notifications + + +
+ + { + 'Ask your users to sign up in Grafana Cloud, verify phone number and feel free to set up SMS & phone call notificaitons in personal settings!' + } + + + ( +
+ + + {count ? count : 0} + {` users matched between OSS and Cloud OnCall`} + + {syncingUsers ? ( + + ) : ( + + )} + +
+ )} + rowKey="id" + // @ts-ignore + columns={columns} + data={results} + pagination={{ + page, + total: Math.ceil((count || 0) / ITEMS_PER_PAGE), + onChange: handleChangePage, + }} + /> +
+
+
+
+ ); + + const DisconnectedBlock = ( + + + + + Cloud OnCall API key + + + + + + + + + + + + + {' '} + Monitor cloud instance with heartbeat + + + Once connected, current OnCall instance will send heartbeats every 3 minutes to the cloud Instance. If no + heartbeat will be received in 10 minutes, cloud instance will issue an alert. + + + + + + + SMS and phone call notifications + + + Users matched between OSS and Cloud OnCall currently unavialable. + + + + ); + return (
Connect Open Source OnCall and Cloud OnCall - - {cloudIsConnected ? ( - - - Cloud OnCall API key - - Cloud OnCall is sucessfully connected. - - - - - - ) : ( - - - Cloud OnCall API key - - - - - - - )} - + {cloudIsConnected === undefined ? ( + + ) : cloudIsConnected ? ( + ConnectedBlock + ) : ( + DisconnectedBlock + )} {showConfirmationModal && ( { )} - - - - - - - {' '} - Monitor cloud instance with heartbeat - - - Once connected, current OnCall instance will send heartbeats every 3 minutes to the cloud Instance. If no - heartbeat will be received in 10 minutes, cloud instance will issue an alert. - - {cloudIsConnected && ( - - )} - - - - - - - SMS and phone call notifications - - {cloudIsConnected ? ( -
- - { - 'Ask your users to sign up in Grafana Cloud, verify phone number and feel free to set up SMS & phone call notificaitons in personal settings!' - } - - - ( -
- - - {/* {count ? count : 0} */} - {`3 users matched between OSS and Cloud OnCall`} - - - -
- )} - rowKey="id" - // @ts-ignore - columns={columns} - data={data} - /> -
- ) : ( - Users matched between OSS and Cloud OnCall currently unavialable. - )} -
-
); -}; +}); export default withMobXProviderContext(CloudPage); diff --git a/grafana-plugin/src/plugin.json b/grafana-plugin/src/plugin.json index 4f5132f1..38a4d7bc 100644 --- a/grafana-plugin/src/plugin.json +++ b/grafana-plugin/src/plugin.json @@ -98,6 +98,13 @@ "path": "/a/grafana-oncall-app/?page=outgoing_webhooks", "role": "Viewer", "addToNav": true + }, + { + "type": "page", + "name": "Cloud", + "path": "/a/grafana-oncall-app/?page=cloud", + "role": "Editor", + "addToNav": true } ], "routes": [ diff --git a/grafana-plugin/src/state/features.ts b/grafana-plugin/src/state/features.ts index 8363575c..91a92d8c 100644 --- a/grafana-plugin/src/state/features.ts +++ b/grafana-plugin/src/state/features.ts @@ -3,4 +3,5 @@ export enum AppFeature { Telegram = 'telegram', LiveSettings = 'live_settings', MobileApp = 'mobile_app', + CloudNotifications = 'grafana_cloud_notifications', }