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',
}