diff --git a/grafana-plugin/jest.config.js b/grafana-plugin/jest.config.js index f70e7d25..ed696b01 100644 --- a/grafana-plugin/jest.config.js +++ b/grafana-plugin/jest.config.js @@ -20,4 +20,6 @@ module.exports = { }, setupFilesAfterEnv: ['/jest.setup.ts'], + + testTimeout: 10000, }; diff --git a/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.module.scss b/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.module.scss index a97a1e43..cd4af8f4 100644 --- a/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.module.scss +++ b/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.module.scss @@ -33,3 +33,28 @@ filter: blur(6px); opacity: 0.6; } + +.blurry { + filter: blur(4px); + opacity: 0.2; +} + +.qr-loader { + position: absolute; + z-index: 10; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + + &__text { + text-align: center; + margin-bottom: 12px; + display: block; + } + + i { // Overwrite Grafana's loading icon + font-size: 32px; + } + +} diff --git a/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.test.tsx b/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.test.tsx index c14bb3c4..00a2d6aa 100644 --- a/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.test.tsx +++ b/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.test.tsx @@ -200,9 +200,12 @@ describe('MobileAppVerification', () => { render(); - await waitFor(() => { - expect(loadUserMock).toHaveBeenCalledTimes(1); - }); + await waitFor( + () => { + expect(loadUserMock).toHaveBeenCalled(); + }, + { timeout: 6000 } + ); }); test('it polls loadUser after disconnect', async () => { @@ -222,8 +225,11 @@ describe('MobileAppVerification', () => { await userEvent.click(button); // click the disconnect button, which opens the modal await userEvent.click(screen.getByText('Remove')); // click the confirm button within the modal, which actually triggers the callback - await waitFor(() => { - expect(loadUserMock).toHaveBeenCalledTimes(1); - }); + await waitFor( + () => { + expect(loadUserMock).toHaveBeenCalled(); + }, + { timeout: 6000 } + ); }); }); diff --git a/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.tsx b/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.tsx index 7b8d2afb..ed634a9f 100644 --- a/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.tsx +++ b/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Icon, LoadingPlaceholder, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; @@ -13,7 +13,7 @@ import { useStore } from 'state/useStore'; import styles from './MobileAppVerification.module.scss'; import DisconnectButton from './parts/DisconnectButton/DisconnectButton'; import DownloadIcons from './parts/DownloadIcons'; -import QRCode from './parts/QRCode'; +import QRCode from './parts/QRCode/QRCode'; const cx = cn.bind(styles); @@ -21,12 +21,15 @@ type Props = { userPk: User['pk']; }; -const INTERVAL_MS = 5000; +const INTERVAL_MIN_THROTTLING = 500; +const INTERVAL_QUEUE_QR = 50000; +const INTERVAL_POLLING = 5000; const BACKEND = 'MOBILE_APP'; const MobileAppVerification = observer(({ userPk }: Props) => { const { userStore } = useStore(); + const isMounted = useRef(false); const [mobileAppIsCurrentlyConnected, setMobileAppIsCurrentlyConnected] = useState(isUserConnected()); const [fetchingQRCode, setFetchingQRCode] = useState(!mobileAppIsCurrentlyConnected); @@ -36,18 +39,29 @@ const MobileAppVerification = observer(({ userPk }: Props) => { const [disconnectingMobileApp, setDisconnectingMobileApp] = useState(false); const [errorDisconnectingMobileApp, setErrorDisconnectingMobileApp] = useState(null); const [userTimeoutId, setUserTimeoutId] = useState(undefined); + const [refreshTimeoutId, setRefreshTimeoutId] = useState(undefined); + const [isQRBlurry, setIsQRBlurry] = useState(false); - const fetchQRCode = useCallback(async () => { - setFetchingQRCode(true); - try { - // backend verification code that we receive is a JSON object that has been "stringified" - const qrCodeContent = await userStore.sendBackendConfirmationCode(userPk, BACKEND); - setQRCodeValue(qrCodeContent); - } catch (e) { - setErrorFetchingQRCode('There was an error fetching your QR code. Please try again.'); - } - setFetchingQRCode(false); - }, [userPk]); + const fetchQRCode = useCallback( + async (showLoader = true) => { + if (showLoader) { + setFetchingQRCode(true); + } + + try { + // backend verification code that we receive is a JSON object that has been "stringified" + const qrCodeContent = await userStore.sendBackendConfirmationCode(userPk, BACKEND); + setQRCodeValue(qrCodeContent); + } catch (e) { + setErrorFetchingQRCode('There was an error fetching your QR code. Please try again.'); + } + + if (showLoader) { + setFetchingQRCode(false); + } + }, + [userPk] + ); const resetState = useCallback(() => { setErrorDisconnectingMobileApp(null); @@ -66,19 +80,21 @@ const MobileAppVerification = observer(({ userPk }: Props) => { } setDisconnectingMobileApp(false); - pollUserProfile(); + clearTimeouts(); + triggerTimeouts(); }, [userPk, resetState]); useEffect(() => { + isMounted.current = true; + if (!isUserConnected()) { - pollUserProfile(); + triggerTimeouts(); } // clear on unmount return () => { - if (userTimeoutId) { - clearTimeout(userTimeoutId); - } + isMounted.current = false; + clearTimeouts(); }; }, []); @@ -116,13 +132,10 @@ const MobileAppVerification = observer(({ userPk }: Props) => { Sign In Open Grafana IRM mobile application and scan this code to sync it with your account. -
- +
+ + {isQRBlurry && }
- - Note: the QR code is only valid for one minute. If you have issues connecting your mobile - app, try refreshing this page to generate a new code. - ); } @@ -138,21 +151,84 @@ const MobileAppVerification = observer(({ userPk }: Props) => {
); + function clearTimeouts(): void { + clearTimeout(userTimeoutId); + clearTimeout(refreshTimeoutId); + } + + function triggerTimeouts(): void { + setTimeout(queueRefreshQR, INTERVAL_QUEUE_QR); + setTimeout(pollUserProfile, INTERVAL_POLLING); + } + function isUserConnected(user?: User): boolean { return !!(user || userStore.currentUser).messaging_backends[BACKEND]?.connected; } + async function queueRefreshQR(): Promise { + if (!isMounted.current) { + return; + } + + clearTimeout(refreshTimeoutId); + setRefreshTimeoutId(undefined); + + const user = await userStore.loadUser(userPk); + if (!isUserConnected(user)) { + let didCallThrottleWithNoEffect = false; + let isRequestDone = false; + + const throttle = () => { + if (!isMounted.current) { + return; + } + if (!isRequestDone) { + didCallThrottleWithNoEffect = true; + return; + } + + setIsQRBlurry(false); + setTimeout(queueRefreshQR, INTERVAL_QUEUE_QR); + }; + + setTimeout(throttle, INTERVAL_MIN_THROTTLING); + setIsQRBlurry(true); + + await fetchQRCode(false); + + isRequestDone = true; + if (didCallThrottleWithNoEffect) { + throttle(); + } + } + } + async function pollUserProfile(): Promise { + if (!isMounted.current) { + return; + } + clearTimeout(userTimeoutId); setUserTimeoutId(undefined); const user = await userStore.loadUser(userPk); if (!isUserConnected(user)) { - setUserTimeoutId(setTimeout(() => pollUserProfile(), INTERVAL_MS)); + setUserTimeoutId(setTimeout(pollUserProfile, INTERVAL_POLLING)); } else { setMobileAppIsCurrentlyConnected(true); } } }); +function QRLoading() { + return ( +
+ + Regenerating QR code... + + +
+ ); +} + export default MobileAppVerification; diff --git a/grafana-plugin/src/containers/MobileAppVerification/__snapshots__/MobileAppVerification.test.tsx.snap b/grafana-plugin/src/containers/MobileAppVerification/__snapshots__/MobileAppVerification.test.tsx.snap index 6bcaa911..e471869c 100644 --- a/grafana-plugin/src/containers/MobileAppVerification/__snapshots__/MobileAppVerification.test.tsx.snap +++ b/grafana-plugin/src/containers/MobileAppVerification/__snapshots__/MobileAppVerification.test.tsx.snap @@ -90,22 +90,7 @@ exports[`MobileAppVerification if we disconnect the app, it disconnects and fetc - App connected -
- - - -
+ Sign In
- You can sync one application to your account. To setup new device please disconnect app first. + Open Grafana IRM mobile application and scan this code to sync it with your account.
- - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/grafana-plugin/src/containers/MobileAppVerification/parts/QRCode/QRCode.test.tsx b/grafana-plugin/src/containers/MobileAppVerification/parts/QRCode/QRCode.test.tsx index 8c3e1554..76008c27 100644 --- a/grafana-plugin/src/containers/MobileAppVerification/parts/QRCode/QRCode.test.tsx +++ b/grafana-plugin/src/containers/MobileAppVerification/parts/QRCode/QRCode.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import QRCode from './'; +import QRCode from './QRCode'; describe('QRCode', () => { test('it renders properly', () => { diff --git a/grafana-plugin/src/containers/MobileAppVerification/parts/QRCode/QRCode.tsx b/grafana-plugin/src/containers/MobileAppVerification/parts/QRCode/QRCode.tsx new file mode 100644 index 00000000..b41de79d --- /dev/null +++ b/grafana-plugin/src/containers/MobileAppVerification/parts/QRCode/QRCode.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; + +import QRCodeBase from 'react-qr-code'; + +import Block from 'components/GBlock/Block'; + +type Props = { + value: string; + className?: string; +}; + +const QRCode: FC = (props: Props) => { + const { value, className = '' } = props; + + return ( + + + + ); +}; + +export default QRCode; diff --git a/grafana-plugin/src/containers/MobileAppVerification/parts/QRCode/index.tsx b/grafana-plugin/src/containers/MobileAppVerification/parts/QRCode/index.tsx deleted file mode 100644 index 98ec3561..00000000 --- a/grafana-plugin/src/containers/MobileAppVerification/parts/QRCode/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { FC } from 'react'; - -import QRCodeBase from 'react-qr-code'; - -import Block from 'components/GBlock/Block'; - -type Props = { - value: string; -}; - -const QRCode: FC = ({ value }) => ( - - - -); - -export default QRCode; diff --git a/grafana-plugin/src/containers/UserSettings/UserSettings.tsx b/grafana-plugin/src/containers/UserSettings/UserSettings.tsx index 78fdad24..4c025479 100644 --- a/grafana-plugin/src/containers/UserSettings/UserSettings.tsx +++ b/grafana-plugin/src/containers/UserSettings/UserSettings.tsx @@ -49,7 +49,10 @@ const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserInfo }: U }, []); const isModalWide = - !isDesktopOrLaptop || activeTab === UserSettingsTab.UserInfo || activeTab === UserSettingsTab.PhoneVerification; + !isDesktopOrLaptop || + activeTab === UserSettingsTab.UserInfo || + activeTab === UserSettingsTab.PhoneVerification || + activeTab === UserSettingsTab.MobileAppVerification; const [ showNotificationSettingsTab,