Merge pull request #956 from grafana/rares/mobileapp-tweaks

Mobile app screen tweaks
This commit is contained in:
Rares Mardare 2022-12-07 13:51:46 +02:00 committed by GitHub
commit 01d9333bd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 2384 additions and 82 deletions

View file

@ -20,4 +20,6 @@ module.exports = {
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testTimeout: 10000,
};

View file

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

View file

@ -200,9 +200,12 @@ describe('MobileAppVerification', () => {
render(<MobileAppVerification userPk={USER_PK} />);
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 }
);
});
});

View file

@ -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<boolean>(isUserConnected());
const [fetchingQRCode, setFetchingQRCode] = useState<boolean>(!mobileAppIsCurrentlyConnected);
@ -36,18 +39,29 @@ const MobileAppVerification = observer(({ userPk }: Props) => {
const [disconnectingMobileApp, setDisconnectingMobileApp] = useState<boolean>(false);
const [errorDisconnectingMobileApp, setErrorDisconnectingMobileApp] = useState<string>(null);
const [userTimeoutId, setUserTimeoutId] = useState<NodeJS.Timeout>(undefined);
const [refreshTimeoutId, setRefreshTimeoutId] = useState<NodeJS.Timeout>(undefined);
const [isQRBlurry, setIsQRBlurry] = useState<boolean>(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
</Text>
<Text type="primary">Open Grafana IRM mobile application and scan this code to sync it with your account.</Text>
<div className="u-width-100 u-flex u-flex-center">
<QRCode value={QRCodeValue} />
<div className={cx('u-width-100', 'u-flex', 'u-flex-center', 'u-position-relative')}>
<QRCode className={cx({ blurry: isQRBlurry })} value={QRCodeValue} />
{isQRBlurry && <QRLoading />}
</div>
<Text type="primary" className="u-break-word">
<strong>Note:</strong> 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.
</Text>
</VerticalGroup>
);
}
@ -138,21 +151,84 @@ const MobileAppVerification = observer(({ userPk }: Props) => {
</div>
);
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<void> {
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<void> {
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 (
<div className={cx('qr-loader')}>
<Text type="primary" className={cx('qr-loader__text')}>
Regenerating QR code...
</Text>
<LoadingPlaceholder />
</div>
);
}
export default MobileAppVerification;

View file

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

View file

@ -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: Props) => {
const { value, className = '' } = props;
return (
<Block bordered className={className}>
<QRCodeBase value={value} />
</Block>
);
};
export default QRCode;

View file

@ -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<Props> = ({ value }) => (
<Block bordered>
<QRCodeBase value={value} />
</Block>
);
export default QRCode;

View file

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