From 2d50c3c97248e8af903b7449e76f0ae5851c71b6 Mon Sep 17 00:00:00 2001 From: Vasily Pozdnyakov Date: Fri, 31 Mar 2023 13:42:39 +0200 Subject: [PATCH] Added check for newer releases functionality Signed-off-by: Vasily Pozdnyakov --- src/ElectronBackend/main/menu.ts | 8 ++ .../BackendCommunication.tsx | 14 +++ .../__tests__/BackendCommunication.test.tsx | 2 +- .../Components/GlobalPopup/GlobalPopup.tsx | 3 + .../__tests__/GlobalPopup.test.tsx | 22 ++++ .../TopBar/__tests__/TopBar.test.tsx | 2 +- .../UpdateAppPopup/UpdateAppPopup.tsx | 78 ++++++++++++++ .../__tests__/UpdateAppPopup.test.tsx | 101 ++++++++++++++++++ .../update-app-popup-helpers.ts | 21 ++++ src/Frontend/enums/enums.ts | 1 + src/shared/ipc-channels.ts | 1 + 11 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 src/Frontend/Components/UpdateAppPopup/UpdateAppPopup.tsx create mode 100644 src/Frontend/Components/UpdateAppPopup/__tests__/UpdateAppPopup.test.tsx create mode 100644 src/Frontend/Components/UpdateAppPopup/update-app-popup-helpers.ts diff --git a/src/ElectronBackend/main/menu.ts b/src/ElectronBackend/main/menu.ts index c7890750b..61bd9ae47 100644 --- a/src/ElectronBackend/main/menu.ts +++ b/src/ElectronBackend/main/menu.ts @@ -213,6 +213,14 @@ export function createMenu(mainWindow: BrowserWindow): Menu { await shell.openPath(app.getPath('logs')); }, }, + { + label: 'Check for updates', + click(): void { + webContents.send(AllowedFrontendChannels.ShowUpdateAppPopup, { + showUpdateAppPopup: true, + }); + }, + }, ], }, ]); diff --git a/src/Frontend/Components/BackendCommunication/BackendCommunication.tsx b/src/Frontend/Components/BackendCommunication/BackendCommunication.tsx index 620ebb897..efebd03ad 100644 --- a/src/Frontend/Components/BackendCommunication/BackendCommunication.tsx +++ b/src/Frontend/Components/BackendCommunication/BackendCommunication.tsx @@ -225,6 +225,15 @@ export function BackendCommunication(): ReactElement | null { } } + function showUpdateAppPopupListener( + event: IpcRendererEvent, + showUpdateAppPopup: boolean + ): void { + if (showUpdateAppPopup) { + dispatch(openPopup(PopupType.UpdateAppPopup)); + } + } + function setBaseURLForRootListener( event: IpcRendererEvent, baseURLForRootArgs: BaseURLForRootArgs @@ -313,6 +322,11 @@ export function BackendCommunication(): ReactElement | null { showFileSupportPopupListener, [dispatch] ); + useIpcRenderer( + AllowedFrontendChannels.ShowUpdateAppPopup, + showUpdateAppPopupListener, + [dispatch] + ); return null; } diff --git a/src/Frontend/Components/BackendCommunication/__tests__/BackendCommunication.test.tsx b/src/Frontend/Components/BackendCommunication/__tests__/BackendCommunication.test.tsx index c197f6187..016a6f9a0 100644 --- a/src/Frontend/Components/BackendCommunication/__tests__/BackendCommunication.test.tsx +++ b/src/Frontend/Components/BackendCommunication/__tests__/BackendCommunication.test.tsx @@ -16,7 +16,7 @@ import { Attributions, ExportType } from '../../../../shared/shared-types'; describe('BackendCommunication', () => { it('renders an Open file icon', () => { renderComponentWithStore(); - const expectedNumberOfCalls = 11; + const expectedNumberOfCalls = 12; expect(window.electronAPI.on).toHaveBeenCalledTimes(expectedNumberOfCalls); expect(window.electronAPI.on).toHaveBeenCalledWith( AllowedFrontendChannels.FileLoaded, diff --git a/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx b/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx index 5267c8052..5695d79b8 100644 --- a/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx +++ b/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx @@ -23,6 +23,7 @@ import { ChangedInputFilePopup } from '../ChangedInputFilePopup/ChangedInputFile import { AttributionWizardPopup } from '../AttributionWizardPopup/AttributionWizardPopup'; import { FileSupportPopup } from '../FileSupportPopup/FileSupportPopup'; import { FileSupportDotOpossumAlreadyExistsPopup } from '../FileSupportDotOpossumAlreadyExistsPopup/FileSupportDotOpossumAlreadyExistsPopup'; +import { UpdateAppPopup } from '../UpdateAppPopup/UpdateAppPopup'; function getPopupComponent(popupType: PopupType | null): ReactElement | null { switch (popupType) { @@ -58,6 +59,8 @@ function getPopupComponent(popupType: PopupType | null): ReactElement | null { return ; case PopupType.FileSupportDotOpossumAlreadyExistsPopup: return ; + case PopupType.UpdateAppPopup: + return ; default: return null; } diff --git a/src/Frontend/Components/GlobalPopup/__tests__/GlobalPopup.test.tsx b/src/Frontend/Components/GlobalPopup/__tests__/GlobalPopup.test.tsx index 98ae5c47c..f2543d95d 100644 --- a/src/Frontend/Components/GlobalPopup/__tests__/GlobalPopup.test.tsx +++ b/src/Frontend/Components/GlobalPopup/__tests__/GlobalPopup.test.tsx @@ -26,6 +26,7 @@ import { } from '../../../state/actions/resource-actions/all-views-simple-actions'; import { setSelectedResourceId } from '../../../state/actions/resource-actions/audit-view-simple-actions'; import { openAttributionWizardPopup } from '../../../state/actions/popup-actions/popup-actions'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; describe('The GlobalPopUp', () => { it('does not open by default', () => { @@ -210,4 +211,25 @@ describe('The GlobalPopUp', () => { const header = 'Warning: Outdated input file format'; expect(screen.getByText(header)).toBeInTheDocument(); }); + + it('opens the UpdateAppPopup', () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + const { store } = renderComponentWithStore( + + + + ); + act(() => { + store.dispatch(openPopup(PopupType.UpdateAppPopup)); + }); + + const header = 'Check for updates'; + expect(screen.getByText(header)).toBeInTheDocument(); + }); }); diff --git a/src/Frontend/Components/TopBar/__tests__/TopBar.test.tsx b/src/Frontend/Components/TopBar/__tests__/TopBar.test.tsx index 1450c7797..0a87be2c0 100644 --- a/src/Frontend/Components/TopBar/__tests__/TopBar.test.tsx +++ b/src/Frontend/Components/TopBar/__tests__/TopBar.test.tsx @@ -20,7 +20,7 @@ import { setResources } from '../../../state/actions/resource-actions/all-views- describe('TopBar', () => { it('renders an Open file icon', () => { const { store } = renderComponentWithStore(); - const totalNumberOfCalls = 11; + const totalNumberOfCalls = 12; expect(window.electronAPI.on).toHaveBeenCalledTimes(totalNumberOfCalls); expect(window.electronAPI.on).toHaveBeenCalledWith( AllowedFrontendChannels.FileLoaded, diff --git a/src/Frontend/Components/UpdateAppPopup/UpdateAppPopup.tsx b/src/Frontend/Components/UpdateAppPopup/UpdateAppPopup.tsx new file mode 100644 index 000000000..1eaadbdb9 --- /dev/null +++ b/src/Frontend/Components/UpdateAppPopup/UpdateAppPopup.tsx @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +import React, { ReactElement } from 'react'; +import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; +import { useAppDispatch } from '../../state/hooks'; +import { closePopup } from '../../state/actions/view-actions/view-actions'; +import { ButtonText } from '../../enums/enums'; +import commitInfo from '../../../commitInfo.json'; +import MuiLink from '@mui/material/Link'; +import { openUrl } from '../../util/open-url'; +import MuiTypography from '@mui/material/Typography'; +import { searchLatestReleaseNameAndUrl } from './update-app-popup-helpers'; +import { useQuery } from '@tanstack/react-query'; +import { Alert } from '../Alert/Alert'; +import { Spinner } from '../Spinner/Spinner'; + +export function UpdateAppPopup(): ReactElement { + const dispatch = useAppDispatch(); + + function close(): void { + dispatch(closePopup()); + } + + const { isLoading, data, isError, error } = useQuery( + ['latestReleaseNameSearch'], + () => searchLatestReleaseNameAndUrl(), + { + refetchOnWindowFocus: false, + } + ); + + const content = !isError ? ( + isLoading ? ( + + ) : data ? ( + data.name === commitInfo.commitInfo ? ( + 'You have the latest version of the app.' + ) : ( + <> + + There is a new release! You can download it using the following + link: +
+ openUrl(data.url)}> + {data.name} + +
+ + ) + ) : ( + 'No information found.' + ) + ) : ( + + ); + + return ( + + ); +} diff --git a/src/Frontend/Components/UpdateAppPopup/__tests__/UpdateAppPopup.test.tsx b/src/Frontend/Components/UpdateAppPopup/__tests__/UpdateAppPopup.test.tsx new file mode 100644 index 000000000..858c55e19 --- /dev/null +++ b/src/Frontend/Components/UpdateAppPopup/__tests__/UpdateAppPopup.test.tsx @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { renderComponentWithStore } from '../../../test-helpers/render-component-with-store'; +import { UpdateAppPopup } from '../UpdateAppPopup'; +import { screen } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import MockAdapter from 'axios-mock-adapter'; +import axios from 'axios'; +import commitInfo from '../../../../commitInfo.json'; + +describe('UpdateAppPopup', () => { + const okStatus = 200; + const notFoundStatus = 404; + const axiosMock = new MockAdapter(axios); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + it('shows the popup with a link to a new release', async () => { + axiosMock + .onGet( + 'https://api.github.com/repos/opossum-tool/OpossumUI/releases/latest' + ) + .replyOnce(okStatus, { + name: 'Latest release', + html_url: 'some url', + }); + renderComponentWithStore( + + + + ); + expect(screen.getByText('Check for updates')); + expect( + await screen.findByText( + 'There is a new release! You can download it using the following link:' + ) + ); + expect(await screen.findByText('Latest release')); + }); + + it('shows the popup with no newer release', async () => { + axiosMock + .onGet( + 'https://api.github.com/repos/opossum-tool/OpossumUI/releases/latest' + ) + .replyOnce(okStatus, { + name: commitInfo.commitInfo, + html_url: 'some url', + }); + renderComponentWithStore( + + + + ); + expect(screen.getByText('Check for updates')); + expect(await screen.findByText('You have the latest version of the app.')); + }); + + it('shows the popup with no info found', async () => { + axiosMock + .onGet( + 'https://api.github.com/repos/opossum-tool/OpossumUI/releases/latest' + ) + .replyOnce(okStatus, null); + renderComponentWithStore( + + + + ); + expect(screen.getByText('Check for updates')); + expect(await screen.findByText('No information found.')); + }); + + it('shows the popup with error', async () => { + axiosMock + .onGet( + 'https://api.github.com/repos/opossum-tool/OpossumUI/releases/latest' + ) + .replyOnce(notFoundStatus); + renderComponentWithStore( + + + + ); + expect(screen.getByText('Check for updates')); + expect( + await screen.findByText( + 'Failed while fetching release data: Request failed with status code 404' + ) + ); + }); +}); diff --git a/src/Frontend/Components/UpdateAppPopup/update-app-popup-helpers.ts b/src/Frontend/Components/UpdateAppPopup/update-app-popup-helpers.ts new file mode 100644 index 000000000..0fb851d5c --- /dev/null +++ b/src/Frontend/Components/UpdateAppPopup/update-app-popup-helpers.ts @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +import axios from 'axios'; + +export async function searchLatestReleaseNameAndUrl(): Promise<{ + name: string; + url: string; +} | null> { + const response = await axios.get( + 'https://api.github.com/repos/opossum-tool/OpossumUI/releases/latest' + ); + if (!response.data) { + return null; + } + const name = response.data.name as string; + const url = response.data.html_url as string; + return { name, url }; +} diff --git a/src/Frontend/enums/enums.ts b/src/Frontend/enums/enums.ts index a1efebcd9..200c4342b 100644 --- a/src/Frontend/enums/enums.ts +++ b/src/Frontend/enums/enums.ts @@ -27,6 +27,7 @@ export enum PopupType { AttributionWizardPopup = 'AttributionWizardPopup', FileSupportPopup = 'FileSupportPopup', FileSupportDotOpossumAlreadyExistsPopup = 'FileSupportDotOpossumAlreadyExistsPopup', + UpdateAppPopup = 'UpdateAppPopup', } export enum SavePackageInfoOperation { diff --git a/src/shared/ipc-channels.ts b/src/shared/ipc-channels.ts index 3c0e79c84..655b40e5f 100644 --- a/src/shared/ipc-channels.ts +++ b/src/shared/ipc-channels.ts @@ -31,4 +31,5 @@ export enum AllowedFrontendChannels { ShowSearchPopup = 'show-search-pop-up', ShowProjectMetadataPopup = 'show-project-metadata-pop-up', ShowProjectStatisticsPopup = 'show-project-statistics-pop-up', + ShowUpdateAppPopup = 'show-update-app-pop-up', }