Repository: opossum-tool/OpossumUI
Pull request #2371 (2023-12-04 10:38:12)

feat: create package autocompletes
### Summary of changes

- enforce import of custom faker via linter
- create generic Autocomplete component
- create specific PackageAutocomplete component
- replace MuiAutocomplete in LocatorPopup with new generic Autocomplete
- properly hide icon button instead of only applying hidden visibility (which means the button is still interactive by mouse and keyboard)
- add util for pluralizing nouns

![image](https://github.com/opossum-tool/OpossumUI/assets/46091730/442e87fb-e378-437e-90b3-6d7194012457)

![image](https://github.com/opossum-tool/OpossumUI/assets/46091730/d142320f-91d2-4931-92c2-7196d693d82f)

![image](https://github.com/opossum-tool/OpossumUI/assets/46091730/b00f6a10-3d9c-4bdc-8383-93682f27ba74)

### Context and reason for change

#2343 

### How can the changes be tested

Test all input fields that involve autocompletes:
- package name
- package type (also contains defaults)
- package version
- package namespace
- license (also contains defaults)
- locator popup

mstykow: > Feature is cool
> 
> One small bug:
> 
> 1. fill in the license name
> 2. unselect the license name field
> 3. click on the license name field
>    I expect the suggestions to be filtered, but they are not.

this is intentional. i want the user to be able to still see all options in case they want to change their opinion. if i were to only show the current selection when the dropdown opens, i'd then have to delete my input to be able to see more choices again.
MarkusObendrauf: > > Feature is cool
> > One small bug:
> > 
> > 1. fill in the license name
> > 2. unselect the license name field
> > 3. click on the license name field
> >    I expect the suggestions to be filtered, but they are not.
> 
> this is intentional. i want the user to be able to still see all options in case they want to change their opinion. if i were to only show the current selection when the dropdown opens, i'd then have to delete my input to be able to see more choices again.

Alright, makes sense

Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 560fb0b7..d18ac5a0 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -49,6 +49,13 @@ const sharedRules = {
   'no-multi-assign': 'error',
   'no-nested-ternary': 'off',
   'no-new-func': 'error',
+  'no-restricted-imports': [
+    'error',
+    {
+      name: '@faker-js/faker',
+      message: 'Please use shared faker instead.',
+    },
+  ],
   'no-return-assign': 'error',
   'no-return-await': 'error',
   'no-self-compare': 'error',
@@ -120,6 +127,7 @@ module.exports = {
     'jest.config.js',
     'notices.template.html',
     'vite.config.mts',
+    'typings',
   ],
   reportUnusedDisableDirectives: true,
   rules: {
diff --git a/example-files/opossum_input.json b/example-files/opossum_input.json
index 7779d31b..bfcef1ce 100644
--- a/example-files/opossum_input.json
+++ b/example-files/opossum_input.json
@@ -211,6 +211,7 @@
       "attributionConfidence": 100,
       "comment": "TypeScript. \nhttps://www.npmjs.com/package/typescript",
       "packageName": "TypeScript",
+      "packageType": "github",
       "preSelected": true,
       "criticality": "medium",
       "packageVersion": "3.9.6",
diff --git a/index.html b/index.html
index 5daf7770..5440a4e1 100644
--- a/index.html
+++ b/index.html
@@ -9,7 +9,10 @@ SPDX-License-Identifier: Apache-2.0
 <html lang="en">
   <head>
     <meta charset="utf-8" />
-    <meta http-equiv="Content-Security-Policy" content="script-src 'self';" />
+    <meta
+      http-equiv="Content-Security-Policy"
+      content="script-src 'self'; worker-src 'self' blob:;"
+    />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="theme-color" content="#000000" />
     <meta name="description" content="OpossumUI" />
diff --git a/jest.config.js b/jest.config.js
index 96f68c99..c7df668c 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -7,7 +7,7 @@ module.exports = {
   testEnvironment: 'jsdom',
   resetMocks: false,
   transformIgnorePatterns: ['/node_modules/(?!d3-*|internmap|axios)'],
-  setupFilesAfterEnv: ['./setupTests.ts'],
+  setupFilesAfterEnv: ['./src/shared/setupTests.ts'],
   watchAll: false,
   roots: ['<rootDir>/src/Frontend', '<rootDir>/src/ElectronBackend'],
   modulePathIgnorePatterns: ['<rootDir>/build/'],
diff --git a/src/Frontend/Components/AggregatedAttributionsPanel/AccordionPanel.tsx b/src/Frontend/Components/AggregatedAttributionsPanel/AccordionPanel.tsx
index be6f5de6..9a8367f5 100644
--- a/src/Frontend/Components/AggregatedAttributionsPanel/AccordionPanel.tsx
+++ b/src/Frontend/Components/AggregatedAttributionsPanel/AccordionPanel.tsx
@@ -9,6 +9,7 @@ import MuiTypography from '@mui/material/Typography';
 import { isEmpty } from 'lodash';
 import { ReactElement, useMemo, useState } from 'react';
 
+import { PackagePanelTitle } from '../../enums/enums';
 import { PanelData } from '../../types/types';
 import { PackagePanel } from '../PackagePanel/PackagePanel';
 
@@ -37,6 +38,7 @@ const classes = {
 
 interface AccordionPanelProps {
   panelData: PanelData;
+  title: PackagePanelTitle;
   isAddToPackageEnabled: boolean;
   ['aria-label']?: string;
 }
@@ -63,7 +65,7 @@ export function AccordionPanel(props: AccordionPanelProps): ReactElement {
       }}
       elevation={0}
       square={true}
-      key={`PackagePanel-${props.panelData.title}`}
+      key={`PackagePanel-${props.title}`}
       expanded={expanded}
       onChange={handleExpansionChange}
       disabled={isEmpty(props.panelData.displayPackageInfosWithCount)}
@@ -75,7 +77,7 @@ export function AccordionPanel(props: AccordionPanelProps): ReactElement {
         }}
         expandIcon={<ExpandMoreIcon />}
       >
-        <MuiTypography>{props.panelData.title}</MuiTypography>
+        <MuiTypography>{props.title}</MuiTypography>
       </MuiAccordionSummary>
       <MuiAccordionDetails
         sx={{
@@ -83,7 +85,7 @@ export function AccordionPanel(props: AccordionPanelProps): ReactElement {
         }}
       >
         <PackagePanel
-          title={props.panelData.title}
+          title={props.title}
           displayPackageInfosWithCount={
             props.panelData.displayPackageInfosWithCount
           }
diff --git a/src/Frontend/Components/AggregatedAttributionsPanel/AggregatedAttributionsPanel.tsx b/src/Frontend/Components/AggregatedAttributionsPanel/AggregatedAttributionsPanel.tsx
index 8e467b4a..33ba01c2 100644
--- a/src/Frontend/Components/AggregatedAttributionsPanel/AggregatedAttributionsPanel.tsx
+++ b/src/Frontend/Components/AggregatedAttributionsPanel/AggregatedAttributionsPanel.tsx
@@ -2,31 +2,21 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { memo, ReactElement, useMemo } from 'react';
+import { memo, ReactElement } from 'react';
 
 import { PackagePanelTitle } from '../../enums/enums';
 import { useAppSelector } from '../../state/hooks';
 import {
   getExternalAttributionsToHashes,
   getExternalData,
-  getManualData,
 } from '../../state/selectors/all-views-resource-selectors';
-import {
-  getResolvedExternalAttributions,
-  getSelectedResourceId,
-} from '../../state/selectors/audit-view-resource-selectors';
+import { getSelectedResourceId } from '../../state/selectors/audit-view-resource-selectors';
 import { AttributionIdWithCount } from '../../types/types';
 import { isIdOfResourceWithChildren } from '../../util/can-resource-have-children';
-import {
-  getExternalAttributionIdsWithCount,
-  PanelAttributionData,
-} from '../../util/get-contained-packages';
-import {
-  getContainedExternalDisplayPackageInfosWithCount,
-  getContainedManualDisplayPackageInfosWithCount,
-} from './accordion-panel-helpers';
+import { getExternalAttributionIdsWithCount } from '../../util/get-contained-packages';
+import { usePanelData } from '../../web-workers/use-signals-worker';
+import { AccordionPanel } from './AccordionPanel';
 import { SyncAccordionPanel } from './SyncAccordionPanel';
-import { WorkerAccordionPanel } from './WorkerAccordionPanel';
 
 interface AggregatedAttributionsPanelProps {
   isAddToPackageEnabled: boolean;
@@ -34,74 +24,14 @@ interface AggregatedAttributionsPanelProps {
 
 export const AggregatedAttributionsPanel = memo(
   (props: AggregatedAttributionsPanelProps): ReactElement => {
-    const manualData = useAppSelector(getManualData);
     const externalData = useAppSelector(getExternalData);
     const attributionsToHashes = useAppSelector(
       getExternalAttributionsToHashes,
     );
-
     const selectedResourceId = useAppSelector(getSelectedResourceId);
-    const resolvedExternalAttributions: Set<string> = useAppSelector(
-      getResolvedExternalAttributions,
-    );
-
-    const containedExternalPackagesWorkerArgs = useMemo(
-      () => ({
-        selectedResourceId,
-        resolvedExternalAttributions,
-        panelTitle: PackagePanelTitle.ContainedExternalPackages,
-      }),
-      [selectedResourceId, resolvedExternalAttributions],
-    );
-    const containedExternalPackagesSyncFallbackArgs = useMemo(
-      () => ({
-        selectedResourceId,
-        externalData,
-        resolvedExternalAttributions,
-        attributionsToHashes,
-        panelTitle: PackagePanelTitle.ContainedExternalPackages,
-      }),
-      [
-        selectedResourceId,
-        externalData,
-        resolvedExternalAttributions,
-        attributionsToHashes,
-      ],
-    );
-
-    const containedManualPackagesWorkerArgs = useMemo(
-      () => ({
-        selectedResourceId,
-        panelTitle: PackagePanelTitle.ContainedManualPackages,
-      }),
 
-      //  manualData is excluded from dependencies on purpose to avoid recalculation
-      //  when it changes. Usually this is not an issue as the displayed data
-      //  remains correct. Therefore, the panelData is eventually consistent.
-      //  We still need manualData.resourcesToAttributions in the dependencies to
-      //  update panelData, when replaceAttributionPopup was called. This is
-      //  relevant for manual attributions in the attributions in folder content panel.
-
-      // eslint-disable-next-line react-hooks/exhaustive-deps
-      [selectedResourceId, manualData.resourcesToAttributions],
-    );
-
-    const manualPanelData: PanelAttributionData = {
-      attributions: manualData.attributions,
-      resourcesToAttributions: manualData.resourcesToAttributions,
-      resourcesWithAttributedChildren:
-        manualData.resourcesWithAttributedChildren,
-    };
-    const containedManualPackagesSyncFallbackArgs = useMemo(
-      () => ({
-        selectedResourceId,
-        manualData: manualPanelData,
-        panelTitle: PackagePanelTitle.ContainedManualPackages,
-      }),
-
-      // eslint-disable-next-line react-hooks/exhaustive-deps
-      [selectedResourceId, manualData],
-    );
+    const { signalsInFolderContent, attributionsInFolderContent } =
+      usePanelData();
 
     return (
       <>
@@ -119,23 +49,15 @@ export const AggregatedAttributionsPanel = memo(
         />
         {isIdOfResourceWithChildren(selectedResourceId) ? (
           <>
-            <WorkerAccordionPanel
+            <AccordionPanel
               title={PackagePanelTitle.ContainedExternalPackages}
-              workerArgs={containedExternalPackagesWorkerArgs}
-              syncFallbackArgs={containedExternalPackagesSyncFallbackArgs}
-              getDisplayPackageInfosWithCount={
-                getContainedExternalDisplayPackageInfosWithCount
-              }
+              panelData={signalsInFolderContent}
               isAddToPackageEnabled={props.isAddToPackageEnabled}
               aria-label={'signals in folder content panel'}
             />
-            <WorkerAccordionPanel
+            <AccordionPanel
               title={PackagePanelTitle.ContainedManualPackages}
-              workerArgs={containedManualPackagesWorkerArgs}
-              syncFallbackArgs={containedManualPackagesSyncFallbackArgs}
-              getDisplayPackageInfosWithCount={
-                getContainedManualDisplayPackageInfosWithCount
-              }
+              panelData={attributionsInFolderContent}
               isAddToPackageEnabled={props.isAddToPackageEnabled}
               aria-label={'attributions in folder content panel'}
             />
diff --git a/src/Frontend/Components/AggregatedAttributionsPanel/SyncAccordionPanel.tsx b/src/Frontend/Components/AggregatedAttributionsPanel/SyncAccordionPanel.tsx
index e784775e..f9a273b1 100644
--- a/src/Frontend/Components/AggregatedAttributionsPanel/SyncAccordionPanel.tsx
+++ b/src/Frontend/Components/AggregatedAttributionsPanel/SyncAccordionPanel.tsx
@@ -8,7 +8,7 @@ import {
   AttributionsToHashes,
 } from '../../../shared/shared-types';
 import { PackagePanelTitle } from '../../enums/enums';
-import { AttributionIdWithCount, PanelData } from '../../types/types';
+import { AttributionIdWithCount } from '../../types/types';
 import { getExternalDisplayPackageInfosWithCount } from './accordion-panel-helpers';
 import { AccordionPanel } from './AccordionPanel';
 
@@ -31,15 +31,14 @@ export function SyncAccordionPanel(
       props.attributionsToHashes,
       props.title,
     );
-  const panelData: PanelData = {
-    title: props.title,
-    sortedPackageCardIds,
-    displayPackageInfosWithCount,
-  };
 
   return (
     <AccordionPanel
-      panelData={panelData}
+      panelData={{
+        sortedPackageCardIds,
+        displayPackageInfosWithCount,
+      }}
+      title={props.title}
       isAddToPackageEnabled={props.isAddToPackageEnabled}
       aria-label={props['aria-label']}
     />
diff --git a/src/Frontend/Components/AggregatedAttributionsPanel/WorkerAccordionPanel.tsx b/src/Frontend/Components/AggregatedAttributionsPanel/WorkerAccordionPanel.tsx
deleted file mode 100644
index 02809dae..00000000
--- a/src/Frontend/Components/AggregatedAttributionsPanel/WorkerAccordionPanel.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { ReactElement, useContext, useEffect, useState } from 'react';
-
-import { AttributionsToHashes } from '../../../shared/shared-types';
-import { PackagePanelTitle } from '../../enums/enums';
-import {
-  DisplayPackageInfosWithCount,
-  DisplayPackageInfosWithCountAndResourceId,
-  PanelData,
-} from '../../types/types';
-import { PanelAttributionData } from '../../util/get-contained-packages';
-import { AccordionWorkersContext } from '../WorkersContextProvider/WorkersContextProvider';
-import { AccordionPanel } from './AccordionPanel';
-
-const EMPTY_DISPLAY_PACKAGE_INFOS_WITH_COUNT_AND_RESOURCE_ID: DisplayPackageInfosWithCountAndResourceId =
-  {
-    resourceId: '',
-    sortedPackageCardIds: [],
-    displayPackageInfosWithCount: {},
-  };
-
-type ContainedAttributionsAccordionWorkerArgs =
-  | ContainedExternalAttributionsAccordionWorkerArgs
-  | ContainedManualAttributionsAccordionWorkerArgs;
-
-interface ContainedExternalAttributionsAccordionWorkerArgs {
-  selectedResourceId: string;
-  externalData?: PanelAttributionData;
-  attributionsToHashes?: AttributionsToHashes;
-  resolvedExternalAttributions: Set<string>;
-  panelTitle: PackagePanelTitle;
-}
-
-interface ContainedManualAttributionsAccordionWorkerArgs {
-  selectedResourceId: string;
-  manualData?: PanelAttributionData;
-  panelTitle: PackagePanelTitle;
-}
-
-interface WorkerAccordionPanelProps {
-  title:
-    | PackagePanelTitle.ContainedExternalPackages
-    | PackagePanelTitle.ContainedManualPackages;
-  workerArgs: ContainedAttributionsAccordionWorkerArgs;
-  syncFallbackArgs: ContainedAttributionsAccordionWorkerArgs;
-  getDisplayPackageInfosWithCount(
-    workerArgs: ContainedAttributionsAccordionWorkerArgs,
-  ): [Array<string>, DisplayPackageInfosWithCount];
-  isAddToPackageEnabled: boolean;
-  ['aria-label']?: string;
-}
-
-export function WorkerAccordionPanel(
-  props: WorkerAccordionPanelProps,
-): ReactElement {
-  const [
-    displayPackageInfosWithCountAndResourceId,
-    setDisplayPackageInfosWithCountAndResourceId,
-  ] = useState<DisplayPackageInfosWithCountAndResourceId>(
-    EMPTY_DISPLAY_PACKAGE_INFOS_WITH_COUNT_AND_RESOURCE_ID,
-  );
-  const resourceDetailsTabsWorkers = useContext(AccordionWorkersContext);
-
-  let worker: Worker;
-  switch (props.title) {
-    case PackagePanelTitle.ContainedExternalPackages:
-      worker =
-        resourceDetailsTabsWorkers.containedExternalAttributionsAccordionWorker;
-      break;
-    case PackagePanelTitle.ContainedManualPackages:
-      worker =
-        resourceDetailsTabsWorkers.containedManualAttributionsAccordionWorker;
-      break;
-  }
-
-  useEffect(() => {
-    void loadDisplayPackageInfosWithCount(
-      props.workerArgs,
-      worker,
-      props.title,
-      setDisplayPackageInfosWithCountAndResourceId,
-      props.getDisplayPackageInfosWithCount,
-      props.syncFallbackArgs,
-    );
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [props.syncFallbackArgs, worker, props.workerArgs]);
-
-  let panelData: PanelData;
-  if (
-    props.workerArgs.selectedResourceId ===
-    displayPackageInfosWithCountAndResourceId.resourceId
-  ) {
-    panelData = {
-      title: props.title,
-      sortedPackageCardIds:
-        displayPackageInfosWithCountAndResourceId.sortedPackageCardIds,
-      displayPackageInfosWithCount:
-        displayPackageInfosWithCountAndResourceId.displayPackageInfosWithCount,
-    };
-  } else {
-    panelData = {
-      title: props.title,
-      sortedPackageCardIds: [],
-      displayPackageInfosWithCount: {},
-    };
-  }
-
-  return (
-    <AccordionPanel
-      panelData={panelData}
-      isAddToPackageEnabled={props.isAddToPackageEnabled}
-      aria-label={props['aria-label']}
-    />
-  );
-}
-
-// eslint-disable-next-line @typescript-eslint/require-await
-async function loadDisplayPackageInfosWithCount(
-  workerArgs: ContainedAttributionsAccordionWorkerArgs,
-  worker: Worker,
-  panelTitle: string,
-  setDisplayPackageInfosWithCountAndResourceId: (
-    displayPackageInfosWithCountAndResourceId: DisplayPackageInfosWithCountAndResourceId,
-  ) => void,
-  getDisplayPackageInfosWithCount: (
-    workerArgs: ContainedAttributionsAccordionWorkerArgs,
-  ) => [Array<string>, DisplayPackageInfosWithCount],
-  syncFallbackArgs: ContainedAttributionsAccordionWorkerArgs,
-): Promise<void> {
-  setDisplayPackageInfosWithCountAndResourceId(
-    EMPTY_DISPLAY_PACKAGE_INFOS_WITH_COUNT_AND_RESOURCE_ID,
-  );
-
-  // WebWorkers can fail for different reasons, e.g. because they run out
-  // of memory with huge input files or because Jest does not support
-  // them. When they fail the accordion is calculated on main. The error
-  // message is logged in the console.
-  try {
-    worker.postMessage(workerArgs);
-
-    worker.onmessage = ({ data: { output } }): void => {
-      if (!output) {
-        logErrorAndComputeInMainProcess(
-          panelTitle,
-          Error('Web Worker execution error.'),
-          setDisplayPackageInfosWithCountAndResourceId,
-          getDisplayPackageInfosWithCount,
-          syncFallbackArgs,
-        );
-      } else {
-        setDisplayPackageInfosWithCountAndResourceId(output);
-      }
-    };
-  } catch (error) {
-    logErrorAndComputeInMainProcess(
-      panelTitle,
-      error,
-      setDisplayPackageInfosWithCountAndResourceId,
-      getDisplayPackageInfosWithCount,
-      syncFallbackArgs,
-    );
-  }
-}
-
-function logErrorAndComputeInMainProcess(
-  panelTitle: string,
-  error: unknown,
-  setDisplayPackageInfosWithCountAndResourceId: (
-    displayPackageInfosWithCountAndResourceId: DisplayPackageInfosWithCountAndResourceId,
-  ) => void,
-  getDisplayPackageInfosWithCount: (
-    syncFallbackArgs: ContainedAttributionsAccordionWorkerArgs,
-  ) => [Array<string>, DisplayPackageInfosWithCount],
-  syncFallbackArgs: ContainedAttributionsAccordionWorkerArgs,
-): void {
-  console.info(`Error in ResourceDetailsTab ${panelTitle}: `, error);
-
-  const [sortedPackageCardIds, displayAttributionIdsWithCount] =
-    getDisplayPackageInfosWithCount(syncFallbackArgs);
-
-  setDisplayPackageInfosWithCountAndResourceId({
-    resourceId: syncFallbackArgs.selectedResourceId,
-    sortedPackageCardIds,
-    displayPackageInfosWithCount: displayAttributionIdsWithCount,
-  });
-}
diff --git a/src/Frontend/Components/App/App.tsx b/src/Frontend/Components/App/App.tsx
index c80bd725..a83cb0ce 100644
--- a/src/Frontend/Components/App/App.tsx
+++ b/src/Frontend/Components/App/App.tsx
@@ -11,6 +11,7 @@ import { View } from '../../enums/enums';
 import { OpossumColors } from '../../shared-styles';
 import { useAppSelector } from '../../state/hooks';
 import { getSelectedView } from '../../state/selectors/view-selector';
+import { useSignalsWorker } from '../../web-workers/use-signals-worker';
 import { AttributionView } from '../AttributionView/AttributionView';
 import { AuditView } from '../AuditView/AuditView';
 import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary';
@@ -102,6 +103,8 @@ const theme = createTheme({
 });
 
 export function App(): ReactElement {
+  useSignalsWorker();
+
   const selectedView = useAppSelector(getSelectedView);
 
   function getSelectedViewContainer(): ReactElement {
diff --git a/src/Frontend/Components/AppContainer/AppContainer.tsx b/src/Frontend/Components/AppContainer/AppContainer.tsx
index 51a66273..9052adbe 100644
--- a/src/Frontend/Components/AppContainer/AppContainer.tsx
+++ b/src/Frontend/Components/AppContainer/AppContainer.tsx
@@ -11,10 +11,6 @@ import { Provider } from 'react-redux';
 
 import { createAppStore } from '../../state/configure-store';
 import { App } from '../App/App';
-import {
-  AccordionWorkersContextProvider,
-  ProgressBarWorkersContextProvider,
-} from '../WorkersContextProvider/WorkersContextProvider';
 
 dayjs.extend(localizedFormat);
 
@@ -25,13 +21,9 @@ export function AppContainer(): ReactElement {
   return (
     <Provider store={store}>
       <QueryClientProvider client={queryClient}>
-        <AccordionWorkersContextProvider>
-          <ProgressBarWorkersContextProvider>
-            <StrictMode>
-              <App />
-            </StrictMode>
-          </ProgressBarWorkersContextProvider>
-        </AccordionWorkersContextProvider>
+        <StrictMode>
+          <App />
+        </StrictMode>
       </QueryClientProvider>
     </Provider>
   );
diff --git a/src/Frontend/Components/AttributionColumn/LicenseField.tsx b/src/Frontend/Components/AttributionColumn/LicenseField.tsx
deleted file mode 100644
index 8c342521..00000000
--- a/src/Frontend/Components/AttributionColumn/LicenseField.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { ReactElement } from 'react';
-
-import { FrequentLicenseName } from '../../../shared/shared-types';
-import { AutoComplete } from '../InputElements/AutoComplete';
-import { InputElementProps } from '../InputElements/shared';
-
-function isPresentInOptions(
-  inputValue: string,
-  frequentLicenseNames: Array<FrequentLicenseName>,
-): boolean {
-  const matchesValue = (licenseName: FrequentLicenseName): boolean =>
-    licenseName.shortName === inputValue || licenseName.fullName === inputValue;
-  return frequentLicenseNames.some(matchesValue);
-}
-
-function getFormattedLicenseNamesToShortNameMapping(
-  frequentLicenseNames: Array<FrequentLicenseName>,
-): {
-  [key: string]: string;
-} {
-  function formatLicenseName(licenseName: FrequentLicenseName): string {
-    return `${licenseName.shortName} - ${licenseName.fullName}`;
-  }
-
-  return Object.fromEntries(
-    frequentLicenseNames.map((option: FrequentLicenseName) => [
-      formatLicenseName(option),
-      option.shortName,
-    ]),
-  );
-}
-
-interface LicenseFieldProps extends InputElementProps {
-  frequentLicenseNames: Array<FrequentLicenseName>;
-  endAdornmentText?: string;
-}
-
-export function LicenseField(props: LicenseFieldProps): ReactElement {
-  const formattedLicenseNamesToShortNameMapping =
-    getFormattedLicenseNamesToShortNameMapping(props.frequentLicenseNames);
-  const sortedLicenses = Object.keys(
-    formattedLicenseNamesToShortNameMapping,
-  ).sort((license, otherLicense) =>
-    license.toLowerCase() < otherLicense.toLowerCase() ? -1 : 1,
-  );
-
-  function formatOptionForDisplay(option: string): string {
-    return formattedLicenseNamesToShortNameMapping[option]
-      ? formattedLicenseNamesToShortNameMapping[option]
-      : option;
-  }
-
-  const inputValue = props.text || '';
-  const inputValueIsInOptions = isPresentInOptions(
-    inputValue,
-    props.frequentLicenseNames,
-  );
-
-  return (
-    <AutoComplete
-      isEditable={props.isEditable}
-      fullWidth
-      title={props.title}
-      handleChange={props.handleChange}
-      isHighlighted={props.isHighlighted}
-      options={sortedLicenses}
-      endAdornmentText={props.endAdornmentText}
-      inputValue={inputValue}
-      showTextBold={inputValueIsInOptions}
-      formatOptionForDisplay={formatOptionForDisplay}
-    />
-  );
-}
diff --git a/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx b/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
index 8539db7d..29d5bf4d 100644
--- a/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
+++ b/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
@@ -7,20 +7,23 @@ import MuiAccordion from '@mui/material/Accordion';
 import MuiAccordionDetails from '@mui/material/AccordionDetails';
 import MuiAccordionSummary from '@mui/material/AccordionSummary';
 import MuiBox from '@mui/material/Box';
-import { ReactElement, useState } from 'react';
+import MuiInputAdornment from '@mui/material/InputAdornment';
+import { sortBy } from 'lodash';
+import { ReactElement, useMemo, useState } from 'react';
 
-import { DisplayPackageInfo } from '../../../shared/shared-types';
+import { DisplayPackageInfo, PackageInfo } from '../../../shared/shared-types';
+import { text } from '../../../shared/text';
 import { OpossumColors } from '../../shared-styles';
 import { useAppSelector } from '../../state/hooks';
 import { getFrequentLicensesNameOrder } from '../../state/selectors/all-views-resource-selectors';
-import { isImportantAttributionInformationMissing } from '../../util/is-important-attribution-information-missing';
 import { usePackageInfoChangeHandler } from '../../util/use-package-info-change-handler';
 import { TextBox } from '../InputElements/TextBox';
 import { getLicenseTextLabelText } from './attribution-column-helpers';
-import { LicenseField } from './LicenseField';
+import { PackageAutocomplete } from './PackageAutocomplete';
 import { attributionColumnClasses } from './shared-attribution-column-styles';
 
 const classes = {
+  ...attributionColumnClasses,
   expansionPanel: {
     backgroundColor: OpossumColors.lightestBlue,
     '&.MuiAccordion-expanded': {
@@ -52,6 +55,10 @@ const classes = {
   licenseText: {
     marginTop: '12px',
   },
+  endAdornment: {
+    paddingRight: '6px',
+    paddingTop: '2px',
+  },
 };
 
 interface LicenseSubPanelProps {
@@ -62,63 +69,70 @@ interface LicenseSubPanelProps {
 
 export function LicenseSubPanel(props: LicenseSubPanelProps): ReactElement {
   const [expanded, setExpanded] = useState(false);
-  const licenseSubPanelClasses = { ...attributionColumnClasses, ...classes };
-
-  const frequentLicensesNameOrder = useAppSelector(
-    getFrequentLicensesNameOrder,
+  const frequentLicensesNames = useAppSelector(getFrequentLicensesNameOrder);
+  const defaultLicenses = useMemo(
+    () =>
+      sortBy(
+        frequentLicensesNames.map<PackageInfo>(({ fullName }) => ({
+          licenseName: fullName,
+          source: {
+            documentConfidence: 100,
+            name: text.attributionColumn.commonLicenses,
+          },
+        })),
+        ({ licenseName }) => licenseName?.toLowerCase(),
+      ),
+    [frequentLicensesNames],
   );
 
   const handleChange = usePackageInfoChangeHandler();
 
   return (
-    <MuiBox sx={licenseSubPanelClasses.panel}>
+    <MuiBox sx={classes.panel}>
       <MuiAccordion
-        sx={licenseSubPanelClasses.expansionPanel}
+        sx={classes.expansionPanel}
         elevation={0}
         key={'License'}
         disableGutters
         expanded={expanded}
       >
         <MuiAccordionSummary
-          sx={licenseSubPanelClasses.expansionPanelSummary}
+          sx={classes.expansionPanelSummary}
           expandIcon={
             <MuiBox
-              sx={licenseSubPanelClasses.expandMoreIcon}
+              sx={classes.expandMoreIcon}
               onClick={() => setExpanded((prev) => !prev)}
             >
               <ExpandMoreIcon aria-label={'license text toggle'} />
             </MuiBox>
           }
         >
-          <LicenseField
-            isEditable={props.isEditable}
-            title={'License Name'}
-            text={props.displayPackageInfo.licenseName}
-            frequentLicenseNames={frequentLicensesNameOrder}
-            handleChange={handleChange('licenseName')}
-            endAdornmentText={
-              props.displayPackageInfo.licenseText && '(License text modified)'
-            }
-            isHighlighted={
-              props.showHighlight &&
-              isImportantAttributionInformationMissing(
-                'licenseName',
-                props.displayPackageInfo,
-              )
+          <PackageAutocomplete
+            attribute={'licenseName'}
+            title={text.attributionColumn.licenseName}
+            disabled={!props.isEditable}
+            showHighlight={props.showHighlight}
+            endAdornment={
+              props.displayPackageInfo.licenseText ? (
+                <MuiInputAdornment position="end" sx={classes.endAdornment}>
+                  {text.attributionColumn.licenseTextModified}
+                </MuiInputAdornment>
+              ) : undefined
             }
+            defaults={defaultLicenses}
           />
         </MuiAccordionSummary>
-        <MuiAccordionDetails sx={licenseSubPanelClasses.expansionPanelDetails}>
+        <MuiAccordionDetails sx={classes.expansionPanelDetails}>
           <TextBox
             isEditable={props.isEditable}
-            sx={licenseSubPanelClasses.licenseText}
+            sx={classes.licenseText}
             minRows={3}
             maxRows={10}
             multiline={true}
             title={getLicenseTextLabelText(
               props.displayPackageInfo.licenseName,
               props.isEditable,
-              frequentLicensesNameOrder,
+              frequentLicensesNames,
             )}
             text={props.displayPackageInfo.licenseText}
             handleChange={handleChange('licenseText')}
diff --git a/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx b/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
new file mode 100644
index 00000000..1556bd2f
--- /dev/null
+++ b/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
@@ -0,0 +1,241 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import AddIcon from '@mui/icons-material/Add';
+import ExploreIcon from '@mui/icons-material/Explore';
+import StarIcon from '@mui/icons-material/Star';
+import { styled } from '@mui/material';
+import MuiBadge from '@mui/material/Badge';
+import MuiChip from '@mui/material/Chip';
+import MuiIconButton from '@mui/material/IconButton';
+import MuiTooltip from '@mui/material/Tooltip';
+import { compact } from 'lodash';
+import { useMemo } from 'react';
+
+import { PackageInfo, SignalWithCount } from '../../../shared/shared-types';
+import { text } from '../../../shared/text';
+import { baseIcon, OpossumColors } from '../../shared-styles';
+import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
+import { savePackageInfo } from '../../state/actions/resource-actions/save-actions';
+import { useAppDispatch, useAppSelector } from '../../state/hooks';
+import {
+  getAttributionIdOfDisplayedPackageInManualPanel,
+  getExternalAttributionSources,
+  getTemporaryDisplayPackageInfo,
+} from '../../state/selectors/all-views-resource-selectors';
+import { getSelectedResourceId } from '../../state/selectors/audit-view-resource-selectors';
+import { isAuditViewSelected } from '../../state/selectors/view-selector';
+import { generatePurl } from '../../util/handle-purl';
+import { isImportantAttributionInformationMissing } from '../../util/is-important-attribution-information-missing';
+import { maybePluralize } from '../../util/maybe-pluralize';
+import { useAutocompleteSignals } from '../../web-workers/use-signals-worker';
+import { Autocomplete } from '../Autocomplete/Autocomplete';
+
+type AutocompleteAttribute = Extract<
+  keyof PackageInfo,
+  | 'packageType'
+  | 'packageNamespace'
+  | 'packageName'
+  | 'packageVersion'
+  | 'url'
+  | 'licenseName'
+>;
+
+interface Props {
+  title: string;
+  attribute: AutocompleteAttribute;
+  highlight?: 'default' | 'dark';
+  endAdornment?: React.ReactElement;
+  defaults?: Array<SignalWithCount>;
+  disabled: boolean;
+  showHighlight: boolean | undefined;
+}
+
+const Badge = styled(MuiBadge)({
+  '& .MuiBadge-badge': {
+    top: '1px',
+    right: '14px',
+    zIndex: 0,
+  },
+});
+
+export function PackageAutocomplete({
+  attribute,
+  title,
+  highlight,
+  endAdornment,
+  defaults = [],
+  disabled,
+  showHighlight,
+}: Props) {
+  const dispatch = useAppDispatch();
+  const temporaryPackageInfo = useAppSelector(getTemporaryDisplayPackageInfo);
+  const resourceId = useAppSelector(getSelectedResourceId);
+  const attributionIdOfSelectedPackageInManualPanel = useAppSelector(
+    getAttributionIdOfDisplayedPackageInManualPanel,
+  );
+  const sources = useAppSelector(getExternalAttributionSources);
+  const isAuditView = useAppSelector(isAuditViewSelected);
+
+  const autocompleteSignals = useAutocompleteSignals();
+  const filteredSignals = useMemo(
+    () =>
+      isAuditView
+        ? autocompleteSignals
+            .filter((signal) => !['', undefined].includes(signal[attribute]))
+            .concat(defaults)
+        : [],
+    [isAuditView, autocompleteSignals, defaults, attribute],
+  );
+
+  return (
+    <Autocomplete
+      title={title}
+      disabled={disabled}
+      autoHighlight
+      disableClearable
+      freeSolo
+      highlight={
+        showHighlight &&
+        isImportantAttributionInformationMissing(
+          attribute,
+          temporaryPackageInfo,
+        )
+          ? highlight
+          : undefined
+      }
+      options={filteredSignals}
+      getOptionLabel={(option) =>
+        typeof option === 'string' ? option : option[attribute] || ''
+      }
+      getOptionKey={(option) =>
+        typeof option === 'string'
+          ? option
+          : compact([
+              option.copyright,
+              option.licenseName,
+              option[attribute],
+              generatePurl(option),
+            ]).join()
+      }
+      renderOptionStartIcon={renderOptionStartIcon}
+      renderOptionEndIcon={renderOptionEndIcon}
+      value={temporaryPackageInfo as SignalWithCount}
+      isOptionEqualToValue={(option, value) =>
+        option[attribute] === value[attribute]
+      }
+      groupBy={(option) =>
+        option.source
+          ? sources[option.source.name]?.name || option.source.name
+          : text.attributionColumn.manualAttributions
+      }
+      groupIcon={
+        <ExploreIcon
+          sx={{
+            ...baseIcon,
+            color: `${OpossumColors.black} !important`,
+          }}
+        />
+      }
+      optionText={{
+        primary: (option) =>
+          typeof option === 'string' ? option : option[attribute],
+        secondary: (option) =>
+          typeof option === 'string' ? option : generatePurl(option),
+      }}
+      onInputChange={(_, value) => {
+        temporaryPackageInfo[attribute] !== value &&
+          dispatch(
+            setTemporaryDisplayPackageInfo({
+              ...temporaryPackageInfo,
+              [attribute]: value,
+            }),
+          );
+      }}
+      endAdornment={endAdornment}
+    />
+  );
+
+  function renderOptionStartIcon(option: SignalWithCount) {
+    if (!option.count) {
+      return null;
+    }
+
+    const showStar = option.preferred || option.wasPreferred;
+    const baseTitle = `${maybePluralize(
+      option.count,
+      text.attributionColumn.occurrence,
+    )} ${text.attributionColumn.amongSignals}`;
+
+    return (
+      <MuiTooltip
+        title={
+          showStar
+            ? `${baseTitle} (${
+                option.preferred
+                  ? text.auditingOptions.currentlyPreferred
+                  : text.auditingOptions.previouslyPreferred
+              })`
+            : baseTitle
+        }
+      >
+        <Badge
+          badgeContent={
+            showStar && (
+              <StarIcon
+                sx={{
+                  ...baseIcon,
+                  color: option.preferred
+                    ? OpossumColors.mediumOrange
+                    : OpossumColors.mediumGrey,
+                }}
+              />
+            )
+          }
+        >
+          <MuiChip
+            label={option.count}
+            size={'small'}
+            sx={{ marginRight: '12px' }}
+          />
+        </Badge>
+      </MuiTooltip>
+    );
+  }
+
+  function renderOptionEndIcon(
+    { preSelected, ...option }: PackageInfo,
+    { closePopper }: { closePopper: () => void },
+  ) {
+    if (!option.packageName || !option.packageType) {
+      return null;
+    }
+
+    return (
+      <MuiTooltip
+        title={text.attributionColumn.useAutocompleteSuggestion}
+        disableInteractive
+      >
+        <MuiIconButton
+          onClick={(event) => {
+            event.stopPropagation();
+            dispatch(
+              savePackageInfo(
+                resourceId,
+                attributionIdOfSelectedPackageInManualPanel,
+                option,
+                undefined,
+                true,
+              ),
+            );
+            closePopper();
+          }}
+          size={'small'}
+        >
+          <AddIcon fontSize={'inherit'} color={'primary'} />
+        </MuiIconButton>
+      </MuiTooltip>
+    );
+  }
+}
diff --git a/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx b/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
index 38b361c5..48f8b891 100644
--- a/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
+++ b/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
@@ -7,30 +7,44 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
 import ContentPasteIcon from '@mui/icons-material/ContentPaste';
 import OpenInNewIcon from '@mui/icons-material/OpenInNew';
 import MuiBox from '@mui/material/Box';
+import { styled } from '@mui/system';
+import { useMemo } from 'react';
 
-import { DisplayPackageInfo } from '../../../shared/shared-types';
+import { DisplayPackageInfo, PackageInfo } from '../../../shared/shared-types';
 import { text } from '../../../shared/text';
-import { HighlightingColor, PopupType } from '../../enums/enums';
-import { clickableIcon, disabledIcon } from '../../shared-styles';
+import { PopupType } from '../../enums/enums';
+import { clickableIcon } from '../../shared-styles';
 import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
 import { openPopup } from '../../state/actions/view-actions/view-actions';
 import { useAppDispatch } from '../../state/hooks';
 import { generatePurl, parsePurl } from '../../util/handle-purl';
-import { isImportantAttributionInformationMissing } from '../../util/is-important-attribution-information-missing';
 import { openUrl } from '../../util/open-url';
-import { usePackageInfoChangeHandler } from '../../util/use-package-info-change-handler';
 import { FetchLicenseInformationButton } from '../FetchLicenseInformationButton/FetchLicenseInformationButton';
 import { IconButton } from '../IconButton/IconButton';
 import { SearchPackagesIcon } from '../Icons/Icons';
 import { TextBox } from '../InputElements/TextBox';
+import { PackageAutocomplete } from './PackageAutocomplete';
 import { attributionColumnClasses } from './shared-attribution-column-styles';
 
-const classes = {
-  displayRow: {
-    display: 'flex',
-    gap: '8px',
-  },
-};
+const COMMON_PACKAGE_TYPES = [
+  'bitbucket',
+  'cargo',
+  'deb',
+  'docker',
+  'gem',
+  'github',
+  'golang',
+  'maven',
+  'npm',
+  'nuget',
+  'pypi',
+  'rpm',
+];
+
+const DisplayRow = styled('div')({
+  display: 'flex',
+  gap: '8px',
+});
 
 interface PackageSubPanelProps {
   displayPackageInfo: DisplayPackageInfo;
@@ -40,113 +54,88 @@ interface PackageSubPanelProps {
 
 export function PackageSubPanel(props: PackageSubPanelProps) {
   const dispatch = useAppDispatch();
-  const handleChange = usePackageInfoChangeHandler();
+  const defaultPackageTypes = useMemo(
+    () =>
+      COMMON_PACKAGE_TYPES.map<PackageInfo>((packageType) => ({
+        packageType,
+        source: {
+          name: text.attributionColumn.commonEcosystems,
+          documentConfidence: 100,
+        },
+      })),
+    [],
+  );
 
   return (
     <MuiBox sx={attributionColumnClasses.panel}>
-      <MuiBox sx={classes.displayRow}>
+      <DisplayRow>
         {renderPackageName()}
         {renderPackageNamespace()}
-      </MuiBox>
-      <MuiBox sx={classes.displayRow}>
+      </DisplayRow>
+      <DisplayRow>
         {renderPackageVersion()}
         {renderPackageType()}
-      </MuiBox>
+      </DisplayRow>
       {renderPurl()}
       {renderRepositoryUrl()}
     </MuiBox>
   );
 
-  function renderPackageType() {
+  function renderPackageName() {
     return (
-      <TextBox
-        sx={attributionColumnClasses.textBox}
-        title={text.attributionColumn.packageSubPanel.packageType}
-        text={props.displayPackageInfo.packageType}
-        handleChange={handleChange('packageType')}
-        isEditable={props.isEditable}
-        isHighlighted={
-          props.showHighlight &&
-          isImportantAttributionInformationMissing(
-            'packageType',
-            props.displayPackageInfo,
-          )
+      <PackageAutocomplete
+        attribute={'packageName'}
+        title={text.attributionColumn.packageSubPanel.packageName}
+        highlight={'dark'}
+        disabled={!props.isEditable}
+        showHighlight={props.showHighlight}
+        endAdornment={
+          <IconButton
+            tooltipTitle={
+              text.attributionColumn.packageSubPanel.searchForPackage
+            }
+            tooltipPlacement="right"
+            onClick={() => dispatch(openPopup(PopupType.PackageSearchPopup))}
+            hidden={!props.isEditable}
+            icon={<SearchPackagesIcon sx={clickableIcon} />}
+          />
         }
-        highlightingColor={HighlightingColor.DarkOrange}
       />
     );
   }
 
   function renderPackageNamespace() {
     return (
-      <TextBox
-        sx={attributionColumnClasses.textBox}
+      <PackageAutocomplete
+        attribute={'packageNamespace'}
         title={text.attributionColumn.packageSubPanel.packageNamespace}
-        text={props.displayPackageInfo.packageNamespace}
-        handleChange={handleChange('packageNamespace')}
-        isEditable={props.isEditable}
-        isHighlighted={
-          props.showHighlight &&
-          isImportantAttributionInformationMissing(
-            'packageNamespace',
-            props.displayPackageInfo,
-          )
-        }
-        highlightingColor={HighlightingColor.DarkOrange}
+        highlight={'dark'}
+        disabled={!props.isEditable}
+        showHighlight={props.showHighlight}
       />
     );
   }
 
-  function renderPackageName() {
+  function renderPackageVersion() {
     return (
-      <TextBox
-        sx={attributionColumnClasses.textBox}
-        title={text.attributionColumn.packageSubPanel.packageName}
-        text={props.displayPackageInfo.packageName}
-        handleChange={handleChange('packageName')}
-        isEditable={props.isEditable}
-        endIcon={
-          <IconButton
-            tooltipTitle={
-              text.attributionColumn.packageSubPanel.searchForPackage
-            }
-            tooltipPlacement="right"
-            onClick={() => dispatch(openPopup(PopupType.PackageSearchPopup))}
-            disabled={!props.isEditable}
-            icon={
-              <SearchPackagesIcon
-                sx={props.isEditable ? clickableIcon : disabledIcon}
-              />
-            }
-          />
-        }
-        isHighlighted={
-          props.showHighlight &&
-          isImportantAttributionInformationMissing(
-            'packageName',
-            props.displayPackageInfo,
-          )
-        }
-        highlightingColor={HighlightingColor.DarkOrange}
+      <PackageAutocomplete
+        attribute={'packageVersion'}
+        title={text.attributionColumn.packageSubPanel.packageVersion}
+        disabled={!props.isEditable}
+        showHighlight={props.showHighlight}
       />
     );
   }
 
-  function renderPackageVersion() {
+  function renderPackageType() {
     return (
-      <TextBox
-        sx={attributionColumnClasses.textBox}
-        title={text.attributionColumn.packageSubPanel.packageVersion}
-        text={props.displayPackageInfo.packageVersion}
-        handleChange={handleChange('packageVersion')}
-        isEditable={props.isEditable}
-        isHighlighted={
-          props.showHighlight &&
-          isImportantAttributionInformationMissing(
-            'packageVersion',
-            props.displayPackageInfo,
-          )
-        }
+      <PackageAutocomplete
+        attribute={'packageType'}
+        title={text.attributionColumn.packageSubPanel.packageType}
+        highlight={'dark'}
+        disabled={!props.isEditable}
+        showHighlight={props.showHighlight}
+        defaults={defaultPackageTypes}
       />
     );
   }
@@ -208,45 +197,36 @@ export function PackageSubPanel(props: PackageSubPanelProps) {
     );
   }
 
-  function renderRepositoryUrl(): React.ReactElement {
+  function renderRepositoryUrl() {
     return (
-      <TextBox
-        isEditable={props.isEditable}
-        sx={attributionColumnClasses.textBox}
+      <PackageAutocomplete
+        attribute={'url'}
         title={text.attributionColumn.packageSubPanel.repositoryUrl}
-        text={props.displayPackageInfo.url}
-        handleChange={handleChange('url')}
-        endIcon={
+        disabled={!props.isEditable}
+        showHighlight={props.showHighlight}
+        endAdornment={
           <>
             <FetchLicenseInformationButton
               url={props.displayPackageInfo.url}
               version={props.displayPackageInfo.packageVersion}
               disabled={!props.isEditable}
             />
-            {!!props.displayPackageInfo.url && (
-              <IconButton
-                tooltipTitle={
-                  text.attributionColumn.packageSubPanel.openLinkInBrowser
-                }
-                tooltipPlacement="right"
-                onClick={(): void => {
-                  props.displayPackageInfo.url &&
-                    openUrl(props.displayPackageInfo.url);
-                }}
-                icon={
-                  <OpenInNewIcon aria-label={'Url icon'} sx={clickableIcon} />
-                }
-              />
-            )}
+            <IconButton
+              tooltipTitle={
+                text.attributionColumn.packageSubPanel.openLinkInBrowser
+              }
+              tooltipPlacement="right"
+              onClick={(): void => {
+                props.displayPackageInfo.url &&
+                  openUrl(props.displayPackageInfo.url);
+              }}
+              hidden={!props.displayPackageInfo.url}
+              icon={
+                <OpenInNewIcon aria-label={'Url icon'} sx={clickableIcon} />
+              }
+            />
           </>
         }
-        isHighlighted={
-          props.showHighlight &&
-          isImportantAttributionInformationMissing(
-            'url',
-            props.displayPackageInfo,
-          )
-        }
       />
     );
   }
diff --git a/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx b/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx
new file mode 100644
index 00000000..2c9a24fe
--- /dev/null
+++ b/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx
@@ -0,0 +1,102 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { Popper, PopperProps, styled, SxProps } from '@mui/material';
+import MuiTextField from '@mui/material/TextField';
+
+import { OpossumColors } from '../../shared-styles';
+
+export const Container = styled('div')({
+  flex: 1,
+});
+
+export const TagsContainer = styled('div')({
+  display: 'flex',
+  gap: '8px',
+  flexWrap: 'wrap',
+});
+
+export const Input = styled(MuiTextField, {
+  shouldForwardProp: (name: string) =>
+    !['highlight', 'numberOfEndAdornments'].includes(name),
+})<{
+  highlight: 'default' | 'dark' | undefined;
+  numberOfEndAdornments: number;
+}>(({ highlight, numberOfEndAdornments }) => ({
+  '& .MuiInputLabel-root': {
+    backgroundColor: highlight
+      ? highlight === 'default'
+        ? OpossumColors.lightOrange
+        : OpossumColors.darkOrange
+      : OpossumColors.white,
+    padding: '0px 3px',
+    fontSize: '13px',
+    top: '1px',
+  },
+  '& .MuiInputBase-root': {
+    backgroundColor: highlight
+      ? highlight === 'default'
+        ? OpossumColors.lightOrange
+        : OpossumColors.darkOrange
+      : OpossumColors.white,
+    borderRadius: '0px',
+    display: 'flex',
+    flexWrap: 'wrap',
+    alignItems: 'center',
+    gap: '4px',
+    minHeight: '36.67px',
+    paddingTop: '6px',
+    paddingBottom: '6px',
+    paddingLeft: '14px',
+    paddingRight: `calc(14px + ${numberOfEndAdornments} * 28px)`,
+  },
+  '& .MuiInputBase-input': {
+    flex: 1,
+    padding: 0,
+  },
+  '& legend': {
+    '& span': {
+      display: 'none',
+    },
+  },
+}));
+
+export const StyledPopper = styled((props: PopperProps) => (
+  <Popper
+    {...props}
+    placement={'auto'}
+    modifiers={[
+      {
+        name: 'preventOverflow',
+        enabled: true,
+      },
+      {
+        name: 'flip',
+        options: {
+          padding: 64,
+          allowedAutoPlacements: ['top', 'bottom'],
+        },
+      },
+    ]}
+  />
+))(({ theme, anchorEl }) => ({
+  width: (anchorEl as HTMLElement | null)?.clientWidth,
+  zIndex: theme.zIndex.modal,
+}));
+
+export const EndAdornmentContainer = styled('div')({
+  position: 'absolute',
+  right: '14px',
+  display: 'flex',
+  height: '100%',
+  alignItems: 'center',
+});
+
+export const styles = {
+  overflowEllipsis: {
+    overflow: 'hidden',
+    textOverflow: 'ellipsis',
+    whiteSpace: 'nowrap',
+  },
+} satisfies SxProps;
diff --git a/src/Frontend/Components/Autocomplete/Autocomplete.tsx b/src/Frontend/Components/Autocomplete/Autocomplete.tsx
new file mode 100644
index 00000000..636965bc
--- /dev/null
+++ b/src/Frontend/Components/Autocomplete/Autocomplete.tsx
@@ -0,0 +1,237 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { SxProps } from '@mui/material';
+import MuiChip from '@mui/material/Chip';
+import MuiFade from '@mui/material/Fade';
+import { IconButtonProps as MuiIconButtonProps } from '@mui/material/IconButton';
+import { InputBaseComponentProps as MuiInputBaseComponentProps } from '@mui/material/InputBase';
+import useMuiAutocomplete, {
+  AutocompleteHighlightChangeReason,
+  UseAutocompleteProps as MuiUseAutocompleteProps,
+} from '@mui/material/useAutocomplete';
+import { compact } from 'lodash';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { VirtuosoHandle } from 'react-virtuoso';
+
+import { ClearButton } from '../ClearButton/ClearButton';
+import { PopupIndicator } from '../PopupIndicator/PopupIndicator';
+import {
+  Container,
+  EndAdornmentContainer,
+  Input,
+  StyledPopper,
+} from './Autocomplete.style';
+import { Listbox, ListboxProps } from './Listbox/Listbox';
+
+type AutocompleteProps<
+  Value,
+  Multiple extends boolean | undefined,
+  DisableClearable extends boolean | undefined,
+  FreeSolo extends boolean | undefined,
+> = Omit<
+  MuiUseAutocompleteProps<Value, Multiple, DisableClearable, FreeSolo>,
+  'onClose' | 'onOpen' | 'open' | 'onHighlightChange'
+> &
+  Pick<
+    ListboxProps<Value, FreeSolo>,
+    | 'getOptionKey'
+    | 'groupIcon'
+    | 'optionText'
+    | 'renderOptionEndIcon'
+    | 'renderOptionStartIcon'
+  > & {
+    endAdornment?: React.ReactNode;
+    highlight?: 'default' | 'dark';
+    inputProps?: MuiInputBaseComponentProps;
+    sx?: SxProps;
+    title: string;
+  };
+
+export function Autocomplete<
+  Value,
+  Multiple extends boolean | undefined,
+  DisableClearable extends boolean | undefined,
+  FreeSolo extends boolean | undefined,
+>({
+  disableClearable,
+  disableListWrap = true,
+  disabled,
+  endAdornment,
+  freeSolo,
+  getOptionKey,
+  getOptionLabel,
+  groupBy,
+  groupIcon,
+  highlight,
+  inputProps,
+  multiple,
+  optionText,
+  renderOptionEndIcon,
+  renderOptionStartIcon,
+  sx,
+  title,
+  ...props
+}: AutocompleteProps<Value, Multiple, DisableClearable, FreeSolo>) {
+  const [open, setOpen] = useState(false);
+  const closePopper = () => setOpen(false);
+
+  const groupedOptionsRef = useRef<Array<Value> | null>(null);
+  const virtuosoRef = useRef<VirtuosoHandle>(null);
+  const onHighlightChange = useCallback(
+    (
+      event: React.SyntheticEvent,
+      option: Value | null,
+      reason: AutocompleteHighlightChangeReason,
+    ) => {
+      if (reason !== 'keyboard' || event.type !== 'keydown' || !option) {
+        return;
+      }
+
+      const index = groupedOptionsRef.current?.findIndex(
+        (item) => item === option,
+      );
+
+      if (index !== undefined) {
+        virtuosoRef.current?.scrollToIndex({
+          index,
+          behavior: 'auto',
+          align: 'center',
+        });
+      }
+    },
+    [],
+  );
+
+  const {
+    anchorEl,
+    dirty: containsValue,
+    getClearProps,
+    getInputLabelProps,
+    getInputProps,
+    getListboxProps,
+    getOptionProps,
+    getPopupIndicatorProps,
+    getRootProps,
+    getTagProps,
+    groupedOptions,
+    popupOpen,
+    setAnchorEl,
+    value,
+  } = useMuiAutocomplete<Value, Multiple, DisableClearable, FreeSolo>({
+    disableClearable,
+    disableListWrap,
+    disabled,
+    freeSolo,
+    getOptionLabel,
+    multiple,
+    onClose: () => setOpen(false),
+    onHighlightChange,
+    onOpen: () => setOpen(true),
+    open,
+    ...props,
+  });
+
+  useEffect(() => {
+    groupedOptionsRef.current = groupedOptions as Array<Value>;
+  }, [groupedOptions]);
+
+  const hasPopupIndicator = !freeSolo;
+  const hasClearButton = !disableClearable && !disabled && containsValue;
+  const isPopupOpen = !!groupedOptions.length && popupOpen;
+
+  return (
+    <>
+      <Container {...getRootProps()} sx={sx} ref={setAnchorEl}>
+        <Input
+          disabled={disabled}
+          label={title}
+          highlight={highlight}
+          numberOfEndAdornments={
+            compact([hasClearButton, hasPopupIndicator, !!endAdornment]).length
+          }
+          size={'small'}
+          InputLabelProps={getInputLabelProps()}
+          inputProps={{ ...getInputProps(), ...inputProps }}
+          InputProps={{
+            startAdornment: renderStartAdornment(),
+            endAdornment: renderEndAdornment(),
+          }}
+          onKeyDown={(event) => {
+            // https://github.com/mui/material-ui/issues/21129
+            event.key === 'Backspace' && event.stopPropagation();
+          }}
+          fullWidth
+        />
+      </Container>
+      {renderPopper()}
+    </>
+  );
+
+  function renderStartAdornment() {
+    if (!multiple || !Array.isArray(value) || !value.length) {
+      return null;
+    }
+
+    return value.map((option, index) => {
+      const { key, ...tagProps } = getTagProps({ index });
+      const label = getOptionLabel?.(option) ?? option;
+
+      return (
+        <MuiChip
+          size={'small'}
+          key={key}
+          label={label}
+          {...tagProps}
+          data-testid={`tag-${label}`}
+        />
+      );
+    });
+  }
+
+  function renderEndAdornment() {
+    if (!endAdornment && !hasPopupIndicator && !hasClearButton) {
+      return null;
+    }
+
+    return (
+      <EndAdornmentContainer>
+        {hasClearButton && (
+          <ClearButton {...(getClearProps() as MuiIconButtonProps)} />
+        )}
+        {hasPopupIndicator && (
+          <PopupIndicator
+            popupOpen={isPopupOpen}
+            {...(getPopupIndicatorProps() as MuiIconButtonProps)}
+          />
+        )}
+        {endAdornment}
+      </EndAdornmentContainer>
+    );
+  }
+
+  function renderPopper() {
+    return (
+      <StyledPopper anchorEl={anchorEl} open={isPopupOpen} transition>
+        {({ TransitionProps }) => (
+          <MuiFade {...TransitionProps} timeout={300}>
+            <Listbox
+              {...getListboxProps()}
+              virtuosoRef={virtuosoRef}
+              closePopper={closePopper}
+              optionText={optionText}
+              options={groupedOptions as Array<Value>}
+              groupBy={groupBy}
+              groupIcon={groupIcon}
+              getOptionKey={getOptionKey}
+              getOptionProps={getOptionProps}
+              renderOptionStartIcon={renderOptionStartIcon}
+              renderOptionEndIcon={renderOptionEndIcon}
+            />
+          </MuiFade>
+        )}
+      </StyledPopper>
+    );
+  }
+}
diff --git a/src/Frontend/Components/Autocomplete/Listbox/Listbox.style.ts b/src/Frontend/Components/Autocomplete/Listbox/Listbox.style.ts
new file mode 100644
index 00000000..6970f722
--- /dev/null
+++ b/src/Frontend/Components/Autocomplete/Listbox/Listbox.style.ts
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { styled, SxProps } from '@mui/material';
+
+import { OpossumColors } from '../../../shared-styles';
+
+export const GroupContainer = styled('div')({
+  display: 'flex',
+  alignItems: 'center',
+  gap: '8px',
+  padding: '4px 10px',
+  backgroundColor: OpossumColors.lightBlue,
+});
+
+export const styles = {
+  overflowEllipsis: {
+    overflow: 'hidden',
+    textOverflow: 'ellipsis',
+    whiteSpace: 'nowrap',
+  },
+  virtuoso: {
+    maxHeight: '40vh',
+    transition: 'height all 0.2s ease-out',
+  },
+} satisfies SxProps;
diff --git a/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx b/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx
new file mode 100644
index 00000000..9e4641db
--- /dev/null
+++ b/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx
@@ -0,0 +1,191 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import MuiListItemButton from '@mui/material/ListItemButton';
+import MuiListItemText from '@mui/material/ListItemText';
+import MuiPaper from '@mui/material/Paper';
+import MuiTypography from '@mui/material/Typography';
+import {
+  AutocompleteFreeSoloValueMapping,
+  UseAutocompleteRenderedOption,
+} from '@mui/material/useAutocomplete';
+import { groupBy as _groupBy } from 'lodash';
+import { forwardRef, useMemo, useState } from 'react';
+import { GroupedVirtuoso, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
+
+import { GroupContainer, styles } from './Listbox.style';
+
+export type ListboxProps<
+  Value,
+  FreeSolo extends boolean | undefined,
+> = React.HTMLAttributes<HTMLElement> & {
+  virtuosoRef: React.RefObject<VirtuosoHandle>;
+  options: Array<Value>;
+  groupIcon?: React.ReactNode;
+  closePopper: () => void;
+  groupBy?: (option: Value) => string;
+  getOptionKey?: (
+    option: Value | AutocompleteFreeSoloValueMapping<FreeSolo>,
+  ) => string;
+  getOptionProps: (
+    renderedOption: UseAutocompleteRenderedOption<Value>,
+  ) => React.HTMLAttributes<HTMLElement> & React.Attributes;
+  renderOptionStartIcon?: (
+    option: Value,
+    { closePopper }: { closePopper: () => void },
+  ) => React.ReactNode;
+  renderOptionEndIcon?: (
+    option: Value,
+    { closePopper }: { closePopper: () => void },
+  ) => React.ReactNode;
+  optionText: {
+    primary: (
+      option: Value | AutocompleteFreeSoloValueMapping<FreeSolo>,
+    ) => React.ReactNode;
+    secondary?: (
+      option: Value | AutocompleteFreeSoloValueMapping<FreeSolo>,
+    ) => React.ReactNode;
+  };
+};
+
+interface Groups<Value> {
+  options: Array<Value>;
+  groupNames: Array<string>;
+  groupCounts: Array<number>;
+  firstSelectedIndex: number;
+}
+
+export const Listbox = forwardRef(
+  <Value, FreeSolo extends boolean | undefined>(
+    {
+      virtuosoRef,
+      closePopper,
+      getOptionKey,
+      getOptionProps,
+      groupBy,
+      groupIcon,
+      optionText,
+      options,
+      renderOptionEndIcon,
+      renderOptionStartIcon,
+      ...listboxProps
+    }: ListboxProps<Value, FreeSolo>,
+    forwardedRef: React.ForwardedRef<HTMLDivElement>,
+  ) => {
+    const [height, setHeight] = useState<number>(Number.MAX_SAFE_INTEGER); // will result in max-height
+
+    const groups = useMemo((): Groups<Value> | undefined => {
+      if (!groupBy) {
+        return undefined;
+      }
+
+      const grouped = _groupBy(options, groupBy);
+      const flattened = Object.values(grouped).flat();
+
+      return {
+        options: flattened,
+        groupNames: Object.keys(grouped),
+        groupCounts: Object.values(grouped).map((group) => group.length),
+        firstSelectedIndex: flattened.findIndex(
+          (option, index) =>
+            !!getOptionProps({ option, index })['aria-selected'],
+        ),
+      };
+    }, [getOptionProps, groupBy, options]);
+
+    return (
+      <MuiPaper
+        {...listboxProps}
+        ref={forwardedRef}
+        elevation={4}
+        square
+        role={'listbox'}
+      >
+        {groups ? renderGroupedList(groups) : renderList(options)}
+      </MuiPaper>
+    );
+
+    function renderGroupedList({
+      groupCounts,
+      groupNames,
+      options,
+      firstSelectedIndex,
+    }: Groups<Value>) {
+      return (
+        <GroupedVirtuoso
+          ref={virtuosoRef}
+          style={{ ...styles.virtuoso, height: height + groupNames.length - 1 }}
+          overscan={4}
+          increaseViewportBy={2}
+          initialTopMostItemIndex={
+            ~firstSelectedIndex && {
+              index: firstSelectedIndex,
+              align: 'center',
+            }
+          }
+          totalListHeightChanged={setHeight}
+          groupCounts={groupCounts}
+          groupContent={(index) => (
+            <GroupContainer role={'group'}>
+              {groupIcon}
+              <MuiTypography
+                sx={{ ...styles.overflowEllipsis, paddingTop: '2px' }}
+              >
+                {groupNames[index]}
+              </MuiTypography>
+            </GroupContainer>
+          )}
+          itemContent={(index) =>
+            renderOption({ index, option: options[index] })
+          }
+        />
+      );
+    }
+
+    function renderList(options: Array<Value>) {
+      return (
+        <Virtuoso
+          ref={virtuosoRef}
+          style={{ ...styles.virtuoso, height: height + 2 }}
+          data={options}
+          itemContent={(index, option) => renderOption({ option, index })}
+          overscan={4}
+          increaseViewportBy={2}
+          totalListHeightChanged={setHeight}
+        />
+      );
+    }
+
+    function renderOption({
+      index,
+      option,
+    }: UseAutocompleteRenderedOption<Value>) {
+      const { key, ...optionProps } = getOptionProps({
+        option,
+        index,
+      });
+
+      return (
+        <MuiListItemButton
+          {...optionProps}
+          selected={optionProps['aria-selected'] as boolean}
+          disabled={optionProps['aria-disabled'] as boolean}
+          key={getOptionKey?.(option) ?? key}
+        >
+          {renderOptionStartIcon?.(option, { closePopper })}
+          <MuiListItemText
+            primary={optionText.primary(option)}
+            primaryTypographyProps={{ sx: styles.overflowEllipsis }}
+            secondary={optionText.secondary?.(option)}
+            secondaryTypographyProps={{
+              variant: 'caption',
+              sx: styles.overflowEllipsis,
+            }}
+          />
+          {renderOptionEndIcon?.(option, { closePopper })}
+        </MuiListItemButton>
+      );
+    }
+  },
+);
diff --git a/src/Frontend/Components/ClearButton/ClearButton.tsx b/src/Frontend/Components/ClearButton/ClearButton.tsx
new file mode 100644
index 00000000..d7447f2d
--- /dev/null
+++ b/src/Frontend/Components/ClearButton/ClearButton.tsx
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import ClearIcon from '@mui/icons-material/Clear';
+import { styled } from '@mui/material';
+import MuiIconButton, {
+  IconButtonProps as MuiIconButtonProps,
+} from '@mui/material/IconButton';
+
+export const ClearButton = styled(
+  ({ size = 'small', ...props }: MuiIconButtonProps) => (
+    <MuiIconButton aria-label={'clear button'} size={size} {...props}>
+      <ClearIcon fontSize={'small'} />
+    </MuiIconButton>
+  ),
+)({
+  padding: '4px',
+});
diff --git a/src/Frontend/Components/IconButton/IconButton.tsx b/src/Frontend/Components/IconButton/IconButton.tsx
index 9bbb206b..120b055e 100644
--- a/src/Frontend/Components/IconButton/IconButton.tsx
+++ b/src/Frontend/Components/IconButton/IconButton.tsx
@@ -5,54 +5,40 @@
 import { SxProps } from '@mui/material';
 import MuiBox from '@mui/material/Box';
 import MuiButtonBase from '@mui/material/ButtonBase';
-import MuiTooltip from '@mui/material/Tooltip';
+import MuiTooltip, { TooltipProps } from '@mui/material/Tooltip';
 import { ReactElement } from 'react';
 
-import { getSxFromPropsAndClasses } from '../../util/get-sx-from-props-and-classes';
-
-const classes = {
-  hidden: {
-    visibility: 'hidden',
-  },
-};
-
 interface IconButtonProps {
-  tooltipTitle: string;
-  tooltipPlacement: 'left' | 'right';
+  tooltipTitle?: string;
+  tooltipPlacement?: TooltipProps['placement'];
   iconSx?: SxProps;
   containerSx?: SxProps;
-  onClick(): void;
+  onClick?(): void;
   icon: ReactElement;
   disabled?: boolean;
   hidden?: boolean;
 }
 
-export function IconButton(props: IconButtonProps): ReactElement {
+export function IconButton(props: IconButtonProps) {
+  if (props.hidden) {
+    return null;
+  }
+
   return (
     <MuiTooltip
       describeChild={true}
       disableInteractive
-      title={props.hidden ? '' : props.tooltipTitle}
+      title={props.tooltipTitle}
       placement={props.tooltipPlacement}
-      enterDelay={1500}
+      enterDelay={1200}
     >
       <MuiBox component="span" sx={props.containerSx}>
-        {
-          // the container is needed to enable tooltips for disabled buttons
-        }
         <MuiButtonBase
           aria-label={props.tooltipTitle}
-          sx={
-            props.hidden
-              ? getSxFromPropsAndClasses({
-                  styleClass: classes.hidden,
-                  sxProps: props.iconSx,
-                })
-              : props.iconSx
-          }
-          onClick={(event): void => {
+          sx={props.iconSx}
+          onClick={(event) => {
             event.stopPropagation();
-            props.onClick();
+            props.onClick?.();
           }}
           disabled={props.disabled}
         >
diff --git a/src/Frontend/Components/InputElements/AutoComplete.tsx b/src/Frontend/Components/InputElements/AutoComplete.tsx
deleted file mode 100644
index e1770e03..00000000
--- a/src/Frontend/Components/InputElements/AutoComplete.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import MuiAutocomplete from '@mui/material/Autocomplete';
-import MuiInputAdornment from '@mui/material/InputAdornment';
-import MuiTextField from '@mui/material/TextField';
-import { ChangeEvent, ReactElement } from 'react';
-
-import { inputElementClasses, InputElementProps } from './shared';
-
-interface AutoCompleteProps extends InputElementProps {
-  options: Array<string>;
-  endAdornmentText?: string;
-  inputValue?: string;
-  value?: Array<string>; // Use value if multiple is true and inputValue otherwise.
-  showTextBold?: boolean;
-  multiple?: boolean;
-  formatOptionForDisplay?(value: string): string;
-  fullWidth?: boolean;
-}
-
-const classes = {
-  ...inputElementClasses,
-  endAdornment: {
-    paddingRight: '6px',
-    paddingTop: '2px',
-  },
-};
-
-function enterWasPressed(event: KeyboardEvent): boolean {
-  return event.key === 'Enter';
-}
-
-export function AutoComplete(props: AutoCompleteProps): ReactElement {
-  function onChange(
-    event: ChangeEvent<unknown> | KeyboardEvent,
-    value: Array<string> | string,
-  ): void {
-    if (
-      value &&
-      event &&
-      (event.type === 'click' || enterWasPressed(event as KeyboardEvent))
-    ) {
-      props.handleChange?.({
-        target: {
-          value:
-            props.formatOptionForDisplay && typeof value === 'string'
-              ? props.formatOptionForDisplay(value)
-              : value,
-        },
-      } as unknown as ChangeEvent<HTMLInputElement | HTMLTextAreaElement>);
-    }
-  }
-
-  return (
-    <MuiAutocomplete
-      multiple={props.multiple}
-      freeSolo
-      fullWidth={props.fullWidth}
-      sx={{
-        ...(props.isHighlighted
-          ? {
-              '&.MuiAutocomplete-root': classes.defaultHighlightedTextField,
-            }
-          : {}),
-        ...classes.popper,
-      }}
-      options={props.options}
-      disableClearable={true}
-      disabled={!props.isEditable}
-      inputValue={props.inputValue}
-      value={props.value}
-      onChange={onChange}
-      aria-label={'auto complete'}
-      renderInput={(params): ReactElement => {
-        const paramsWithAdornment = props.endAdornmentText
-          ? {
-              ...params,
-              InputProps: {
-                ...params.InputProps,
-                endAdornment: (
-                  <MuiInputAdornment position="end" sx={classes.endAdornment}>
-                    {props.endAdornmentText}
-                  </MuiInputAdornment>
-                ),
-              },
-            }
-          : params;
-
-        return (
-          <MuiTextField
-            {...paramsWithAdornment}
-            label={props.title}
-            sx={{
-              ...classes.textField,
-              ...(props.showTextBold ? classes.textFieldBoldText : {}),
-              ...(props.multiple ? classes.textFieldMultiple : {}),
-            }}
-            variant="outlined"
-            size="small"
-            onChange={props.multiple ? undefined : props.handleChange}
-          />
-        );
-      }}
-    />
-  );
-}
diff --git a/src/Frontend/Components/InputElements/Dropdown.style.ts b/src/Frontend/Components/InputElements/Dropdown.style.ts
new file mode 100644
index 00000000..cddcad55
--- /dev/null
+++ b/src/Frontend/Components/InputElements/Dropdown.style.ts
@@ -0,0 +1,12 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { styled } from '@mui/system';
+
+import { PopupIndicator } from '../PopupIndicator/PopupIndicator';
+
+export const StyledPopupIndicator = styled(PopupIndicator)({
+  top: 'calc(50% - 14px) !important',
+  right: '14px !important',
+});
diff --git a/src/Frontend/Components/InputElements/Dropdown.tsx b/src/Frontend/Components/InputElements/Dropdown.tsx
index b8bf3f45..ad04ce4f 100644
--- a/src/Frontend/Components/InputElements/Dropdown.tsx
+++ b/src/Frontend/Components/InputElements/Dropdown.tsx
@@ -8,6 +8,7 @@ import MuiTextField from '@mui/material/TextField';
 import MuiTypography from '@mui/material/Typography';
 import { ReactElement } from 'react';
 
+import { StyledPopupIndicator } from './Dropdown.style';
 import { inputElementClasses, InputElementProps } from './shared';
 
 interface DropdownProps extends InputElementProps {
@@ -47,6 +48,7 @@ export function Dropdown(props: DropdownProps): ReactElement {
             : {}),
         }}
         select
+        SelectProps={{ IconComponent: StyledPopupIndicator }}
         label={props.title}
         InputProps={{
           inputProps: {
@@ -59,15 +61,13 @@ export function Dropdown(props: DropdownProps): ReactElement {
         variant="outlined"
         size="small"
       >
-        {menuItems.map((menuItem) => {
-          return (
-            <MuiMenuItem value={menuItem.value} key={menuItem.name}>
-              <MuiTypography sx={{ lineHeight: 1.3 }}>
-                {menuItem.name}
-              </MuiTypography>
-            </MuiMenuItem>
-          );
-        })}
+        {menuItems.map((menuItem) => (
+          <MuiMenuItem value={menuItem.value} key={menuItem.name}>
+            <MuiTypography sx={{ lineHeight: 1.3, paddingTop: '2px' }}>
+              {menuItem.name}
+            </MuiTypography>
+          </MuiMenuItem>
+        ))}
       </MuiTextField>
     </MuiBox>
   );
diff --git a/src/Frontend/Components/LocatorPopup/LocatorPopup.tsx b/src/Frontend/Components/LocatorPopup/LocatorPopup.tsx
index fc904d0a..2a39a20a 100644
--- a/src/Frontend/Components/LocatorPopup/LocatorPopup.tsx
+++ b/src/Frontend/Components/LocatorPopup/LocatorPopup.tsx
@@ -2,16 +2,12 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import MuiAutocomplete from '@mui/material/Autocomplete';
 import MuiBox from '@mui/material/Box';
-import MuiTextField from '@mui/material/TextField';
 import MuiTypography from '@mui/material/Typography';
-import { ChangeEvent, ReactElement, useState } from 'react';
+import { compact, sortBy, uniq } from 'lodash';
+import { ChangeEvent, ReactElement, useMemo, useState } from 'react';
 
-import {
-  Attributions,
-  SelectedCriticality,
-} from '../../../shared/shared-types';
+import { SelectedCriticality } from '../../../shared/shared-types';
 import { text } from '../../../shared/text';
 import { ButtonText, CriticalityTypes } from '../../enums/enums';
 import { OpossumColors } from '../../shared-styles';
@@ -27,33 +23,20 @@ import {
   getLocatePopupFilters,
   getShowNoSignalsLocatedMessage,
 } from '../../state/selectors/locate-popup-selectors';
-import { compareAlphabeticalStrings } from '../../util/get-alphabetical-comparer';
+import { Autocomplete } from '../Autocomplete/Autocomplete';
 import { Checkbox } from '../Checkbox/Checkbox';
 import { Dropdown, MenuItem } from '../InputElements/Dropdown';
-import { inputElementClasses } from '../InputElements/shared';
 import { NotificationPopup } from '../NotificationPopup/NotificationPopup';
 import { SearchTextField } from '../SearchTextField/SearchTextField';
 
 const classes = {
-  ...inputElementClasses,
   dropdown: {
     marginTop: '8px',
   },
   autocomplete: { marginTop: '12px' },
-  autocompleteInput: {
-    '& .MuiInputLabel-root': {
-      top: '2px',
-    },
-    '& .MuiInputBase-input': {
-      height: '24px',
-    },
-    '& .MuiInputBase-root': {
-      borderRadius: '0px',
-    },
-  },
   noSignalsMessage: { color: OpossumColors.red, marginTop: '8px' },
   dialogContent: {
-    width: '25vw',
+    width: '400px',
   },
   search: {
     padding: '8px',
@@ -78,18 +61,6 @@ const criticalityMenuItems: Array<MenuItem> = [
   },
 ];
 
-// exported for testing
-export function getLicenseNames(attributions: Attributions): Array<string> {
-  const licenseNames: Set<string> = new Set();
-  for (const attribution of Object.values(attributions)) {
-    const licenseName = attribution.licenseName;
-    if (licenseName) {
-      licenseNames.add(licenseName);
-    }
-  }
-  return Array.from(licenseNames.values());
-}
-
 export function LocatorPopup(): ReactElement {
   const dispatch = useAppDispatch();
   const externalAttributions = useAppSelector(getExternalAttributions);
@@ -103,7 +74,20 @@ export function LocatorPopup(): ReactElement {
     getShowNoSignalsLocatedMessage,
   );
 
-  const licenseNameOptions = getLicenseNames(externalAttributions);
+  const licenseNameOptions = useMemo(
+    () =>
+      sortBy(
+        compact(
+          uniq(
+            Object.values(externalAttributions).map(
+              ({ licenseName }) => licenseName,
+            ),
+          ),
+        ),
+        (licenseName) => licenseName.toLowerCase(),
+      ),
+    [externalAttributions],
+  );
 
   const [searchedLicenses, setSearchedLicenses] = useState(selectedLicenses);
 
@@ -176,28 +160,18 @@ export function LocatorPopup(): ReactElement {
         menuItems={criticalityMenuItems}
         handleChange={updateCriticalityDropdownChoice}
       />
-      <MuiAutocomplete
-        title={'License'}
+      <Autocomplete
         sx={classes.autocomplete}
+        options={licenseNameOptions}
+        optionText={{ primary: (option) => option }}
         multiple
-        options={licenseNameOptions.sort((a, b) =>
-          compareAlphabeticalStrings(a, b),
-        )}
-        size={'small'}
-        filterSelectedOptions
-        aria-label={'auto complete'}
-        getOptionLabel={(option): string => option}
+        title={text.locatorPopup.license}
         value={[...searchedLicenses]}
-        onChange={(_event, searchedLicenses): void => {
-          setSearchedLicenses(new Set(searchedLicenses));
+        onChange={(_, value) => {
+          setSearchedLicenses(new Set(value));
         }}
-        renderInput={(params): ReactElement => (
-          <MuiTextField
-            {...params}
-            label="License"
-            sx={classes.autocompleteInput}
-          />
-        )}
+        filterSelectedOptions
+        inputProps={{ 'aria-label': 'auto complete' }}
       />
       {showNoSignalsLocatedMessage ? (
         <MuiTypography variant={'subtitle2'} sx={classes.noSignalsMessage}>
diff --git a/src/Frontend/Components/PopupIndicator/PopupIndicator.tsx b/src/Frontend/Components/PopupIndicator/PopupIndicator.tsx
new file mode 100644
index 00000000..0804e062
--- /dev/null
+++ b/src/Frontend/Components/PopupIndicator/PopupIndicator.tsx
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
+import { styled } from '@mui/material';
+import MuiIconButton, {
+  IconButtonProps as MuiIconButtonProps,
+} from '@mui/material/IconButton';
+
+export const PopupIndicator = styled(
+  ({ size = 'small', ...props }: MuiIconButtonProps) => (
+    <MuiIconButton aria-label={'popup indicator'} size={size} {...props}>
+      <ArrowDropDownIcon />
+    </MuiIconButton>
+  ),
+  { shouldForwardProp: (prop) => prop !== 'popupOpen' },
+)<{ popupOpen?: boolean }>(({ popupOpen }) => ({
+  padding: '2px',
+  transform: popupOpen ? 'rotate(180deg)' : 'rotate(0deg)',
+  transition: 'all 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
+}));
diff --git a/src/Frontend/Components/ProgressBar/FolderProgressBar.tsx b/src/Frontend/Components/ProgressBar/FolderProgressBar.tsx
index 135c698d..6bff2ab8 100644
--- a/src/Frontend/Components/ProgressBar/FolderProgressBar.tsx
+++ b/src/Frontend/Components/ProgressBar/FolderProgressBar.tsx
@@ -3,26 +3,9 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 import MuiSkeleton from '@mui/material/Skeleton';
-import { ReactElement, useContext, useEffect, useMemo, useState } from 'react';
+import { ReactElement } from 'react';
 
-import { getUpdatedProgressBarData } from '../../state/helpers/progress-bar-data-helpers';
-import { useAppSelector } from '../../state/hooks';
-import {
-  getAttributionBreakpoints,
-  getExternalAttributions,
-  getFilesWithChildren,
-  getManualAttributions,
-  getResources,
-  getResourcesToExternalAttributions,
-  getResourcesToManualAttributions,
-} from '../../state/selectors/all-views-resource-selectors';
-import { getResolvedExternalAttributions } from '../../state/selectors/audit-view-resource-selectors';
-import {
-  ProgressBarData,
-  ProgressBarDataAndResourceId,
-  ProgressBarWorkerArgs,
-} from '../../types/types';
-import { ProgressBarWorkersContext } from '../WorkersContextProvider/WorkersContextProvider';
+import { useFolderProgressData } from '../../web-workers/use-signals-worker';
 import { ProgressBar } from './ProgressBar';
 
 const classes = {
@@ -31,157 +14,16 @@ const classes = {
   },
 };
 
-const EMPTY_FOLDER_PROGRESS_BAR_AND_RESOURCE_ID = {
-  progressBarData: null,
-  resourceId: '',
-};
-
-interface FolderProgressBarProps {
-  resourceId: string;
-}
-
-export function FolderProgressBar(props: FolderProgressBarProps): ReactElement {
-  const resourceId = props.resourceId;
-
-  const resources = useAppSelector(getResources);
-  const manualAttributions = useAppSelector(getManualAttributions);
-  const externalAttributions = useAppSelector(getExternalAttributions);
-  const resourcesToManualAttributions = useAppSelector(
-    getResourcesToManualAttributions,
-  );
-  const resourcesToExternalAttributions = useAppSelector(
-    getResourcesToExternalAttributions,
-  );
-  const resolvedExternalAttributions = useAppSelector(
-    getResolvedExternalAttributions,
-  );
-  const attributionBreakpoints = useAppSelector(getAttributionBreakpoints);
-  const filesWithChildren = useAppSelector(getFilesWithChildren);
-
-  const [
-    folderProgressBarDataAndResourceId,
-    setFolderProgressBarDataAndResourceId,
-  ] = useState<ProgressBarDataAndResourceId>(
-    EMPTY_FOLDER_PROGRESS_BAR_AND_RESOURCE_ID,
-  );
-
-  const folderProgressBarWorker = useContext(
-    ProgressBarWorkersContext,
-  ).FolderProgressBarWorker;
-
-  const folderProgressBarWorkerArgs = useMemo(
-    () => ({
-      manualAttributions,
-      resourceId,
-      resourcesToManualAttributions,
-    }),
-    [manualAttributions, resourceId, resourcesToManualAttributions],
-  );
-
-  const folderProgressBarSyncFallbackArgs = useMemo(
-    () => ({
-      resources,
-      resourceId,
-      manualAttributions,
-      externalAttributions,
-      resourcesToManualAttributions,
-      resourcesToExternalAttributions,
-      resolvedExternalAttributions,
-      attributionBreakpoints,
-      filesWithChildren,
-    }),
-    [
-      resources,
-      resourceId,
-      manualAttributions,
-      externalAttributions,
-      resourcesToManualAttributions,
-      resourcesToExternalAttributions,
-      resolvedExternalAttributions,
-      attributionBreakpoints,
-      filesWithChildren,
-    ],
-  );
+export function FolderProgressBar(): ReactElement {
+  const progressData = useFolderProgressData();
 
-  useEffect(() => {
-    void loadProgressBarData(
-      folderProgressBarWorker,
-      folderProgressBarWorkerArgs,
-      setFolderProgressBarDataAndResourceId,
-      folderProgressBarSyncFallbackArgs,
-    );
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [folderProgressBarWorker, folderProgressBarWorkerArgs]);
-
-  let displayedProgressBarData: ProgressBarData | null;
-  if (
-    folderProgressBarWorkerArgs.resourceId ===
-    folderProgressBarDataAndResourceId.resourceId
-  ) {
-    displayedProgressBarData =
-      folderProgressBarDataAndResourceId.progressBarData;
-  } else {
-    displayedProgressBarData = null;
-  }
-
-  return displayedProgressBarData ? (
+  return progressData ? (
     <ProgressBar
       sx={classes.root}
       progressBarType={'FolderProgressBar'}
-      progressBarData={displayedProgressBarData}
+      progressBarData={progressData}
     />
   ) : (
     <MuiSkeleton height={20} />
   );
 }
-
-// eslint-disable-next-line @typescript-eslint/require-await
-async function loadProgressBarData(
-  worker: Worker,
-  workerArgs: Partial<ProgressBarWorkerArgs>,
-  setFolderProgressBarDataAndResourceId: (
-    folderProgressBarDataAndResourceId: ProgressBarDataAndResourceId,
-  ) => void,
-  syncFallbackArgs: ProgressBarWorkerArgs,
-): Promise<void> {
-  setFolderProgressBarDataAndResourceId(
-    EMPTY_FOLDER_PROGRESS_BAR_AND_RESOURCE_ID,
-  );
-
-  try {
-    worker.postMessage(workerArgs);
-
-    worker.onmessage = ({ data: { output } }): void => {
-      if (!output) {
-        logErrorAndComputeInMainProcess(
-          Error('Web Worker execution error.'),
-          setFolderProgressBarDataAndResourceId,
-          syncFallbackArgs,
-        );
-      } else {
-        setFolderProgressBarDataAndResourceId(output);
-      }
-    };
-  } catch (error) {
-    logErrorAndComputeInMainProcess(
-      error,
-      setFolderProgressBarDataAndResourceId,
-      syncFallbackArgs,
-    );
-  }
-}
-
-function logErrorAndComputeInMainProcess(
-  error: unknown,
-  setFolderProgressBarDataAndResourceId: (
-    folderProgressBarDataAndResourceId: ProgressBarDataAndResourceId,
-  ) => void,
-  syncFallbackArgs: ProgressBarWorkerArgs,
-): void {
-  console.info('Error in rendering folder progress bar: ', error);
-  const progressBarData = getUpdatedProgressBarData(syncFallbackArgs);
-  setFolderProgressBarDataAndResourceId({
-    progressBarData,
-    resourceId: syncFallbackArgs.resourceId,
-  });
-}
diff --git a/src/Frontend/Components/ProgressBar/TopProgressBar.tsx b/src/Frontend/Components/ProgressBar/TopProgressBar.tsx
index 8526e94f..97b212ec 100644
--- a/src/Frontend/Components/ProgressBar/TopProgressBar.tsx
+++ b/src/Frontend/Components/ProgressBar/TopProgressBar.tsx
@@ -3,23 +3,10 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 import MuiBox from '@mui/material/Box';
-import { ReactElement, useContext, useEffect, useMemo, useState } from 'react';
+import { ReactElement, useState } from 'react';
 
-import { getUpdatedProgressBarData } from '../../state/helpers/progress-bar-data-helpers';
-import { useAppSelector } from '../../state/hooks';
-import {
-  getAttributionBreakpoints,
-  getExternalAttributions,
-  getFilesWithChildren,
-  getManualAttributions,
-  getResources,
-  getResourcesToExternalAttributions,
-  getResourcesToManualAttributions,
-} from '../../state/selectors/all-views-resource-selectors';
-import { getResolvedExternalAttributions } from '../../state/selectors/audit-view-resource-selectors';
-import { ProgressBarData, ProgressBarWorkerArgs } from '../../types/types';
+import { useOverallProgressData } from '../../web-workers/use-signals-worker';
 import { SwitchWithTooltip } from '../SwitchWithTooltip/SwitchWithTooltip';
-import { ProgressBarWorkersContext } from '../WorkersContextProvider/WorkersContextProvider';
 import { ProgressBar } from './ProgressBar';
 
 const classes = {
@@ -35,68 +22,6 @@ const classes = {
 };
 
 export function TopProgressBar(): ReactElement {
-  const resources = useAppSelector(getResources);
-  const manualAttributions = useAppSelector(getManualAttributions);
-  const externalAttributions = useAppSelector(getExternalAttributions);
-  const resourcesToManualAttributions = useAppSelector(
-    getResourcesToManualAttributions,
-  );
-  const resourcesToExternalAttributions = useAppSelector(
-    getResourcesToExternalAttributions,
-  );
-  const resolvedExternalAttributions = useAppSelector(
-    getResolvedExternalAttributions,
-  );
-  const attributionBreakpoints = useAppSelector(getAttributionBreakpoints);
-  const filesWithChildren = useAppSelector(getFilesWithChildren);
-
-  const [topProgressBarData, setTopProgressBarData] =
-    useState<ProgressBarData | null>(null);
-
-  const topProgressBarWorker = useContext(
-    ProgressBarWorkersContext,
-  ).TopProgressBarWorker;
-
-  const topProgressBarWorkerArgs = useMemo(
-    () => ({
-      resourceId: '/',
-      manualAttributions,
-      externalAttributions,
-      resourcesToManualAttributions,
-      resolvedExternalAttributions,
-    }),
-    [
-      manualAttributions,
-      externalAttributions,
-      resourcesToManualAttributions,
-      resolvedExternalAttributions,
-    ],
-  );
-
-  const topProgressBarSyncFallbackArgs = useMemo(
-    () => ({
-      resources,
-      resourceId: '/',
-      manualAttributions,
-      externalAttributions,
-      resourcesToManualAttributions,
-      resourcesToExternalAttributions,
-      resolvedExternalAttributions,
-      attributionBreakpoints,
-      filesWithChildren,
-    }),
-    [
-      resources,
-      manualAttributions,
-      externalAttributions,
-      resourcesToManualAttributions,
-      resourcesToExternalAttributions,
-      resolvedExternalAttributions,
-      attributionBreakpoints,
-      filesWithChildren,
-    ],
-  );
-
   const [progressBarCriticalityState, setProgressBarCriticalityState] =
     useState<boolean>(false);
 
@@ -107,21 +32,14 @@ export function TopProgressBar(): ReactElement {
     ? 'Critical signals progress bar selected'
     : 'Progress bar selected';
 
-  useEffect(() => {
-    void loadProgressBarData(
-      topProgressBarWorker,
-      topProgressBarWorkerArgs,
-      setTopProgressBarData,
-      topProgressBarSyncFallbackArgs,
-    );
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [topProgressBarWorker, topProgressBarWorkerArgs]);
-  return topProgressBarData ? (
+  const progressData = useOverallProgressData();
+
+  return progressData ? (
     <MuiBox sx={classes.root}>
       <ProgressBar
         sx={classes.root}
         progressBarType={'TopProgressBar'}
-        progressBarData={topProgressBarData}
+        progressBarData={progressData}
         progressBarCriticalityState={progressBarCriticalityState}
       />
       <SwitchWithTooltip
@@ -129,51 +47,9 @@ export function TopProgressBar(): ReactElement {
         switchToolTipText={switchToolTipText}
         isChecked={progressBarCriticalityState}
         handleSwitchClick={handleSwitchClick}
-        ariaLabel="CriticalityStateSwitch"
       />
     </MuiBox>
   ) : (
-    <MuiBox sx={classes.root} />
+    <MuiBox aria-label={'TopProgressBar'} sx={classes.root} />
   );
-
-  // eslint-disable-next-line @typescript-eslint/require-await
-  async function loadProgressBarData(
-    worker: Worker,
-    workerArgs: Partial<ProgressBarWorkerArgs>,
-    setTopProgressBarData: (progressBarData: ProgressBarData | null) => void,
-    syncFallbackArgs: ProgressBarWorkerArgs,
-  ): Promise<void> {
-    try {
-      worker.postMessage(workerArgs);
-
-      worker.onmessage = ({ data: { output } }): void => {
-        if (!output) {
-          logErrorAndComputeInMainProcess(
-            Error('Web Worker execution error.'),
-            setTopProgressBarData,
-            syncFallbackArgs,
-          );
-        } else {
-          setTopProgressBarData(output.progressBarData);
-        }
-      };
-    } catch (error) {
-      logErrorAndComputeInMainProcess(
-        error,
-        setTopProgressBarData,
-        syncFallbackArgs,
-      );
-    }
-  }
-
-  function logErrorAndComputeInMainProcess(
-    error: unknown,
-    setTopProgressBarData: (topProgressBarData: ProgressBarData | null) => void,
-    syncFallbackArgs: ProgressBarWorkerArgs,
-  ): void {
-    console.info('Error in rendering top progress bar: ', error);
-    const progressBarData = getUpdatedProgressBarData(syncFallbackArgs);
-
-    setTopProgressBarData(progressBarData);
-  }
 }
diff --git a/src/Frontend/Components/ResourceDetailsViewer/ResourceDetailsViewer.tsx b/src/Frontend/Components/ResourceDetailsViewer/ResourceDetailsViewer.tsx
index 5ce092cd..2df81334 100644
--- a/src/Frontend/Components/ResourceDetailsViewer/ResourceDetailsViewer.tsx
+++ b/src/Frontend/Components/ResourceDetailsViewer/ResourceDetailsViewer.tsx
@@ -125,9 +125,7 @@ export function ResourceDetailsViewer(): ReactElement | null {
               onOverrideParentClick={onOverrideParentClick}
             />
           )}
-          {showProgressBar && (
-            <FolderProgressBar resourceId={selectedResourceId} />
-          )}
+          {showProgressBar && <FolderProgressBar />}
           <MuiBox sx={classes.tabsDiv}>
             <ResourceDetailsTabs
               isGlobalTabEnabled={!showParentAttributions}
diff --git a/src/Frontend/Components/SwitchWithTooltip/SwitchWithTooltip.tsx b/src/Frontend/Components/SwitchWithTooltip/SwitchWithTooltip.tsx
index e22b1008..a3ac3721 100644
--- a/src/Frontend/Components/SwitchWithTooltip/SwitchWithTooltip.tsx
+++ b/src/Frontend/Components/SwitchWithTooltip/SwitchWithTooltip.tsx
@@ -12,14 +12,12 @@ interface SwitchWithTooltipProps {
   switchToolTipText: string;
   isChecked: boolean;
   handleSwitchClick: () => void;
-  ariaLabel: string;
 }
 
 export function SwitchWithTooltip(props: SwitchWithTooltipProps): ReactElement {
   return (
     <MuiTooltip title={props.switchToolTipText}>
       <MuiSwitch
-        aria-label={props.ariaLabel}
         checked={props.isChecked}
         onChange={props.handleSwitchClick}
         size="small"
diff --git a/src/Frontend/Components/WorkersContextProvider/WorkersContextProvider.tsx b/src/Frontend/Components/WorkersContextProvider/WorkersContextProvider.tsx
deleted file mode 100644
index bbcfd376..00000000
--- a/src/Frontend/Components/WorkersContextProvider/WorkersContextProvider.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { createContext, FC, ReactNode, useMemo } from 'react';
-
-import { useAppSelector } from '../../state/hooks';
-import {
-  getAttributionBreakpoints,
-  getExternalAttributions,
-  getExternalAttributionsToHashes,
-  getExternalData,
-  getFilesWithChildren,
-  getManualAttributions,
-  getManualData,
-  getResources,
-  getResourcesToExternalAttributions,
-  getResourcesToManualAttributions,
-} from '../../state/selectors/all-views-resource-selectors';
-import { getResolvedExternalAttributions } from '../../state/selectors/audit-view-resource-selectors';
-import { PanelAttributionData } from '../../util/get-contained-packages';
-import { getNewAccordionWorkers } from '../../web-workers/get-new-accordion-workers';
-import { getNewProgressBarWorkers } from '../../web-workers/get-new-progress-bar-workers';
-
-const resourceDetailsTabsWorkers = getNewAccordionWorkers();
-
-export const AccordionWorkersContext = createContext(
-  resourceDetailsTabsWorkers,
-);
-
-export const AccordionWorkersContextProvider: FC<{
-  children: ReactNode | null;
-}> = ({ children }) => {
-  const externalAttributionData = useAppSelector(getExternalData);
-  const attributionsToHashes = useAppSelector(getExternalAttributionsToHashes);
-  const manualAttributionData = useAppSelector(getManualData);
-
-  const externalData: PanelAttributionData = useMemo(
-    () => ({
-      attributions: externalAttributionData.attributions,
-      resourcesToAttributions: externalAttributionData.resourcesToAttributions,
-      resourcesWithAttributedChildren:
-        externalAttributionData.resourcesWithAttributedChildren,
-    }),
-    [externalAttributionData],
-  );
-
-  const manualData: PanelAttributionData = useMemo(
-    () => ({
-      attributions: manualAttributionData.attributions,
-      resourcesToAttributions: manualAttributionData.resourcesToAttributions,
-      resourcesWithAttributedChildren:
-        manualAttributionData.resourcesWithAttributedChildren,
-    }),
-    [manualAttributionData],
-  );
-
-  useMemo(() => {
-    try {
-      // remove data from previous file or empty data from app just opened
-      resourceDetailsTabsWorkers.containedExternalAttributionsAccordionWorker.postMessage(
-        { externalData: null },
-      );
-      resourceDetailsTabsWorkers.containedExternalAttributionsAccordionWorker.postMessage(
-        { externalData, attributionsToHashes },
-      );
-    } catch (error) {
-      console.info('Web worker error in workers context provider: ', error);
-    }
-  }, [externalData, attributionsToHashes]);
-
-  useMemo(() => {
-    try {
-      // remove data from previous file or empty data from app just opened
-      resourceDetailsTabsWorkers.containedManualAttributionsAccordionWorker.postMessage(
-        { manualData: null },
-      );
-      resourceDetailsTabsWorkers.containedManualAttributionsAccordionWorker.postMessage(
-        { manualData },
-      );
-    } catch (error) {
-      console.info('Web worker error in workers context provider: ', error);
-    }
-  }, [manualData]);
-
-  return (
-    <AccordionWorkersContext.Provider value={resourceDetailsTabsWorkers}>
-      {children}
-    </AccordionWorkersContext.Provider>
-  );
-};
-
-const progressBarWorkers = getNewProgressBarWorkers();
-export const ProgressBarWorkersContext = createContext(progressBarWorkers);
-
-export const ProgressBarWorkersContextProvider: FC<{
-  children: ReactNode | null;
-}> = ({ children }) => {
-  const resources = useAppSelector(getResources);
-  const manualAttributions = useAppSelector(getManualAttributions);
-  const externalAttributions = useAppSelector(getExternalAttributions);
-  const resourcesToManualAttributions = useAppSelector(
-    getResourcesToManualAttributions,
-  );
-  const resourcesToExternalAttributions = useAppSelector(
-    getResourcesToExternalAttributions,
-  );
-  const resolvedExternalAttributions = useAppSelector(
-    getResolvedExternalAttributions,
-  );
-  const attributionBreakpoints = useAppSelector(getAttributionBreakpoints);
-  const filesWithChildren = useAppSelector(getFilesWithChildren);
-
-  useMemo(() => {
-    try {
-      progressBarWorkers.TopProgressBarWorker.postMessage({
-        isCacheInitializationMessage: true,
-        resources: null,
-        externalAttributions: null,
-        resourcesToExternalAttributions: null,
-        attributionBreakpoints: null,
-        filesWithChildren: null,
-      });
-      Object.values(progressBarWorkers).forEach((worker) => {
-        worker.postMessage({
-          isCacheInitializationMessage: true,
-          resources,
-          externalAttributions,
-          resourcesToExternalAttributions,
-          attributionBreakpoints,
-          filesWithChildren,
-        });
-      });
-    } catch (error) {
-      console.info('Web worker error in workers context provider: ', error);
-    }
-  }, [
-    resources,
-    externalAttributions,
-    resourcesToExternalAttributions,
-    attributionBreakpoints,
-    filesWithChildren,
-  ]);
-
-  useMemo(() => {
-    try {
-      progressBarWorkers.TopProgressBarWorker.postMessage({
-        manualAttributions: null,
-        resourcesToManualAttributions: null,
-        resolvedExternalAttributions: null,
-      });
-      Object.values(progressBarWorkers).forEach((worker) => {
-        worker.postMessage({
-          manualAttributions,
-          resourcesToManualAttributions,
-          resolvedExternalAttributions,
-        });
-      });
-    } catch (error) {
-      console.info('Web worker error in workers context provider: ', error);
-    }
-  }, [
-    manualAttributions,
-    resourcesToManualAttributions,
-    resolvedExternalAttributions,
-  ]);
-
-  return (
-    <ProgressBarWorkersContext.Provider value={progressBarWorkers}>
-      {children}
-    </ProgressBarWorkersContext.Provider>
-  );
-};
diff --git a/src/Frontend/state/actions/resource-actions/save-actions.ts b/src/Frontend/state/actions/resource-actions/save-actions.ts
index 2de8ffb2..70d52304 100644
--- a/src/Frontend/state/actions/resource-actions/save-actions.ts
+++ b/src/Frontend/state/actions/resource-actions/save-actions.ts
@@ -108,6 +108,7 @@ export function savePackageInfo(
   attributionId: string | null,
   packageInfo: PackageInfo,
   applyContextMenuActionOnDifferentAttribution?: boolean,
+  noReplace?: boolean,
 ): AppThunkAction {
   return (dispatch: AppThunkDispatch, getState: () => State): void => {
     const strippedPackageInfo = getStrippedPackageInfo(packageInfo);
@@ -124,6 +125,7 @@ export function savePackageInfo(
       attributionId,
       resourceId,
       strippedPackageInfo,
+      noReplace,
       state,
     );
 
@@ -183,6 +185,7 @@ function getSavePackageInfoOperation(
   attributionId: string | null,
   resourceId: string | null,
   strippedPackageInfo: PackageInfo,
+  noReplace: boolean | undefined,
   state: State,
 ): SavePackageInfoOperation {
   if (packageInfoHasNoSignificantFields(strippedPackageInfo)) {
@@ -190,6 +193,7 @@ function getSavePackageInfoOperation(
   }
 
   if (
+    !noReplace &&
     attributionForTemporaryDisplayPackageInfoExists(strippedPackageInfo, state)
   ) {
     return attributionId
diff --git a/src/Frontend/state/actions/variables-actions/variables-actions.ts b/src/Frontend/state/actions/variables-actions/variables-actions.ts
index f1e1ae52..8cc485ac 100644
--- a/src/Frontend/state/actions/variables-actions/variables-actions.ts
+++ b/src/Frontend/state/actions/variables-actions/variables-actions.ts
@@ -6,13 +6,13 @@ import { SET_VARIABLE } from './types';
 
 export type VariablesAction = SetVariable;
 
-export interface SetVariable {
+export interface SetVariable<T = unknown> {
   type: SET_VARIABLE;
   name: string;
-  value: unknown;
+  value: T;
 }
 
-export function setVariable(name: string, value: unknown): SetVariable {
+export function setVariable<T>(name: string, value: T): SetVariable<T> {
   return {
     type: SET_VARIABLE,
     name,
diff --git a/src/Frontend/state/actions/view-actions/view-actions.ts b/src/Frontend/state/actions/view-actions/view-actions.ts
index e78f6b95..99c00ba5 100644
--- a/src/Frontend/state/actions/view-actions/view-actions.ts
+++ b/src/Frontend/state/actions/view-actions/view-actions.ts
@@ -61,7 +61,7 @@ export function navigateToView(view: View): AppThunkAction {
   };
 }
 
-function setView(view: View): SetView {
+export function setView(view: View): SetView {
   return {
     type: ACTION_SET_VIEW,
     payload: view,
diff --git a/src/Frontend/state/helpers/progress-bar-data-helpers.ts b/src/Frontend/state/helpers/progress-bar-data-helpers.ts
index 0a733feb..3fe833d1 100644
--- a/src/Frontend/state/helpers/progress-bar-data-helpers.ts
+++ b/src/Frontend/state/helpers/progress-bar-data-helpers.ts
@@ -8,11 +8,7 @@ import {
   Resources,
   ResourcesToAttributions,
 } from '../../../shared/shared-types';
-import {
-  PathPredicate,
-  ProgressBarData,
-  ProgressBarWorkerArgs,
-} from '../../types/types';
+import { PathPredicate, ProgressBarData } from '../../types/types';
 import { canResourceHaveChildren } from '../../util/can-resource-have-children';
 import { getAttributionBreakpointCheck } from '../../util/is-attribution-breakpoint';
 import { getFileWithChildrenCheck } from '../../util/is-file-with-children';
@@ -50,7 +46,7 @@ function updateProgressBarDataForResources(
   hasParentExternalAttribution = false,
 ): void {
   for (const resourceName of Object.keys(resources)) {
-    const resource: Resources | 1 = resources[resourceName];
+    const resource = resources[resourceName] as Resources | 1 | undefined;
     const path = `${parentPath}${resourceName}${
       canResourceHaveChildren(resource) ? '/' : ''
     }`;
@@ -196,9 +192,17 @@ export function resourceHasOnlyPreSelectedAttributions(
   );
 }
 
-export function getUpdatedProgressBarData(
-  args: ProgressBarWorkerArgs,
-): ProgressBarData {
+export function getUpdatedProgressBarData(args: {
+  resources: Resources | null;
+  resourceId: string;
+  manualAttributions: Attributions;
+  externalAttributions: Attributions;
+  resourcesToManualAttributions: ResourcesToAttributions;
+  resourcesToExternalAttributions: ResourcesToAttributions;
+  resolvedExternalAttributions: Set<string>;
+  attributionBreakpoints: Set<string>;
+  filesWithChildren: Set<string>;
+}): ProgressBarData {
   const isAttributionBreakpoint = getAttributionBreakpointCheck(
     args.attributionBreakpoints,
   );
diff --git a/src/Frontend/state/hooks.ts b/src/Frontend/state/hooks.ts
index e9e75c26..6816a4c4 100644
--- a/src/Frontend/state/hooks.ts
+++ b/src/Frontend/state/hooks.ts
@@ -5,6 +5,7 @@
 import { memoize } from 'proxy-memoize';
 import { useCallback } from 'react';
 import { useDispatch, useSelector, useStore } from 'react-redux';
+import { AnyAction, Store } from 'redux';
 
 import { State } from '../types/types';
 import { AppThunkDispatch } from './types';
@@ -18,4 +19,4 @@ export const useAppSelector = <T>(
   // eslint-disable-next-line react-hooks/exhaustive-deps
   useSelector<State, T>(useCallback(memoize(fn), deps));
 
-export const useAppStore = useStore<State>;
+export const useAppStore: () => Store<State, AnyAction> = useStore;
diff --git a/src/Frontend/types/types.ts b/src/Frontend/types/types.ts
index 8952788d..d27c9ebc 100644
--- a/src/Frontend/types/types.ts
+++ b/src/Frontend/types/types.ts
@@ -4,11 +4,8 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 import {
-  Attributions,
   Criticality,
   DisplayPackageInfo,
-  Resources,
-  ResourcesToAttributions,
   SelectedCriticality,
 } from '../../shared/shared-types';
 import { PackagePanelTitle, PopupType } from '../enums/enums';
@@ -82,7 +79,6 @@ export interface PopupInfo {
 }
 
 export interface PanelData {
-  title: PackagePanelTitle;
   sortedPackageCardIds: Array<string>;
   displayPackageInfosWithCount: DisplayPackageInfosWithCount;
 }
@@ -98,18 +94,6 @@ export interface ProgressBarDataAndResourceId {
   resourceId: string;
 }
 
-export interface ProgressBarWorkerArgs {
-  resources: Resources | null;
-  resourceId: string;
-  manualAttributions: Attributions;
-  externalAttributions: Attributions;
-  resourcesToManualAttributions: ResourcesToAttributions;
-  resourcesToExternalAttributions: ResourcesToAttributions;
-  resolvedExternalAttributions: Set<string>;
-  attributionBreakpoints: Set<string>;
-  filesWithChildren: Set<string>;
-}
-
 export interface PieChartData {
   name: string;
   count: number;
diff --git a/src/Frontend/util/can-resource-have-children.ts b/src/Frontend/util/can-resource-have-children.ts
index 6aa66814..d5fe7d34 100644
--- a/src/Frontend/util/can-resource-have-children.ts
+++ b/src/Frontend/util/can-resource-have-children.ts
@@ -5,9 +5,9 @@
 import { Resources } from '../../shared/shared-types';
 
 export function canResourceHaveChildren(
-  resource: Resources | 1,
+  resource: Resources | 1 | undefined,
 ): resource is Resources {
-  return resource !== 1;
+  return resource !== 1 && resource !== undefined;
 }
 
 export function isIdOfResourceWithChildren(resourceId: string): boolean {
diff --git a/src/Frontend/util/maybe-pluralize.ts b/src/Frontend/util/maybe-pluralize.ts
new file mode 100644
index 00000000..40267b8d
--- /dev/null
+++ b/src/Frontend/util/maybe-pluralize.ts
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+
+/**
+ * Adds 's' depending on value, for example user vs users
+ * @param count
+ * @param noun
+ * @param pluralNoun
+ * @example maybePluralize(0, 'User') returns '0 Users'
+ */
+export function maybePluralize(
+  count: number,
+  noun: string,
+  { pluralNoun }: Partial<{ pluralNoun: string }> = {},
+) {
+  return `${count} ${count === 1 ? noun : pluralNoun || `${noun}s`}`;
+}
diff --git a/src/Frontend/util/prettify-source.ts b/src/Frontend/util/prettify-source.ts
index 0c773651..2c6fc7da 100644
--- a/src/Frontend/util/prettify-source.ts
+++ b/src/Frontend/util/prettify-source.ts
@@ -9,7 +9,7 @@ export function prettifySource(
   attributionSources: ExternalAttributionSources,
 ): string {
   if (!source) {
-    return 'none';
+    return '';
   }
   return attributionSources[source] ? attributionSources[source].name : source;
 }
diff --git a/src/Frontend/util/use-variable.ts b/src/Frontend/util/use-variable.ts
index b526f08b..ac2f5a0b 100644
--- a/src/Frontend/util/use-variable.ts
+++ b/src/Frontend/util/use-variable.ts
@@ -2,10 +2,12 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
+import { isFunction } from 'lodash';
 import { useCallback } from 'react';
 import { useDispatch, useSelector } from 'react-redux';
 
 import { setVariable } from '../state/actions/variables-actions/variables-actions';
+import { useAppStore } from '../state/hooks';
 import { State } from '../types/types';
 
 /**
@@ -17,22 +19,34 @@ import { State } from '../types/types';
 export function useVariable<T>(
   name: string,
   initialValue: T,
-): [T, (newValue: T) => void] {
+): [T, (newValue: T | ((prev: T) => T)) => void] {
   const dispatch = useDispatch();
-
-  return [
-    useSelector<State, T>((state) => {
+  const store = useAppStore();
+  const getValue = useCallback(
+    (state: State) => {
       if (name in state.variablesState) {
         return state.variablesState[name] as T;
       }
 
       return initialValue;
-    }),
+    },
+    [initialValue, name],
+  );
+
+  return [
+    useSelector<State, T>(getValue),
     useCallback(
-      (value: T) => {
-        dispatch(setVariable(name, value));
+      (newValue: T | ((prev: T) => T)) => {
+        dispatch(
+          setVariable(
+            name,
+            isFunction(newValue)
+              ? newValue(getValue(store.getState()))
+              : newValue,
+          ),
+        );
       },
-      [dispatch, name],
+      [dispatch, getValue, name, store],
     ),
   ];
 }
diff --git a/src/Frontend/web-workers/__mocks__/get-new-accordion-workers.ts b/src/Frontend/web-workers/__mocks__/get-new-accordion-workers.ts
deleted file mode 100644
index 51c6d088..00000000
--- a/src/Frontend/web-workers/__mocks__/get-new-accordion-workers.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { ResourceDetailsTabsWorkers } from '../get-new-accordion-workers';
-
-export function getNewAccordionWorkers(): ResourceDetailsTabsWorkers {
-  return {
-    containedExternalAttributionsAccordionWorker:
-      getNewContainedExternalAttributionsAccordionWorker(),
-    containedManualAttributionsAccordionWorker:
-      getNewContainedManualAttributionsAccordionWorker(),
-  };
-}
-
-function getNewContainedExternalAttributionsAccordionWorker(): Worker {
-  return {
-    postMessage: () => {
-      throw new Error(
-        'JEST-MOCK-GET-NEW-CONTAINED-EXTERNAL-ATTRIBUTIONS-ACCORDION-WORKER',
-      );
-    },
-  } as unknown as Worker;
-}
-
-function getNewContainedManualAttributionsAccordionWorker(): Worker {
-  return {
-    postMessage: () => {
-      throw new Error(
-        'JEST-MOCK-GET-NEW-CONTAINED-MANUAL-ATTRIBUTIONS-ACCORDION-WORKER',
-      );
-    },
-  } as unknown as Worker;
-}
diff --git a/src/Frontend/web-workers/__mocks__/get-new-progress-bar-workers.ts b/src/Frontend/web-workers/__mocks__/get-new-progress-bar-workers.ts
deleted file mode 100644
index 77f1dfc1..00000000
--- a/src/Frontend/web-workers/__mocks__/get-new-progress-bar-workers.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-
-export function getNewProgressBarWorkers(): Worker {
-  return {
-    postMessage: () => {
-      throw new Error('JEST-MOCK-GET-NEW-PROGRESS-BAR-WORKER');
-    },
-  } as unknown as Worker;
-}
diff --git a/src/Frontend/web-workers/contained-external-attributions-accordion-worker.ts b/src/Frontend/web-workers/contained-external-attributions-accordion-worker.ts
deleted file mode 100644
index 0303e202..00000000
--- a/src/Frontend/web-workers/contained-external-attributions-accordion-worker.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { AttributionsToHashes } from '../../shared/shared-types';
-import { getContainedExternalDisplayPackageInfosWithCount } from '../Components/AggregatedAttributionsPanel/accordion-panel-helpers';
-import { DisplayPackageInfosWithCountAndResourceId } from '../types/types';
-import { PanelAttributionData } from '../util/get-contained-packages';
-
-let cachedExternalData: PanelAttributionData | null = null;
-let cachedAttributionsToHashes: AttributionsToHashes | null = null;
-
-self.onmessage = ({
-  data: {
-    selectedResourceId,
-    externalData,
-    resolvedExternalAttributions,
-    attributionsToHashes,
-    panelTitle,
-  },
-}): void => {
-  // externalData = null is used to empty the cached data
-  if (externalData !== undefined) {
-    cachedExternalData = externalData;
-  }
-
-  if (attributionsToHashes !== undefined) {
-    cachedAttributionsToHashes = attributionsToHashes;
-  }
-
-  if (selectedResourceId) {
-    if (cachedExternalData && cachedAttributionsToHashes) {
-      const [sortedPackageCardIds, displayAttributionIdsWithCount] =
-        getContainedExternalDisplayPackageInfosWithCount({
-          selectedResourceId,
-          externalData: cachedExternalData,
-          resolvedExternalAttributions,
-          attributionsToHashes: cachedAttributionsToHashes,
-          panelTitle,
-        });
-      const output: DisplayPackageInfosWithCountAndResourceId = {
-        resourceId: selectedResourceId,
-        sortedPackageCardIds,
-        displayPackageInfosWithCount: displayAttributionIdsWithCount,
-      };
-
-      self.postMessage({
-        output,
-      });
-    } else {
-      self.postMessage({
-        output: null,
-      });
-    }
-  }
-};
-
-self.onerror = (): void => {
-  cachedExternalData = null;
-};
diff --git a/src/Frontend/web-workers/contained-manual-attributions-accordion-worker.ts b/src/Frontend/web-workers/contained-manual-attributions-accordion-worker.ts
deleted file mode 100644
index a8d626e9..00000000
--- a/src/Frontend/web-workers/contained-manual-attributions-accordion-worker.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { getContainedManualDisplayPackageInfosWithCount } from '../Components/AggregatedAttributionsPanel/accordion-panel-helpers';
-import { DisplayPackageInfosWithCountAndResourceId } from '../types/types';
-import { PanelAttributionData } from '../util/get-contained-packages';
-
-let cachedManualData: PanelAttributionData | null = null;
-
-self.onmessage = ({
-  data: { selectedResourceId, manualData, panelTitle },
-}): void => {
-  if (manualData !== undefined) {
-    cachedManualData = manualData;
-  }
-
-  if (selectedResourceId) {
-    if (cachedManualData) {
-      const [sortedPackageCardIds, displayPackageInfosWithCount] =
-        getContainedManualDisplayPackageInfosWithCount({
-          selectedResourceId,
-          manualData: cachedManualData,
-          panelTitle,
-        });
-      const output: DisplayPackageInfosWithCountAndResourceId = {
-        resourceId: selectedResourceId,
-        sortedPackageCardIds,
-        displayPackageInfosWithCount,
-      };
-
-      self.postMessage({
-        output,
-      });
-    } else {
-      self.postMessage({
-        output: null,
-      });
-    }
-  }
-};
diff --git a/src/Frontend/web-workers/get-new-accordion-workers.ts b/src/Frontend/web-workers/get-new-accordion-workers.ts
deleted file mode 100644
index 79fbbf32..00000000
--- a/src/Frontend/web-workers/get-new-accordion-workers.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import ContainedExternalAttributionsAccordionWorker from './contained-external-attributions-accordion-worker?worker';
-import ContainedManualAttributionsAccordionWorker from './contained-manual-attributions-accordion-worker?worker';
-
-export interface ResourceDetailsTabsWorkers {
-  containedExternalAttributionsAccordionWorker: Worker;
-  containedManualAttributionsAccordionWorker: Worker;
-}
-
-export function getNewAccordionWorkers(): ResourceDetailsTabsWorkers {
-  return {
-    containedExternalAttributionsAccordionWorker:
-      new ContainedExternalAttributionsAccordionWorker(),
-    containedManualAttributionsAccordionWorker:
-      new ContainedManualAttributionsAccordionWorker(),
-  };
-}
diff --git a/src/Frontend/web-workers/get-new-progress-bar-workers.ts b/src/Frontend/web-workers/get-new-progress-bar-workers.ts
deleted file mode 100644
index 9c8f4f1a..00000000
--- a/src/Frontend/web-workers/get-new-progress-bar-workers.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import ProgressBarWorker from './progress-bar-worker?worker';
-
-export interface ProgressBarWorkers {
-  TopProgressBarWorker: Worker;
-  FolderProgressBarWorker: Worker;
-}
-
-export function getNewProgressBarWorkers(): ProgressBarWorkers {
-  return {
-    TopProgressBarWorker: new ProgressBarWorker(),
-    FolderProgressBarWorker: new ProgressBarWorker(),
-  };
-}
diff --git a/src/Frontend/web-workers/progress-bar-worker.ts b/src/Frontend/web-workers/progress-bar-worker.ts
deleted file mode 100644
index 9df62068..00000000
--- a/src/Frontend/web-workers/progress-bar-worker.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import {
-  Attributions,
-  Resources,
-  ResourcesToAttributions,
-} from '../../shared/shared-types';
-import { getUpdatedProgressBarData } from '../state/helpers/progress-bar-data-helpers';
-import { ProgressBarDataAndResourceId } from '../types/types';
-
-let cachedResources: Resources | null = null;
-let cachedExternalAttributions: Attributions | null = null;
-let cachedResourcesToExternalAttributions: ResourcesToAttributions | null =
-  null;
-let cachedAttributionBreakpoints: Set<string> | null = null;
-let cachedFilesWithChildren: Set<string> | null = null;
-let cachedManualAttributions: Attributions | null = null;
-let cachedResourcesToManualAttributions: ResourcesToAttributions | null = null;
-let cachedResolvedExternalAttributions: Set<string> | null = null;
-
-self.onmessage = ({
-  data: {
-    isCacheInitializationMessage,
-    resources,
-    resourceId,
-    manualAttributions,
-    externalAttributions,
-    resourcesToManualAttributions,
-    resourcesToExternalAttributions,
-    resolvedExternalAttributions,
-    attributionBreakpoints,
-    filesWithChildren,
-  },
-}): void => {
-  if (
-    manualAttributions !== undefined &&
-    resourcesToManualAttributions !== undefined &&
-    resolvedExternalAttributions !== undefined
-  ) {
-    cachedManualAttributions = manualAttributions;
-    cachedResourcesToManualAttributions = resourcesToManualAttributions;
-    cachedResolvedExternalAttributions = resolvedExternalAttributions;
-  }
-
-  if (isCacheInitializationMessage) {
-    cachedResources = resources;
-    cachedExternalAttributions = externalAttributions;
-    cachedResourcesToExternalAttributions = resourcesToExternalAttributions;
-    cachedAttributionBreakpoints = attributionBreakpoints;
-    cachedFilesWithChildren = filesWithChildren;
-  }
-
-  if (
-    cachedManualAttributions &&
-    cachedExternalAttributions &&
-    cachedResourcesToManualAttributions &&
-    cachedResolvedExternalAttributions &&
-    cachedResources &&
-    cachedResourcesToExternalAttributions &&
-    cachedAttributionBreakpoints &&
-    cachedFilesWithChildren
-  ) {
-    const progressBarData = getUpdatedProgressBarData({
-      resources: cachedResources,
-      resourceId,
-      manualAttributions: cachedManualAttributions,
-      externalAttributions: cachedExternalAttributions,
-      resourcesToManualAttributions: cachedResourcesToManualAttributions,
-      resourcesToExternalAttributions: cachedResourcesToExternalAttributions,
-      resolvedExternalAttributions: cachedResolvedExternalAttributions,
-      attributionBreakpoints: cachedAttributionBreakpoints,
-      filesWithChildren: cachedFilesWithChildren,
-    });
-
-    const output: ProgressBarDataAndResourceId = {
-      progressBarData,
-      resourceId,
-    };
-
-    self.postMessage({
-      output,
-    });
-  }
-};
diff --git a/src/Frontend/web-workers/scripts/get-attributions-in-folder-content.ts b/src/Frontend/web-workers/scripts/get-attributions-in-folder-content.ts
new file mode 100644
index 00000000..00a3692a
--- /dev/null
+++ b/src/Frontend/web-workers/scripts/get-attributions-in-folder-content.ts
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { getContainedManualDisplayPackageInfosWithCount } from '../../Components/AggregatedAttributionsPanel/accordion-panel-helpers';
+import { PackagePanelTitle } from '../../enums/enums';
+import { PanelData } from '../../types/types';
+import { PanelAttributionData } from '../../util/get-contained-packages';
+
+interface Props {
+  manualData: PanelAttributionData;
+  resourceId: string;
+}
+
+export function getAttributionsInFolderContent({
+  manualData,
+  resourceId,
+}: Props): PanelData {
+  const [sortedPackageCardIds, displayPackageInfosWithCount] =
+    getContainedManualDisplayPackageInfosWithCount({
+      selectedResourceId: resourceId,
+      manualData,
+      panelTitle: PackagePanelTitle.ContainedManualPackages,
+    });
+
+  return {
+    sortedPackageCardIds,
+    displayPackageInfosWithCount,
+  };
+}
diff --git a/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts b/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts
new file mode 100644
index 00000000..40a9691d
--- /dev/null
+++ b/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts
@@ -0,0 +1,96 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { compact, orderBy } from 'lodash';
+
+import {
+  ExternalAttributionSources,
+  PackageInfo,
+  SignalWithCount,
+} from '../../../shared/shared-types';
+import {
+  getContainedExternalPackages,
+  getContainedManualPackages,
+  PanelAttributionData,
+} from '../../util/get-contained-packages';
+import { generatePurl } from '../../util/handle-purl';
+
+export interface Props {
+  externalData: PanelAttributionData;
+  manualData: PanelAttributionData;
+  resolvedExternalAttributions: Set<string>;
+  resourceId: string;
+  sources: ExternalAttributionSources;
+}
+
+export function getAutocompleteSignals({
+  externalData,
+  manualData,
+  resolvedExternalAttributions,
+  resourceId,
+  sources,
+}: Props) {
+  const signalsOnResource = (
+    externalData.resourcesToAttributions[resourceId] || []
+  ).map((id) => externalData.attributions[id]);
+  const signalsOnChildren = getContainedExternalPackages(
+    resourceId,
+    externalData.resourcesWithAttributedChildren,
+    externalData.resourcesToAttributions,
+    resolvedExternalAttributions,
+  ).map(({ attributionId }) => externalData.attributions[attributionId]);
+  const attributionsOnChildren = getContainedManualPackages(
+    resourceId,
+    manualData,
+  ).map(({ attributionId }) => manualData.attributions[attributionId]);
+
+  const getUniqueKey = (item: PackageInfo) =>
+    compact([
+      item.source && sources[item.source.name]?.name,
+      item.copyright,
+      item.licenseName,
+      generatePurl(item),
+    ]).join();
+
+  const signals = [
+    ...signalsOnResource,
+    ...signalsOnChildren,
+    ...attributionsOnChildren,
+  ].reduce<Array<SignalWithCount>>((acc, signal) => {
+    if (!generatePurl(signal)) {
+      return acc;
+    }
+
+    const key = getUniqueKey(signal);
+    const dupeIndex = acc.findIndex((item) => getUniqueKey(item) === key);
+
+    if (dupeIndex === -1) {
+      acc.push({
+        count: 1,
+        ...signal,
+      });
+    } else {
+      acc[dupeIndex] = {
+        ...acc[dupeIndex],
+        count: (acc[dupeIndex].count ?? 0) + 1,
+        preferred: acc[dupeIndex].preferred || signal.preferred,
+        wasPreferred: acc[dupeIndex].wasPreferred || signal.wasPreferred,
+        preSelected: acc[dupeIndex].preSelected || signal.preSelected,
+      };
+    }
+
+    return acc;
+  }, []);
+
+  return orderBy(
+    signals,
+    [
+      ({ source }) => (source && sources[source.name])?.priority ?? 0,
+      ({ preferred }) => (preferred ? 1 : 0),
+      ({ wasPreferred }) => (wasPreferred ? 1 : 0),
+      ({ count }) => count ?? 0,
+    ],
+    ['desc', 'desc', 'desc', 'desc'],
+  );
+}
diff --git a/src/Frontend/web-workers/scripts/get-progress-data.ts b/src/Frontend/web-workers/scripts/get-progress-data.ts
new file mode 100644
index 00000000..2d2b897a
--- /dev/null
+++ b/src/Frontend/web-workers/scripts/get-progress-data.ts
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { Resources } from '../../../shared/shared-types';
+import { getUpdatedProgressBarData } from '../../state/helpers/progress-bar-data-helpers';
+import { ProgressBarData } from '../../types/types';
+import { PanelAttributionData } from '../../util/get-contained-packages';
+
+interface Props {
+  attributionBreakpoints: Set<string>;
+  externalData: PanelAttributionData;
+  filesWithChildren: Set<string>;
+  manualData: PanelAttributionData;
+  resolvedExternalAttributions: Set<string>;
+  resourceId: string;
+  resources: Resources;
+}
+
+export function getProgressData({
+  attributionBreakpoints,
+  externalData,
+  filesWithChildren,
+  manualData,
+  resolvedExternalAttributions,
+  resourceId,
+  resources,
+}: Props): ProgressBarData {
+  return getUpdatedProgressBarData({
+    resources,
+    resourceId,
+    manualAttributions: manualData.attributions,
+    externalAttributions: externalData.attributions,
+    resourcesToManualAttributions: manualData.resourcesToAttributions,
+    resourcesToExternalAttributions: externalData.resourcesToAttributions,
+    resolvedExternalAttributions,
+    attributionBreakpoints,
+    filesWithChildren,
+  });
+}
diff --git a/src/Frontend/web-workers/scripts/get-signals-in-folder-content.ts b/src/Frontend/web-workers/scripts/get-signals-in-folder-content.ts
new file mode 100644
index 00000000..e7de9479
--- /dev/null
+++ b/src/Frontend/web-workers/scripts/get-signals-in-folder-content.ts
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { AttributionsToHashes } from '../../../shared/shared-types';
+import { getContainedExternalDisplayPackageInfosWithCount } from '../../Components/AggregatedAttributionsPanel/accordion-panel-helpers';
+import { PackagePanelTitle } from '../../enums/enums';
+import { PanelData } from '../../types/types';
+import { PanelAttributionData } from '../../util/get-contained-packages';
+
+interface Props {
+  externalData: PanelAttributionData;
+  attributionsToHashes: AttributionsToHashes;
+  resolvedExternalAttributions: Set<string>;
+  resourceId: string;
+}
+
+export function getSignalsInFolderContent({
+  externalData,
+  attributionsToHashes,
+  resolvedExternalAttributions,
+  resourceId,
+}: Props): PanelData {
+  const [sortedPackageCardIds, displayAttributionIdsWithCount] =
+    getContainedExternalDisplayPackageInfosWithCount({
+      selectedResourceId: resourceId,
+      externalData,
+      resolvedExternalAttributions,
+      attributionsToHashes,
+      panelTitle: PackagePanelTitle.ContainedExternalPackages,
+    });
+
+  return {
+    sortedPackageCardIds,
+    displayPackageInfosWithCount: displayAttributionIdsWithCount,
+  };
+}
diff --git a/src/Frontend/web-workers/signals-worker.ts b/src/Frontend/web-workers/signals-worker.ts
new file mode 100644
index 00000000..fd3eea4d
--- /dev/null
+++ b/src/Frontend/web-workers/signals-worker.ts
@@ -0,0 +1,246 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import {
+  AttributionsToHashes,
+  ExternalAttributionSources,
+  Resources,
+  SignalWithCount,
+} from '../../shared/shared-types';
+import { PanelData, ProgressBarData } from '../types/types';
+import { PanelAttributionData } from '../util/get-contained-packages';
+import { shouldNotBeCalled } from '../util/should-not-be-called';
+import { getAttributionsInFolderContent } from './scripts/get-attributions-in-folder-content';
+import { getAutocompleteSignals } from './scripts/get-autocomplete-signals';
+import { getProgressData } from './scripts/get-progress-data';
+import { getSignalsInFolderContent } from './scripts/get-signals-in-folder-content';
+
+type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
+
+export type SignalsWorkerInput =
+  | { name: 'attributionBreakpoints'; data: Set<string> }
+  | { name: 'attributionsToHashes'; data: AttributionsToHashes }
+  | { name: 'externalData'; data: PanelAttributionData }
+  | { name: 'filesWithChildren'; data: Set<string> }
+  | { name: 'manualData'; data: PanelAttributionData }
+  | { name: 'resolvedExternalAttributions'; data: Set<string> }
+  | { name: 'resourceId'; data: string }
+  | { name: 'resources'; data: Resources }
+  | { name: 'sources'; data: ExternalAttributionSources };
+
+export type SignalsWorkerOutput =
+  | {
+      name: 'autocompleteSignals';
+      data: Array<SignalWithCount>;
+    }
+  | {
+      name: 'attributionsInFolderContent';
+      data: PanelData;
+    }
+  | {
+      name: 'signalsInFolderContent';
+      data: PanelData;
+    }
+  | {
+      name: 'overallProgressData';
+      data: ProgressBarData;
+    }
+  | {
+      name: 'folderProgressData';
+      data: ProgressBarData;
+    };
+
+interface State {
+  attributionBreakpoints?: Set<string>;
+  attributionsToHashes?: AttributionsToHashes;
+  externalData?: PanelAttributionData;
+  filesWithChildren?: Set<string>;
+  manualData?: PanelAttributionData;
+  resolvedExternalAttributions?: Set<string>;
+  resourceId?: string;
+  resources?: Resources;
+  sources?: ExternalAttributionSources;
+}
+
+export class SignalsWorker {
+  constructor(
+    private readonly dispatch: (output: SignalsWorkerOutput) => void,
+    private readonly state: State = {
+      externalData: undefined,
+      manualData: undefined,
+      resolvedExternalAttributions: undefined,
+      sources: undefined,
+      resourceId: undefined,
+    },
+  ) {}
+
+  public processInput(input: SignalsWorkerInput) {
+    this.setData(input);
+    this.dispatchSignalsInFolderContent(input);
+    this.dispatchAttributionsInFolderContent(input);
+    this.dispatchFolderProgressData(input);
+    this.dispatchOverallProgressData(input);
+    this.dispatchAutocompleteSignals(input);
+  }
+
+  private setData(input: SignalsWorkerInput) {
+    switch (input.name) {
+      case 'attributionsToHashes':
+        this.state.attributionsToHashes = input.data;
+        break;
+      case 'externalData':
+        this.state.externalData = input.data;
+        break;
+      case 'manualData':
+        this.state.manualData = input.data;
+        break;
+      case 'resolvedExternalAttributions':
+        this.state.resolvedExternalAttributions = input.data;
+        break;
+      case 'sources':
+        this.state.sources = input.data;
+        break;
+      case 'resourceId':
+        this.state.resourceId = input.data;
+        break;
+      case 'resources':
+        this.state.resources = input.data;
+        break;
+      case 'attributionBreakpoints':
+        this.state.attributionBreakpoints = input.data;
+        break;
+      case 'filesWithChildren':
+        this.state.filesWithChildren = input.data;
+        break;
+      default:
+        shouldNotBeCalled(input);
+    }
+  }
+
+  private dispatchAutocompleteSignals(input: SignalsWorkerInput) {
+    if (
+      this.isHydrated(this.state, input, [
+        'resourceId',
+        'externalData',
+        'manualData',
+        'resolvedExternalAttributions',
+        'sources',
+      ])
+    ) {
+      this.dispatch({
+        name: 'autocompleteSignals',
+        data: getAutocompleteSignals({
+          resourceId: this.state.resourceId,
+          externalData: this.state.externalData,
+          manualData: this.state.manualData,
+          resolvedExternalAttributions: this.state.resolvedExternalAttributions,
+          sources: this.state.sources,
+        }),
+      });
+    }
+  }
+
+  private dispatchAttributionsInFolderContent(input: SignalsWorkerInput) {
+    if (this.isHydrated(this.state, input, ['resourceId', 'manualData'])) {
+      this.dispatch({
+        name: 'attributionsInFolderContent',
+        data: getAttributionsInFolderContent({
+          resourceId: this.state.resourceId,
+          manualData: this.state.manualData,
+        }),
+      });
+    }
+  }
+
+  private dispatchSignalsInFolderContent(input: SignalsWorkerInput) {
+    if (
+      this.isHydrated(this.state, input, [
+        'resourceId',
+        'attributionsToHashes',
+        'externalData',
+        'resolvedExternalAttributions',
+      ])
+    ) {
+      this.dispatch({
+        name: 'signalsInFolderContent',
+        data: getSignalsInFolderContent({
+          resourceId: this.state.resourceId,
+          attributionsToHashes: this.state.attributionsToHashes,
+          externalData: this.state.externalData,
+          resolvedExternalAttributions: this.state.resolvedExternalAttributions,
+        }),
+      });
+    }
+  }
+
+  private dispatchOverallProgressData(input: SignalsWorkerInput) {
+    if (
+      this.isHydrated(this.state, input, [
+        'attributionBreakpoints',
+        'externalData',
+        'filesWithChildren',
+        'manualData',
+        'resolvedExternalAttributions',
+        'resources',
+      ])
+    ) {
+      this.dispatch({
+        name: 'overallProgressData',
+        data: getProgressData({
+          attributionBreakpoints: this.state.attributionBreakpoints,
+          externalData: this.state.externalData,
+          filesWithChildren: this.state.filesWithChildren,
+          manualData: this.state.manualData,
+          resolvedExternalAttributions: this.state.resolvedExternalAttributions,
+          resourceId: '/',
+          resources: this.state.resources,
+        }),
+      });
+    }
+  }
+
+  private dispatchFolderProgressData(input: SignalsWorkerInput) {
+    if (
+      this.isHydrated(this.state, input, [
+        'attributionBreakpoints',
+        'externalData',
+        'filesWithChildren',
+        'manualData',
+        'resolvedExternalAttributions',
+        'resourceId',
+        'resources',
+      ])
+    ) {
+      this.dispatch({
+        name: 'folderProgressData',
+        data: getProgressData({
+          attributionBreakpoints: this.state.attributionBreakpoints,
+          externalData: this.state.externalData,
+          filesWithChildren: this.state.filesWithChildren,
+          manualData: this.state.manualData,
+          resolvedExternalAttributions: this.state.resolvedExternalAttributions,
+          resourceId: this.state.resourceId,
+          resources: this.state.resources,
+        }),
+      });
+    }
+  }
+
+  private isHydrated<T extends Array<keyof State>>(
+    state: State,
+    input: SignalsWorkerInput,
+    dependencies: T,
+  ): state is WithRequired<State, (typeof dependencies)[number]> {
+    return (
+      dependencies.includes(input.name) &&
+      dependencies.every((dependency) => !!state[dependency])
+    );
+  }
+}
+
+const worker = new SignalsWorker(self.postMessage.bind(self));
+
+self.onmessage = (event: MessageEvent<SignalsWorkerInput>) => {
+  worker.processInput(event.data);
+};
diff --git a/src/Frontend/web-workers/use-signals-worker.ts b/src/Frontend/web-workers/use-signals-worker.ts
new file mode 100644
index 00000000..d0a8e9c7
--- /dev/null
+++ b/src/Frontend/web-workers/use-signals-worker.ts
@@ -0,0 +1,251 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { useEffect, useState } from 'react';
+
+import { SignalWithCount } from '../../shared/shared-types';
+import { useAppSelector } from '../state/hooks';
+import {
+  getAttributionBreakpoints,
+  getExternalAttributionSources,
+  getExternalAttributionsToHashes,
+  getExternalData,
+  getFilesWithChildren,
+  getManualData,
+  getProjectMetadata,
+  getResources,
+} from '../state/selectors/all-views-resource-selectors';
+import {
+  getResolvedExternalAttributions,
+  getSelectedResourceId,
+} from '../state/selectors/audit-view-resource-selectors';
+import { isAuditViewSelected } from '../state/selectors/view-selector';
+import { PanelData, ProgressBarData } from '../types/types';
+import { shouldNotBeCalled } from '../util/should-not-be-called';
+import { useVariable } from '../util/use-variable';
+import { SignalsWorkerInput, SignalsWorkerOutput } from './signals-worker';
+
+export enum WORKER_REDUX_KEYS {
+  AUTOCOMPLETE_SIGNALS = 'autocomplete-signals',
+  PANEL_DATA = 'panel-data',
+  OVERALL_PROGRESS_DATA = 'overall-progress-data',
+  FOLDER_PROGRESS_DATA = 'folder-progress-data',
+}
+
+interface WorkerPanelData {
+  attributionsInFolderContent: PanelData;
+  signalsInFolderContent: PanelData;
+}
+
+const initialWorkerPanelData: WorkerPanelData = {
+  attributionsInFolderContent: {
+    sortedPackageCardIds: [],
+    displayPackageInfosWithCount: {},
+  },
+  signalsInFolderContent: {
+    sortedPackageCardIds: [],
+    displayPackageInfosWithCount: {},
+  },
+};
+
+export function useAutocompleteSignals() {
+  const [autocompleteSignals] = useVariable<Array<SignalWithCount>>(
+    WORKER_REDUX_KEYS.AUTOCOMPLETE_SIGNALS,
+    [],
+  );
+
+  return autocompleteSignals;
+}
+
+export function usePanelData() {
+  const [panelData] = useVariable<WorkerPanelData>(
+    WORKER_REDUX_KEYS.PANEL_DATA,
+    initialWorkerPanelData,
+  );
+
+  return panelData;
+}
+
+export function useOverallProgressData() {
+  const [overallProgressData] = useVariable<ProgressBarData | null>(
+    WORKER_REDUX_KEYS.OVERALL_PROGRESS_DATA,
+    null,
+  );
+
+  return overallProgressData;
+}
+
+export function useFolderProgressData() {
+  const [folderProgressData] = useVariable<ProgressBarData | null>(
+    WORKER_REDUX_KEYS.FOLDER_PROGRESS_DATA,
+    null,
+  );
+
+  return folderProgressData;
+}
+
+export function useSignalsWorker() {
+  const resourceId = useAppSelector(getSelectedResourceId);
+  const externalData = useAppSelector(getExternalData);
+  const manualData = useAppSelector(getManualData);
+  const sources = useAppSelector(getExternalAttributionSources);
+  const resolvedExternalAttributions = useAppSelector(
+    getResolvedExternalAttributions,
+  );
+  const attributionsToHashes = useAppSelector(getExternalAttributionsToHashes);
+  const resources = useAppSelector(getResources);
+  const attributionBreakpoints = useAppSelector(getAttributionBreakpoints);
+  const filesWithChildren = useAppSelector(getFilesWithChildren);
+  const isAuditView = useAppSelector(isAuditViewSelected);
+  const { projectId } = useAppSelector(getProjectMetadata);
+
+  const [worker, setWorker] = useState<Worker>();
+  const [, setAutocompleteSignals] = useVariable<Array<SignalWithCount>>(
+    WORKER_REDUX_KEYS.AUTOCOMPLETE_SIGNALS,
+    [],
+  );
+  const [, setPanelData] = useVariable<WorkerPanelData>(
+    WORKER_REDUX_KEYS.PANEL_DATA,
+    initialWorkerPanelData,
+  );
+  const [, setOverallProgressData] = useVariable<ProgressBarData | null>(
+    WORKER_REDUX_KEYS.OVERALL_PROGRESS_DATA,
+    null,
+  );
+  const [, setFolderProgressData] = useVariable<ProgressBarData | null>(
+    WORKER_REDUX_KEYS.FOLDER_PROGRESS_DATA,
+    null,
+  );
+
+  useEffect(() => {
+    if (!projectId) {
+      return;
+    }
+
+    const newWorker = new Worker(
+      new URL('./signals-worker.ts', import.meta.url),
+      { type: 'module' },
+    );
+    setWorker(newWorker);
+
+    return () => {
+      newWorker.terminate();
+    };
+  }, [projectId]);
+
+  useEffect(() => {
+    if (worker) {
+      worker.onmessage = ({ data }: MessageEvent<SignalsWorkerOutput>) => {
+        switch (data.name) {
+          case 'autocompleteSignals':
+            setAutocompleteSignals(data.data);
+            break;
+          case 'attributionsInFolderContent':
+            setPanelData((panelData) => ({
+              ...panelData,
+              attributionsInFolderContent: data.data,
+            }));
+            break;
+          case 'signalsInFolderContent':
+            setPanelData((panelData) => ({
+              ...panelData,
+              signalsInFolderContent: data.data,
+            }));
+            break;
+          case 'overallProgressData':
+            setOverallProgressData(data.data);
+            break;
+          case 'folderProgressData':
+            setFolderProgressData(data.data);
+            break;
+          default:
+            shouldNotBeCalled(data);
+        }
+      };
+    }
+  }, [
+    setAutocompleteSignals,
+    setFolderProgressData,
+    setOverallProgressData,
+    setPanelData,
+    worker,
+  ]);
+
+  useEffect(() => {
+    if (isAuditView && resourceId) {
+      worker?.postMessage({
+        name: 'resourceId',
+        data: resourceId,
+      } satisfies SignalsWorkerInput);
+    }
+  }, [isAuditView, resourceId, worker]);
+
+  useEffect(() => {
+    worker?.postMessage({
+      name: 'externalData',
+      data: {
+        attributions: externalData.attributions,
+        resourcesWithAttributedChildren:
+          externalData.resourcesWithAttributedChildren,
+        resourcesToAttributions: externalData.resourcesToAttributions,
+      },
+    } satisfies SignalsWorkerInput);
+  }, [externalData, worker]);
+
+  useEffect(() => {
+    worker?.postMessage({
+      name: 'manualData',
+      data: {
+        attributions: manualData.attributions,
+        resourcesWithAttributedChildren:
+          manualData.resourcesWithAttributedChildren,
+        resourcesToAttributions: manualData.resourcesToAttributions,
+      },
+    } satisfies SignalsWorkerInput);
+  }, [manualData, worker]);
+
+  useEffect(() => {
+    worker?.postMessage({
+      name: 'resolvedExternalAttributions',
+      data: resolvedExternalAttributions,
+    } satisfies SignalsWorkerInput);
+  }, [resolvedExternalAttributions, worker]);
+
+  useEffect(() => {
+    worker?.postMessage({
+      name: 'sources',
+      data: sources,
+    } satisfies SignalsWorkerInput);
+  }, [sources, worker]);
+
+  useEffect(() => {
+    worker?.postMessage({
+      name: 'attributionsToHashes',
+      data: attributionsToHashes,
+    } satisfies SignalsWorkerInput);
+  }, [attributionsToHashes, worker]);
+
+  useEffect(() => {
+    worker?.postMessage({
+      name: 'attributionBreakpoints',
+      data: attributionBreakpoints,
+    } satisfies SignalsWorkerInput);
+  }, [attributionBreakpoints, worker]);
+
+  useEffect(() => {
+    worker?.postMessage({
+      name: 'filesWithChildren',
+      data: filesWithChildren,
+    } satisfies SignalsWorkerInput);
+  }, [filesWithChildren, worker]);
+
+  useEffect(() => {
+    if (resources) {
+      worker?.postMessage({
+        name: 'resources',
+        data: resources,
+      } satisfies SignalsWorkerInput);
+    }
+  }, [resources, worker]);
+}
diff --git a/src/shared/Faker.ts b/src/shared/Faker.ts
index 5452a597..e780e672 100644
--- a/src/shared/Faker.ts
+++ b/src/shared/Faker.ts
@@ -2,7 +2,8 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { en, Faker as NativeFaker } from '@faker-js/faker';
+// eslint-disable-next-line no-restricted-imports
+import { base, en, Faker as NativeFaker } from '@faker-js/faker';
 
 import type {
   RawPackageInfo as ExternalPackageInfo,
@@ -11,6 +12,8 @@ import type {
   RawFrequentLicense,
 } from '../ElectronBackend/types/types';
 import {
+  AttributionData,
+  AttributionsToResources,
   BaseUrlsForSources,
   DiscreteConfidence,
   DisplayPackageInfo,
@@ -19,6 +22,7 @@ import {
   ProjectMetadata,
   Resources,
   ResourcesToAttributions,
+  ResourcesWithAttributedChildren,
   Source,
 } from './shared-types';
 
@@ -159,6 +163,16 @@ class OpossumModule {
     );
   }
 
+  public static attributionsToResources(
+    props?: AttributionsToResources,
+  ): AttributionsToResources {
+    return (
+      props || {
+        [faker.string.uuid()]: [faker.system.filePath()],
+      }
+    );
+  }
+
   public static filePath(...elements: Array<string>): string {
     if (!elements[0]?.startsWith('/')) {
       elements.unshift('');
@@ -241,10 +255,45 @@ class OpossumModule {
       ...props,
     };
   }
+
+  public static resourcesWithAttributedChildren(
+    props: Partial<ResourcesWithAttributedChildren> = {},
+  ): ResourcesWithAttributedChildren {
+    return {
+      paths: [],
+      pathsToIndices: {},
+      attributedChildren: {},
+      ...props,
+    };
+  }
+
+  public static manualAttributionData(
+    props: Partial<AttributionData> = {},
+  ): AttributionData {
+    return {
+      attributions: this.manualAttributions(),
+      attributionsToResources: this.attributionsToResources(),
+      resourcesToAttributions: this.resourcesToAttributions(),
+      resourcesWithAttributedChildren: this.resourcesWithAttributedChildren(),
+      ...props,
+    };
+  }
+
+  public static externalAttributionData(
+    props: Partial<AttributionData> = {},
+  ): AttributionData {
+    return {
+      attributions: this.externalAttributions(),
+      attributionsToResources: this.attributionsToResources(),
+      resourcesToAttributions: this.resourcesToAttributions(),
+      resourcesWithAttributedChildren: this.resourcesWithAttributedChildren(),
+      ...props,
+    };
+  }
 }
 
 class Faker extends NativeFaker {
   public readonly opossum = OpossumModule;
 }
 
-export const faker = new Faker({ locale: [en] });
+export const faker = new Faker({ locale: [en, base] });
diff --git a/src/shared/shared-types.ts b/src/shared/shared-types.ts
index c9dce8a5..4cb9a8cb 100644
--- a/src/shared/shared-types.ts
+++ b/src/shared/shared-types.ts
@@ -276,3 +276,7 @@ export interface Log {
 export interface UserSettings {
   showProjectStatistics: boolean;
 }
+
+export type SignalWithCount = PackageInfo & {
+  count?: number;
+};
diff --git a/src/shared/text.ts b/src/shared/text.ts
index eb6ac583..b1c36c8a 100644
--- a/src/shared/text.ts
+++ b/src/shared/text.ts
@@ -6,7 +6,6 @@
 export const text = {
   attributionColumn: {
     packageSubPanel: {
-      attributionType: 'Attribution Type',
       confidence: 'Confidence',
       copyToClipboard: 'Copy to clipboard',
       fetchPackageInfo: 'Fetch package information',
@@ -24,8 +23,17 @@ export const text = {
       repositoryUrl: 'Repository URL',
       searchForPackage: 'Search for package information',
     },
-    packageCoordinates: 'Package Coordinates',
+    amongSignals: 'among signals',
+    commonEcosystems: 'Common Ecosystems',
+    commonLicenses: 'Common Licenses',
     legalInformation: 'Legal Information',
+    licenseName: 'License Name',
+    licenseTextModified: '(License text modified)',
+    manualAttributions: 'Manual Attributions',
+    occurrence: 'occurrence',
+    packageCoordinates: 'Package Coordinates',
+    useAutocompleteSuggestion:
+      'Adopt all coordinates and legal information from suggestion',
   },
   changePreferredStatusGloballyPopup: {
     markAsPreferred: 'Do you really want to prefer the attribution globally?',
@@ -50,5 +58,6 @@ export const text = {
   },
   locatorPopup: {
     onlySearchLicenseNames: 'Only search license names',
+    license: 'License',
   },
 };
diff --git a/tsconfig.json b/tsconfig.json
index 36f356a3..4708d477 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -28,12 +28,6 @@
       "@testing-library/jest-dom"
     ]
   },
-  "include": [
-    "src/**/*",
-    ".eslintrc.js",
-    "setupTests.ts",
-    "vite.config.mts",
-    "index.html"
-  ],
+  "include": ["src/**/*", "typings"],
   "exclude": ["src/ElectronBackend/**/*"]
 }
diff --git a/typings/react.d.ts b/typings/react.d.ts
new file mode 100644
index 00000000..68f92f3b
--- /dev/null
+++ b/typings/react.d.ts
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { ReactElement, Ref, RefAttributes } from 'react';
+
+declare module 'react' {
+  // Overwrite type definition of forwardRef to allow forwarding generic components.
+  // See https://fettblog.eu/typescript-react-generic-forward-refs/
+  function forwardRef<T, P>(
+    render: (props: P, ref: Ref<T>) => ReactElement | null,
+  ): (props: P & RefAttributes<T>) => ReactElement | null;
+}


Test patch:
diff --git a/src/Frontend/Components/AggregatedAttributionsPanel/__tests__/AggregatedAttributionsPanel.test.tsx b/src/Frontend/Components/AggregatedAttributionsPanel/__tests__/AggregatedAttributionsPanel.test.tsx
index 67bb8aac..5877c9ab 100644
--- a/src/Frontend/Components/AggregatedAttributionsPanel/__tests__/AggregatedAttributionsPanel.test.tsx
+++ b/src/Frontend/Components/AggregatedAttributionsPanel/__tests__/AggregatedAttributionsPanel.test.tsx
@@ -10,21 +10,13 @@ import {
   Resources,
   ResourcesToAttributions,
 } from '../../../../shared/shared-types';
-import { ButtonText } from '../../../enums/enums';
 import { setExternalAttributionsToHashes } from '../../../state/actions/resource-actions/all-views-simple-actions';
 import { setSelectedResourceId } from '../../../state/actions/resource-actions/audit-view-simple-actions';
 import { loadFromFile } from '../../../state/actions/resource-actions/load-actions';
-import {
-  clickOnButtonInPackageContextMenu,
-  expectButtonInPackageContextMenu,
-  expectGlobalOnlyContextMenuForNotPreselectedAttribution,
-  testCorrectMarkAndUnmarkForReplacementInContextMenu,
-} from '../../../test-helpers/context-menu-test-helpers';
 import {
   getPackagePanel,
   getParsedInputFileEnrichedWithTestData,
 } from '../../../test-helpers/general-test-helpers';
-import { expectPackageInPackagePanel } from '../../../test-helpers/package-panel-helpers';
 import {
   createTestAppStore,
   renderComponentWithStore,
@@ -32,146 +24,7 @@ import {
 import { AggregatedAttributionsPanel } from '../AggregatedAttributionsPanel';
 
 describe('The AggregatedAttributionsPanel', () => {
-  it('renders', () => {
-    const testResources: Resources = { file: 1 };
-
-    const testManualAttributions: Attributions = {
-      uuid1: {
-        packageName: 'React',
-        packageVersion: '16.5.0',
-      },
-      uuid2: {
-        packageName: 'JQuery',
-      },
-      uuid3: {
-        packageVersion: '16',
-      },
-    };
-    const testResourcesToManualAttributions: ResourcesToAttributions = {
-      '/file': ['uuid1', 'uuid2', 'uuid3'],
-    };
-
-    const testExternalAttributions: Attributions = {
-      uuid1: {
-        packageName: 'React',
-        packageVersion: '17.0.0',
-      },
-    };
-    const testResourcesToExternalAttributions: ResourcesToAttributions = {
-      '/file': ['uuid1'],
-    };
-
-    const testStore = createTestAppStore();
-    testStore.dispatch(
-      loadFromFile(
-        getParsedInputFileEnrichedWithTestData({
-          resources: testResources,
-          manualAttributions: testManualAttributions,
-          resourcesToManualAttributions: testResourcesToManualAttributions,
-          externalAttributions: testExternalAttributions,
-          resourcesToExternalAttributions: testResourcesToExternalAttributions,
-        }),
-      ),
-    );
-    testStore.dispatch(setSelectedResourceId('/'));
-
-    renderComponentWithStore(
-      <AggregatedAttributionsPanel isAddToPackageEnabled={true} />,
-      { store: testStore },
-    );
-
-    expectPackageInPackagePanel(
-      screen,
-      'React, 16.5.0',
-      'Attributions in Folder Content',
-    );
-    expectPackageInPackagePanel(
-      screen,
-      'JQuery',
-      'Attributions in Folder Content',
-    );
-    expectPackageInPackagePanel(
-      screen,
-      'React, 17.0.0',
-      'Signals in Folder Content',
-    );
-  });
-
-  it('shows correct replace attribution buttons in the context menu', () => {
-    const testResources: Resources = {
-      root: { src: { file_1: 1, file_2: 1 } },
-      file: 1,
-    };
-    const testManualAttributions: Attributions = {
-      uuid_1: {
-        packageName: 'jQuery',
-        packageVersion: '16.0.0',
-        comment: 'ManualPackage',
-      },
-      uuid_2: {
-        packageName: 'React',
-        packageVersion: '16.0.0',
-        comment: 'ManualPackage',
-      },
-      uuid_3: {
-        packageName: 'Vue',
-        packageVersion: '16.0.0',
-        comment: 'ManualPackage',
-        preSelected: true,
-      },
-    };
-    const testResourcesToManualAttributions: ResourcesToAttributions = {
-      '/root/src/file_1': ['uuid_1'],
-      '/root/src/file_2': ['uuid_2', 'uuid_3'],
-    };
-
-    const testStore = createTestAppStore();
-    testStore.dispatch(
-      loadFromFile(
-        getParsedInputFileEnrichedWithTestData({
-          resources: testResources,
-          manualAttributions: testManualAttributions,
-          resourcesToManualAttributions: testResourcesToManualAttributions,
-        }),
-      ),
-    );
-    testStore.dispatch(setSelectedResourceId('/root/src/'));
-
-    renderComponentWithStore(
-      <AggregatedAttributionsPanel isAddToPackageEnabled={true} />,
-      { store: testStore },
-    );
-
-    expectGlobalOnlyContextMenuForNotPreselectedAttribution(
-      screen,
-      'jQuery, 16.0.0',
-    );
-
-    testCorrectMarkAndUnmarkForReplacementInContextMenu(
-      screen,
-      'jQuery, 16.0.0',
-    );
-
-    clickOnButtonInPackageContextMenu(
-      screen,
-      'jQuery, 16.0.0',
-      ButtonText.MarkForReplacement,
-    );
-
-    expectButtonInPackageContextMenu(
-      screen,
-      'Vue, 16.0.0',
-      ButtonText.ReplaceMarked,
-    );
-
-    expectGlobalOnlyContextMenuForNotPreselectedAttribution(
-      screen,
-      'React, 16.0.0',
-      true,
-    );
-  });
-
-  it('shows merged identical signals', () => {
+  it('shows merged identical signals in signals panel', () => {
     const testResources: Resources = {
       root: {},
     };
@@ -216,57 +69,7 @@ describe('The AggregatedAttributionsPanel', () => {
     expect(queryAllByText(signalsPanel, 'jQuery, 16.0.0')).toHaveLength(1);
   });
 
-  it('shows merged identical signals in folder content', () => {
-    const testResources: Resources = {
-      root: {},
-    };
-    const testExternalAttribution: PackageInfo = {
-      packageName: 'jQuery',
-      packageVersion: '16.0.0',
-      comment: 'ManualPackage',
-    };
-    const testExternalAttributions: Attributions = {
-      uuid_1: testExternalAttribution,
-      uuid_2: testExternalAttribution,
-    };
-    const testResourcesToExternalAttributions: ResourcesToAttributions = {
-      '/root/': ['uuid_1', 'uuid_2'],
-    };
-
-    const testStore = createTestAppStore();
-    testStore.dispatch(
-      loadFromFile(
-        getParsedInputFileEnrichedWithTestData({
-          resources: testResources,
-          externalAttributions: testExternalAttributions,
-          resourcesToExternalAttributions: testResourcesToExternalAttributions,
-        }),
-      ),
-    );
-    testStore.dispatch(setSelectedResourceId('/'));
-    testStore.dispatch(
-      setExternalAttributionsToHashes({
-        uuid_1: '1',
-        uuid_2: '1',
-      }),
-    );
-
-    renderComponentWithStore(
-      <AggregatedAttributionsPanel isAddToPackageEnabled={true} />,
-      { store: testStore },
-    );
-
-    const signalsInFolderContentPanel = getPackagePanel(
-      screen,
-      'Signals in Folder Content',
-    );
-    expect(
-      // eslint-disable-next-line testing-library/prefer-screen-queries
-      queryAllByText(signalsInFolderContentPanel, 'jQuery, 16.0.0'),
-    ).toHaveLength(1);
-  });
-
-  it('does not merge signals without a hash', () => {
+  it('does not merge signals without a hash in signals panel', () => {
     const testResources: Resources = {
       root: {},
     };
diff --git a/src/Frontend/Components/AppContainer/__tests__/AppContainer.test.tsx b/src/Frontend/Components/AppContainer/__tests__/AppContainer.test.tsx
deleted file mode 100644
index 558a858f..00000000
--- a/src/Frontend/Components/AppContainer/__tests__/AppContainer.test.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { renderComponentWithStore } from '../../../test-helpers/render-component-with-store';
-import { AppContainer } from '../AppContainer';
-
-describe('The AppContainer', () => {
-  it('renders AppContainer', () => {
-    renderComponentWithStore(<AppContainer />);
-  });
-});
diff --git a/src/Frontend/Components/AttributionColumn/__tests__/LicenseField.test.tsx b/src/Frontend/Components/AttributionColumn/__tests__/LicenseField.test.tsx
deleted file mode 100644
index 937e1ac1..00000000
--- a/src/Frontend/Components/AttributionColumn/__tests__/LicenseField.test.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { fireEvent, render, screen } from '@testing-library/react';
-import { noop } from 'lodash';
-
-import { FrequentLicenseName } from '../../../../shared/shared-types';
-import { expectElementsInAutoCompleteAndSelectFirst } from '../../../test-helpers/general-test-helpers';
-import { LicenseField } from '../LicenseField';
-
-describe('The LicenseField', () => {
-  it('renders text and label', () => {
-    const testLicenseNames: Array<FrequentLicenseName> = [
-      { shortName: 'MIT', fullName: 'MIT license' },
-      { shortName: 'GPL', fullName: 'General Public License' },
-    ];
-    render(
-      <LicenseField
-        title={'Test Title'}
-        frequentLicenseNames={testLicenseNames}
-        text={'MIT License'}
-        endAdornmentText={'Test Adornment Text'}
-        handleChange={noop}
-      />,
-    );
-
-    expect(screen.queryAllByText('Test Title')).toHaveLength(2);
-    screen.getByText('Test Adornment Text');
-    expect(screen.getByDisplayValue('MIT License'));
-    expectElementsInAutoCompleteAndSelectFirst(screen, ['MIT - MIT license']);
-  });
-
-  it('shows options with substring matching', () => {
-    const testLicenseNames: Array<FrequentLicenseName> = [
-      { shortName: 'MIT', fullName: 'MIT license' },
-      { shortName: 'GPL', fullName: 'General Public License' },
-    ];
-    render(
-      <LicenseField
-        title={'Test Title'}
-        frequentLicenseNames={testLicenseNames}
-        text={'Public Lic'}
-        handleChange={noop}
-        endAdornmentText={'Test Adornment Text'}
-      />,
-    );
-
-    expectElementsInAutoCompleteAndSelectFirst(screen, [
-      'GPL - General Public License',
-    ]);
-  });
-
-  it('sorts entries', () => {
-    const testLicenseNames: Array<FrequentLicenseName> = [
-      { shortName: 'MIT', fullName: 'MIT license' },
-      { shortName: 'GPL', fullName: 'General Public License' },
-    ];
-    render(
-      <LicenseField
-        title={'Test Title'}
-        frequentLicenseNames={testLicenseNames}
-        text={''}
-        endAdornmentText={'Test Adornment Text'}
-        handleChange={noop}
-      />,
-    );
-
-    const autoComplete = screen.getByRole('combobox');
-    autoComplete.focus();
-    fireEvent.keyDown(autoComplete, { key: 'ArrowDown' });
-
-    const licenseDropdownList = screen.getByRole('listbox');
-    expect(licenseDropdownList.children[0]).toHaveTextContent(
-      'GPL - General Public License',
-    );
-    expect(licenseDropdownList.children[1]).toHaveTextContent(
-      'MIT - MIT license',
-    );
-  });
-});
diff --git a/src/Frontend/Components/Autocomplete/__tests__/Autocomplete.test.tsx b/src/Frontend/Components/Autocomplete/__tests__/Autocomplete.test.tsx
new file mode 100644
index 00000000..917f8a31
--- /dev/null
+++ b/src/Frontend/Components/Autocomplete/__tests__/Autocomplete.test.tsx
@@ -0,0 +1,428 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { isEqual } from 'lodash';
+
+import { faker } from '../../../../shared/Faker';
+import { renderComponentWithStore } from '../../../test-helpers/render-component-with-store';
+import { Autocomplete } from '../Autocomplete';
+
+describe('Autocomplete', () => {
+  it('renders label and value', () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        value={options[0]}
+        optionText={{ primary: (option) => option }}
+      />,
+    );
+
+    expect(screen.getByLabelText(title)).toBeInTheDocument();
+    expect(screen.getByDisplayValue(options[0])).toBeInTheDocument();
+    options.forEach((option) => {
+      expect(screen.queryByText(option)).not.toBeInTheDocument();
+    });
+  });
+
+  it('renders popup indicator when not free solo', () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        value={options[0]}
+        optionText={{ primary: (option) => option }}
+      />,
+    );
+
+    expect(screen.getByLabelText('popup indicator')).toBeInTheDocument();
+  });
+
+  it('does not render popup indicator when free solo', () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        value={options[0]}
+        optionText={{ primary: (option) => option }}
+        freeSolo
+      />,
+    );
+
+    expect(screen.queryByLabelText('popup indicator')).not.toBeInTheDocument();
+  });
+
+  it('renders custom end adornment even when free solo and not clearable', () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    const testId = faker.string.sample();
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        optionText={{ primary: (option) => option }}
+        endAdornment={<div data-testid={testId} />}
+        freeSolo
+      />,
+    );
+
+    expect(screen.getByTestId(testId)).toBeInTheDocument();
+  });
+
+  it('renders options when clicked', async () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        value={options[0]}
+        optionText={{ primary: (option) => option }}
+      />,
+    );
+
+    await userEvent.click(screen.getByRole('combobox'));
+
+    options.forEach((option) => {
+      expect(screen.getByText(option)).toBeInTheDocument();
+    });
+  });
+
+  it('displays only filtered options', async () => {
+    const title = faker.string.alpha({ length: 8 });
+    const option = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={[...options, option]}
+        value={options[0]}
+        optionText={{ primary: (option) => option }}
+      />,
+    );
+
+    await userEvent.type(screen.getByRole('combobox'), option);
+
+    expect(screen.getByText(option)).toBeInTheDocument();
+    options.forEach((option) => {
+      expect(screen.queryByText(option)).not.toBeInTheDocument();
+    });
+  });
+
+  it('selects option with mouse', async () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        optionText={{ primary: (option) => option }}
+      />,
+    );
+
+    await userEvent.click(screen.getByRole('combobox'));
+    await userEvent.click(screen.getByText(options[0]));
+
+    expect(screen.getByDisplayValue(options[0])).toBeInTheDocument();
+    options.forEach((option) => {
+      option !== options[0] &&
+        expect(screen.queryByDisplayValue(option)).not.toBeInTheDocument();
+    });
+  });
+
+  it('selects option with keyboard', async () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        optionText={{ primary: (option) => option }}
+      />,
+    );
+
+    await userEvent.click(screen.getByRole('combobox'));
+    await userEvent.keyboard('{ArrowDown}');
+    await userEvent.keyboard('{Enter}');
+
+    expect(screen.getByDisplayValue(options[0])).toBeInTheDocument();
+    options.forEach((option) => {
+      option !== options[0] &&
+        expect(screen.queryByDisplayValue(option)).not.toBeInTheDocument();
+    });
+  });
+
+  it('renders clear button when a value is selected', () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        value={options[0]}
+        optionText={{ primary: (option) => option }}
+      />,
+    );
+
+    expect(screen.getByLabelText('clear button')).toBeInTheDocument();
+  });
+
+  it('does not render clear button when no value is selected', () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        optionText={{ primary: (option) => option }}
+      />,
+    );
+
+    expect(screen.queryByLabelText('clear button')).not.toBeInTheDocument();
+  });
+
+  it('does not render clear button when value is selected but clearing is disabled', () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        value={options[0]}
+        optionText={{ primary: (option) => option }}
+        disableClearable
+      />,
+    );
+
+    expect(screen.queryByLabelText('clear button')).not.toBeInTheDocument();
+  });
+
+  it('does not render clear button when value is selected but autocomplete is disabled', () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        optionText={{ primary: (option) => option }}
+        value={options[0]}
+        disabled
+      />,
+    );
+
+    expect(screen.queryByLabelText('clear button')).not.toBeInTheDocument();
+  });
+
+  it('renders multiple values', () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        value={options}
+        optionText={{ primary: (option) => option }}
+        multiple
+      />,
+    );
+
+    options.forEach((option) => {
+      expect(screen.getByTestId(`tag-${option}`)).toBeInTheDocument();
+    });
+  });
+
+  it('adds one of multiple values', async () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        optionText={{ primary: (option) => option }}
+        multiple
+      />,
+    );
+
+    await userEvent.click(screen.getByRole('combobox'));
+    await userEvent.click(screen.getByText(options[0]));
+
+    expect(screen.getByTestId(`tag-${options[0]}`)).toBeInTheDocument();
+  });
+
+  it('clears one of multiple values', async () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        optionText={{ primary: (option) => option }}
+        multiple
+      />,
+    );
+
+    await userEvent.click(screen.getByRole('combobox'));
+    await userEvent.click(screen.getByText(options[0]));
+    await userEvent.click(
+      within(screen.getByTestId(`tag-${options[0]}`)).getByTestId('CancelIcon'),
+    );
+
+    expect(screen.queryByText(options[0])).not.toBeInTheDocument();
+  });
+
+  it('closes popup when clicking on custom start icon', async () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    const testId = faker.string.sample();
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        optionText={{ primary: (option) => option }}
+        renderOptionStartIcon={(option, { closePopper }) => (
+          <div onClick={closePopper} data-testid={`${option}-${testId}`} />
+        )}
+      />,
+    );
+
+    await userEvent.click(screen.getByRole('combobox'));
+
+    expect(screen.getByText(options[0])).toBeInTheDocument();
+
+    await userEvent.click(screen.getByTestId(`${options[0]}-${testId}`));
+
+    expect(screen.queryByText(options[0])).not.toBeInTheDocument();
+  });
+
+  it('closes popup when clicking on custom end icon', async () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() =>
+      faker.string.alpha({ length: 8 }),
+    );
+    const testId = faker.string.sample();
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        optionText={{ primary: (option) => option }}
+        renderOptionEndIcon={(option, { closePopper }) => (
+          <div onClick={closePopper} data-testid={`${option}-${testId}`} />
+        )}
+      />,
+    );
+
+    await userEvent.click(screen.getByRole('combobox'));
+
+    expect(screen.getByText(options[0])).toBeInTheDocument();
+
+    await userEvent.click(screen.getByTestId(`${options[0]}-${testId}`));
+
+    expect(screen.queryByText(options[0])).not.toBeInTheDocument();
+  });
+
+  it('renders in grouped mode', () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() => ({
+      group: faker.string.alpha({ length: 8 }),
+      value: faker.string.alpha({ length: 8 }),
+    }));
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        value={options[0]}
+        groupBy={(option) => option.group}
+        getOptionLabel={(option) =>
+          typeof option === 'string' ? option : option.value
+        }
+        optionText={{
+          primary: (option) =>
+            typeof option === 'string' ? option : option.value,
+        }}
+      />,
+    );
+
+    expect(screen.getByLabelText(title)).toBeInTheDocument();
+    expect(screen.getByDisplayValue(options[0].value)).toBeInTheDocument();
+    options.forEach((option) => {
+      expect(screen.queryByText(option.value)).not.toBeInTheDocument();
+    });
+  });
+
+  it('selects option in grouped mode', async () => {
+    const title = faker.string.alpha({ length: 8 });
+    const options = faker.helpers.multiple(() => ({
+      group: faker.string.alpha({ length: 8 }),
+      value: faker.string.alpha({ length: 8 }),
+    }));
+    renderComponentWithStore(
+      <Autocomplete
+        title={title}
+        options={options}
+        groupBy={(option) => option.group}
+        getOptionLabel={(option) =>
+          typeof option === 'string' ? option : option.value
+        }
+        optionText={{
+          primary: (option) =>
+            typeof option === 'string' ? option : option.value,
+        }}
+      />,
+    );
+
+    await userEvent.click(screen.getByRole('combobox'));
+
+    expect(screen.getByText(options[0].group)).toBeInTheDocument();
+
+    await userEvent.click(screen.getByText(options[0].value));
+
+    expect(screen.getByDisplayValue(options[0].value)).toBeInTheDocument();
+    options.forEach((option) => {
+      !isEqual(option, options[0]) &&
+        expect(
+          screen.queryByDisplayValue(option.value),
+        ).not.toBeInTheDocument();
+    });
+  });
+});
diff --git a/src/Frontend/Components/IconButton/__tests__/IconButton.test.tsx b/src/Frontend/Components/IconButton/__tests__/IconButton.test.tsx
index 908a7ba2..2b53cb59 100644
--- a/src/Frontend/Components/IconButton/__tests__/IconButton.test.tsx
+++ b/src/Frontend/Components/IconButton/__tests__/IconButton.test.tsx
@@ -53,8 +53,8 @@ describe('IconButton', () => {
     expect(screen.queryByTitle('Test')).not.toBeInTheDocument();
   });
 
-  it('checks for successful application of hidden visibility style', () => {
-    render(
+  it('hides button', () => {
+    const { container } = render(
       <IconButton
         tooltipTitle={'Test'}
         tooltipPlacement="left"
@@ -65,6 +65,6 @@ describe('IconButton', () => {
       />,
     );
 
-    expect(screen.getByLabelText('Test')).toHaveStyle('visibility: hidden');
+    expect(container).toBeEmptyDOMElement();
   });
 });
diff --git a/src/Frontend/Components/InputElements/__tests__/AutoComplete.test.tsx b/src/Frontend/Components/InputElements/__tests__/AutoComplete.test.tsx
deleted file mode 100644
index 91f64f4d..00000000
--- a/src/Frontend/Components/InputElements/__tests__/AutoComplete.test.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { render, screen } from '@testing-library/react';
-import { ChangeEvent } from 'react';
-
-import { expectElementsInAutoCompleteAndSelectFirst } from '../../../test-helpers/general-test-helpers';
-import { doNothing } from '../../../util/do-nothing';
-import { AutoComplete } from '../AutoComplete';
-
-describe('The AutoComplete', () => {
-  it('renders text and label', () => {
-    const testLicenseNames = ['MIT', 'GPL'];
-    render(
-      <AutoComplete
-        title={'Test Title'}
-        options={testLicenseNames}
-        inputValue={''}
-        showTextBold={false}
-        handleChange={
-          doNothing as unknown as (
-            event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
-          ) => void
-        }
-      />,
-    );
-
-    expect(screen.queryAllByText('Test Title')).toHaveLength(2);
-
-    expectElementsInAutoCompleteAndSelectFirst(screen, testLicenseNames);
-  });
-
-  it('renders end adornment', () => {
-    const testLicenseNames = ['MIT', 'GPL'];
-    render(
-      <AutoComplete
-        title={'Test Title'}
-        options={testLicenseNames}
-        inputValue={''}
-        showTextBold={false}
-        handleChange={
-          doNothing as unknown as (
-            event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
-          ) => void
-        }
-        endAdornmentText={'Adornment Text'}
-      />,
-    );
-
-    screen.getByText('Adornment Text');
-  });
-});
diff --git a/src/Frontend/Components/LocatorPopup/__tests__/LocatorPopup.test.tsx b/src/Frontend/Components/LocatorPopup/__tests__/LocatorPopup.test.tsx
index a399218e..7342f54e 100644
--- a/src/Frontend/Components/LocatorPopup/__tests__/LocatorPopup.test.tsx
+++ b/src/Frontend/Components/LocatorPopup/__tests__/LocatorPopup.test.tsx
@@ -27,7 +27,7 @@ import {
   createTestAppStore,
   renderComponentWithStore,
 } from '../../../test-helpers/render-component-with-store';
-import { getLicenseNames, LocatorPopup } from '../LocatorPopup';
+import { LocatorPopup } from '../LocatorPopup';
 
 describe('Locator popup ', () => {
   jest.useFakeTimers();
@@ -46,9 +46,7 @@ describe('Locator popup ', () => {
     ).not.toBeChecked();
     expect(screen.getByLabelText('Criticality')).toBeInTheDocument();
     expect(screen.getByText('Any')).toBeInTheDocument();
-    expect(
-      screen.getByRole('combobox', { name: 'License' }),
-    ).toBeInTheDocument();
+    expect(screen.getByLabelText('auto complete')).toBeInTheDocument();
   });
 
   it('selects criticality values using the dropdown', () => {
@@ -309,26 +307,6 @@ describe('Locator popup ', () => {
   });
 });
 
-describe('getLicenseNamesFromExternalAttributions', () => {
-  it('collects the correct license names', () => {
-    const testExternalMITAttribution: PackageInfo = {
-      licenseName: 'MIT',
-    };
-    const testExternalApacheAttribution: PackageInfo = {
-      licenseName: 'Apache-2.0',
-    };
-
-    const testExternalAttributions: Attributions = {
-      uuid_1: testExternalMITAttribution,
-      uuid_2: testExternalApacheAttribution,
-    };
-
-    const licenseNames = getLicenseNames(testExternalAttributions);
-    const expectedLicenseNames = ['MIT', 'Apache-2.0'];
-    expect(licenseNames).toEqual(expectedLicenseNames);
-  });
-});
-
 describe('locateResourcesByCriticalityAndLicense', () => {
   const testAttributions: Attributions = {
     MITHighAttribution: {
@@ -348,7 +326,7 @@ describe('locateResourcesByCriticalityAndLicense', () => {
       criticality: Criticality.Medium,
     },
     GPLMediumAttribution: {
-      licenseName: 'General Public License',
+      licenseName: 'GPL',
       criticality: Criticality.Medium,
       packageVersion: '2.0',
     },
@@ -572,7 +550,7 @@ describe('locateResourcesByCriticalityAndLicense', () => {
     testStore.dispatch(setFrequentLicenses(testFrequentLicenses));
 
     const criticality = SelectedCriticality.High;
-    const licenseNames = new Set(['General Public License']);
+    const licenseNames = new Set(['GPL']);
     testStore.dispatch(
       setLocatePopupFilters({
         selectedCriticality: criticality,
diff --git a/src/Frontend/Components/ProcessPopup/__tests__/ProcessPopup.test.tsx b/src/Frontend/Components/ProcessPopup/__tests__/ProcessPopup.test.tsx
index cabcd565..0dddb970 100644
--- a/src/Frontend/Components/ProcessPopup/__tests__/ProcessPopup.test.tsx
+++ b/src/Frontend/Components/ProcessPopup/__tests__/ProcessPopup.test.tsx
@@ -2,11 +2,11 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { faker } from '@faker-js/faker';
 import { act, screen, within } from '@testing-library/react';
 import { IpcRendererEvent } from 'electron';
 import { noop } from 'lodash';
 
+import { faker } from '../../../../shared/Faker';
 import { AllowedFrontendChannels } from '../../../../shared/ipc-channels';
 import { ElectronAPI, Log } from '../../../../shared/shared-types';
 import { text } from '../../../../shared/text';
diff --git a/src/Frontend/Components/ProgressBar/__tests__/FolderProgressBar.test.tsx b/src/Frontend/Components/ProgressBar/__tests__/FolderProgressBar.test.tsx
index 16d7e594..cb47d546 100644
--- a/src/Frontend/Components/ProgressBar/__tests__/FolderProgressBar.test.tsx
+++ b/src/Frontend/Components/ProgressBar/__tests__/FolderProgressBar.test.tsx
@@ -2,184 +2,73 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { act, fireEvent, screen } from '@testing-library/react';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 
-import {
-  Attributions,
-  DiscreteConfidence,
-  PackageInfo,
-  Resources,
-  ResourcesToAttributions,
-} from '../../../../shared/shared-types';
-import {
-  setAttributionBreakpoints,
-  setExternalData,
-  setFilesWithChildren,
-  setManualData,
-  setResources,
-} from '../../../state/actions/resource-actions/all-views-simple-actions';
-import { setResolvedExternalAttributions } from '../../../state/actions/resource-actions/audit-view-simple-actions';
-import {
-  createTestAppStore,
-  renderComponentWithStore,
-} from '../../../test-helpers/render-component-with-store';
+import { setVariable } from '../../../state/actions/variables-actions/variables-actions';
+import { renderComponentWithStore } from '../../../test-helpers/render-component-with-store';
+import { ProgressBarData } from '../../../types/types';
+import { WORKER_REDUX_KEYS } from '../../../web-workers/use-signals-worker';
 import { FolderProgressBar } from '../FolderProgressBar';
 
 describe('FolderProgressBar', () => {
   jest.useFakeTimers();
-  it('renders correctly', () => {
-    const testResources: Resources = {
-      folder1: { file1: 1, file2: 1 },
-      folder2: { file1: 1, file2: 1 },
-      file1: 1,
-      file2: 1,
-    };
 
-    const testManualAttributionUuid_1 = '4d9f0b16-fbff-11ea-adc1-0242ac120002';
-    const testManualAttributionUuid_2 = 'b5da73d4-f400-11ea-adc1-0242ac120002';
-    const testExternalAttributionUuid = 'b5da73d4-f400-11ea-adc1-0242ac120003';
-    const testPackageInfo: PackageInfo = {
-      attributionConfidence: DiscreteConfidence.High,
-      packageVersion: '1.0',
-      packageName: 'test Package',
-      licenseText: ' test License text',
-    };
-    const secondTestPackageInfo: PackageInfo = {
-      packageVersion: '2.0',
-      packageName: 'not assigned test Package',
-      licenseText: ' test not assigned License text',
-      preSelected: true,
-    };
-    const testManualAttributions: Attributions = {
-      [testManualAttributionUuid_1]: testPackageInfo,
-      [testManualAttributionUuid_2]: secondTestPackageInfo,
-    };
-
-    const testExternalAttributions: Attributions = {
-      [testExternalAttributionUuid]: {
-        packageName: 'React',
-        packageVersion: '17.0.0',
-      },
-    };
-
-    const testResourcesToManualAttributions: ResourcesToAttributions = {
-      '/folder1/': [testManualAttributionUuid_1],
-      '/folder2/file1': [testManualAttributionUuid_1],
-      '/file1': [testManualAttributionUuid_2],
-    };
-    const testResourcesToExternalAttributions: ResourcesToAttributions = {
-      '/folder1/file2': [testExternalAttributionUuid],
-      '/folder2/file2': [testExternalAttributionUuid],
-    };
-
-    const testStore = createTestAppStore();
-
-    testStore.dispatch(setResources(testResources));
-    testStore.dispatch(
-      setManualData(testManualAttributions, testResourcesToManualAttributions),
-    );
-    testStore.dispatch(
-      setExternalData(
-        testExternalAttributions,
-        testResourcesToExternalAttributions,
-      ),
-    );
-    testStore.dispatch(setResolvedExternalAttributions(new Set()));
-    testStore.dispatch(setAttributionBreakpoints(new Set()));
-    testStore.dispatch(setFilesWithChildren(new Set()));
-
-    renderComponentWithStore(<FolderProgressBar resourceId="/folder2/" />, {
-      store: testStore,
+  it('renders correctly when folder has no attributions', async () => {
+    renderComponentWithStore(<FolderProgressBar />, {
+      actions: [
+        setVariable<ProgressBarData>(WORKER_REDUX_KEYS.FOLDER_PROGRESS_DATA, {
+          fileCount: 2,
+          filesWithHighlyCriticalExternalAttributionsCount: 1,
+          filesWithMediumCriticalExternalAttributionsCount: 1,
+          filesWithManualAttributionCount: 1,
+          filesWithOnlyExternalAttributionCount: 1,
+          filesWithOnlyPreSelectedAttributionCount: 0,
+          resourcesWithMediumCriticalExternalAttributions: [],
+          resourcesWithNonInheritedExternalAttributionOnly: [],
+          resourcesWithHighlyCriticalExternalAttributions: [],
+        }),
+      ],
     });
 
-    const progressBar = screen.getByLabelText('FolderProgressBar');
-    fireEvent.mouseOver(progressBar);
-    act(() => {
-      jest.runAllTimers();
+    await userEvent.hover(screen.getByLabelText('FolderProgressBar'), {
+      advanceTimers: jest.runOnlyPendingTimersAsync,
     });
+
     expect(screen.getByText(/Number of files: 2/)).toBeInTheDocument();
+    expect(screen.getByText(/Files with attributions: 1/)).toBeInTheDocument();
     expect(
-      screen.getByText(/Files with attributions: 1/) &&
-        screen.getByText(/Files with only pre-selected attributions: 0/) &&
-        screen.getByText(/Files with only signals: 1/),
-    ).toBeDefined();
+      screen.getByText(/Files with only pre-selected attributions: 0/),
+    ).toBeInTheDocument();
+    expect(screen.getByText(/Files with only signals: 1/)).toBeInTheDocument();
   });
 
-  it('renders correctly if folder has an attribution', () => {
-    const testResources: Resources = {
-      folder1: { file1: 1, file2: 1 },
-      folder2: { file1: 1, file2: 1 },
-      file1: 1,
-      file2: 1,
-    };
-
-    const testManualAttributionUuid_1 = '4d9f0b16-fbff-11ea-adc1-0242ac120002';
-    const testManualAttributionUuid_2 = 'b5da73d4-f400-11ea-adc1-0242ac120002';
-    const testExternalAttributionUuid = 'b5da73d4-f400-11ea-adc1-0242ac120003';
-    const testPackageInfo: PackageInfo = {
-      attributionConfidence: DiscreteConfidence.High,
-      packageVersion: '1.0',
-      packageName: 'test Package',
-      licenseText: ' test License text',
-    };
-    const secondTestPackageInfo: PackageInfo = {
-      packageVersion: '2.0',
-      packageName: 'not assigned test Package',
-      licenseText: ' test not assigned License text',
-      preSelected: true,
-    };
-    const testManualAttributions: Attributions = {
-      [testManualAttributionUuid_1]: testPackageInfo,
-      [testManualAttributionUuid_2]: secondTestPackageInfo,
-    };
-
-    const testExternalAttributions: Attributions = {
-      [testExternalAttributionUuid]: {
-        packageName: 'React',
-        packageVersion: '17.0.0',
-      },
-    };
-
-    const testResourcesToManualAttributions: ResourcesToAttributions = {
-      '/folder1/': [testManualAttributionUuid_1],
-      '/folder2/file1': [testManualAttributionUuid_1],
-      '/file1': [testManualAttributionUuid_2],
-    };
-    const testResourcesToExternalAttributions: ResourcesToAttributions = {
-      '/folder1/file2': [testExternalAttributionUuid],
-      '/folder2/file2': [testExternalAttributionUuid],
-    };
-
-    const testStore = createTestAppStore();
-
-    testStore.dispatch(setResources(testResources));
-    testStore.dispatch(
-      setManualData(testManualAttributions, testResourcesToManualAttributions),
-    );
-    testStore.dispatch(
-      setExternalData(
-        testExternalAttributions,
-        testResourcesToExternalAttributions,
-      ),
-    );
-    testStore.dispatch(setResolvedExternalAttributions(new Set()));
-    testStore.dispatch(setAttributionBreakpoints(new Set()));
-    testStore.dispatch(setFilesWithChildren(new Set()));
-
-    renderComponentWithStore(<FolderProgressBar resourceId="/folder1/" />, {
-      store: testStore,
+  it('renders correctly if folder has an attribution', async () => {
+    renderComponentWithStore(<FolderProgressBar />, {
+      actions: [
+        setVariable<ProgressBarData>(WORKER_REDUX_KEYS.FOLDER_PROGRESS_DATA, {
+          fileCount: 2,
+          filesWithHighlyCriticalExternalAttributionsCount: 1,
+          filesWithMediumCriticalExternalAttributionsCount: 1,
+          filesWithManualAttributionCount: 2,
+          filesWithOnlyExternalAttributionCount: 0,
+          filesWithOnlyPreSelectedAttributionCount: 0,
+          resourcesWithMediumCriticalExternalAttributions: [],
+          resourcesWithNonInheritedExternalAttributionOnly: [],
+          resourcesWithHighlyCriticalExternalAttributions: [],
+        }),
+      ],
     });
 
-    const progressBar = screen.getByLabelText('FolderProgressBar');
-    fireEvent.mouseOver(progressBar);
-    act(() => {
-      jest.runAllTimers();
+    await userEvent.hover(screen.getByLabelText('FolderProgressBar'), {
+      advanceTimers: jest.runOnlyPendingTimersAsync,
     });
+
     expect(screen.getByText(/Number of files: 2/)).toBeInTheDocument();
+    expect(screen.getByText(/Files with attributions: 2/)).toBeInTheDocument();
     expect(
-      screen.getByText(/Files with attributions: 2/) &&
-        screen.getByText(/Files with only pre-selected attributions: 0/) &&
-        screen.getByText(/Files with only signals: 0/),
-    ).toBeDefined();
+      screen.getByText(/Files with only pre-selected attributions: 0/),
+    ).toBeInTheDocument();
+    expect(screen.getByText(/Files with only signals: 0/)).toBeInTheDocument();
   });
 });
diff --git a/src/Frontend/Components/ProgressBar/__tests__/TopProgressBar.test.tsx b/src/Frontend/Components/ProgressBar/__tests__/TopProgressBar.test.tsx
index bee6fe6e..47d39ff0 100644
--- a/src/Frontend/Components/ProgressBar/__tests__/TopProgressBar.test.tsx
+++ b/src/Frontend/Components/ProgressBar/__tests__/TopProgressBar.test.tsx
@@ -2,238 +2,84 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { act, fireEvent, screen } from '@testing-library/react';
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 
-import {
-  Attributions,
-  Criticality,
-  DiscreteConfidence,
-  PackageInfo,
-  Resources,
-  ResourcesToAttributions,
-} from '../../../../shared/shared-types';
-import {
-  setAttributionBreakpoints,
-  setExternalData,
-  setFilesWithChildren,
-  setManualData,
-  setResources,
-} from '../../../state/actions/resource-actions/all-views-simple-actions';
-import { setResolvedExternalAttributions } from '../../../state/actions/resource-actions/audit-view-simple-actions';
-import {
-  createTestAppStore,
-  renderComponentWithStore,
-} from '../../../test-helpers/render-component-with-store';
+import { setVariable } from '../../../state/actions/variables-actions/variables-actions';
+import { renderComponentWithStore } from '../../../test-helpers/render-component-with-store';
+import { ProgressBarData } from '../../../types/types';
+import { WORKER_REDUX_KEYS } from '../../../web-workers/use-signals-worker';
 import { TopProgressBar } from '../TopProgressBar';
 
 describe('TopProgressBar', () => {
   jest.useFakeTimers();
-  it('TopProgressBar renders', () => {
-    const testResources: Resources = {
-      folder1: { file1: 1, file2: 1 },
-      folder2: { file1: 1, file2: 1 },
-      file1: 1,
-      file2: 1,
-    };
-
-    const testManualAttributionUuid_1 = '4d9f0b16-fbff-11ea-adc1-0242ac120002';
-    const testManualAttributionUuid_2 = 'b5da73d4-f400-11ea-adc1-0242ac120002';
-    const testExternalAttributionUuid = 'b5da73d4-f400-11ea-adc1-0242ac120003';
-    const testPackageInfo: PackageInfo = {
-      attributionConfidence: DiscreteConfidence.High,
-      packageVersion: '1.0',
-      packageName: 'test Package',
-      licenseText: ' test License text',
-    };
-    const secondTestPackageInfo: PackageInfo = {
-      packageVersion: '2.0',
-      packageName: 'not assigned test Package',
-      licenseText: ' test not assigned License text',
-      preSelected: true,
-    };
-    const testManualAttributions: Attributions = {
-      [testManualAttributionUuid_1]: testPackageInfo,
-      [testManualAttributionUuid_2]: secondTestPackageInfo,
-    };
-
-    const testExternalAttributions: Attributions = {
-      [testExternalAttributionUuid]: {
-        packageName: 'React',
-        packageVersion: '17.0.0',
-      },
-    };
-
-    const testResourcesToManualAttributions: ResourcesToAttributions = {
-      '/folder1/': [testManualAttributionUuid_1],
-      '/folder2/file1': [testManualAttributionUuid_1],
-      '/file1': [testManualAttributionUuid_2],
-    };
-    const testResourcesToExternalAttributions: ResourcesToAttributions = {
-      '/folder1/file2': [testExternalAttributionUuid],
-      '/folder2/file2': [testExternalAttributionUuid],
-    };
-
-    const testStore = createTestAppStore();
-
-    testStore.dispatch(setResources(testResources));
-    testStore.dispatch(
-      setManualData(testManualAttributions, testResourcesToManualAttributions),
-    );
-    testStore.dispatch(
-      setExternalData(
-        testExternalAttributions,
-        testResourcesToExternalAttributions,
-      ),
-    );
-    testStore.dispatch(setResolvedExternalAttributions(new Set()));
-    testStore.dispatch(setAttributionBreakpoints(new Set()));
-    testStore.dispatch(setFilesWithChildren(new Set()));
 
+  it('renders regular progress bar', async () => {
     renderComponentWithStore(<TopProgressBar />, {
-      store: testStore,
+      actions: [
+        setVariable<ProgressBarData>(WORKER_REDUX_KEYS.OVERALL_PROGRESS_DATA, {
+          fileCount: 6,
+          filesWithHighlyCriticalExternalAttributionsCount: 1,
+          filesWithMediumCriticalExternalAttributionsCount: 1,
+          filesWithManualAttributionCount: 3,
+          filesWithOnlyExternalAttributionCount: 1,
+          filesWithOnlyPreSelectedAttributionCount: 1,
+          resourcesWithMediumCriticalExternalAttributions: [],
+          resourcesWithNonInheritedExternalAttributionOnly: [],
+          resourcesWithHighlyCriticalExternalAttributions: [],
+        }),
+      ],
     });
 
-    const progressBar = screen.getByLabelText('TopProgressBar');
-    fireEvent.mouseOver(progressBar);
-    act(() => {
-      jest.runAllTimers();
+    await userEvent.hover(screen.getByLabelText('TopProgressBar'), {
+      advanceTimers: jest.runOnlyPendingTimersAsync,
     });
+
     expect(screen.getByText(/Number of files: 6/)).toBeInTheDocument();
+    expect(screen.getByText(/Files with attributions: 3/)).toBeInTheDocument();
     expect(
-      screen.getByText(/Files with attributions: 3/) &&
-        screen.getByText(/Files with only pre-selected attributions: 1/) &&
-        screen.getByText(/Files with only signals: 1/),
-    ).toBeDefined();
+      screen.getByText(/Files with only pre-selected attributions: 1/),
+    ).toBeInTheDocument();
+    expect(screen.getByText(/Files with only signals: 1/)).toBeInTheDocument();
   });
 
-  it('TopProgressBar renders in criticality view', () => {
-    const testResources: Resources = {
-      folder1: { file1: 1, file2: 1 },
-      folder2: { file1: 1, file2: 1 },
-      folder3: { file1: 1, file2: 1 },
-      file1: 1,
-      file2: 1,
-    };
-
-    const testManualAttributionUuid_1 = '4d9f0b16-fbff-11ea-adc1-0242ac120002';
-    const testManualAttributionUuid_2 = 'b5da73d4-f400-11ea-adc1-0242ac120002';
-
-    const testPackageInfo: PackageInfo = {
-      attributionConfidence: DiscreteConfidence.High,
-      packageVersion: '1.0',
-      packageName: 'test Package',
-      licenseText: ' test License text',
-    };
-    const secondTestPackageInfo: PackageInfo = {
-      packageVersion: '2.0',
-      packageName: 'not assigned test Package',
-      licenseText: ' test not assigned License text',
-      preSelected: true,
-    };
-    const testManualAttributions: Attributions = {
-      [testManualAttributionUuid_1]: testPackageInfo,
-      [testManualAttributionUuid_2]: secondTestPackageInfo,
-    };
-
-    const testHighlyCriticalExternalAttributionUuid =
-      'ed8d3fa1-a53c-42c9-8e48-8f02382b3413';
-    const testMediumCriticalExternalAttributionUuid =
-      'c4b2fd5c-b5e6-4835-8d21-863f89169892';
-    const testNonCriticalExternalAttributionUuid =
-      'a79fbc27-5fe2-4121-b0d5-365dcab151ff';
-    const testNotRelevantForCriticalViewAttributionUuid =
-      'a09f455f-efe3-4347-bfd1-cec0f040a96f';
-
-    const testHighlyCriticalExternalAttributionPackageInfo: PackageInfo = {
-      attributionConfidence: DiscreteConfidence.High,
-      packageVersion: '1.0',
-      packageName: 'highly critical package',
-      licenseText: 'license text of highly critical package',
-      criticality: Criticality.High,
-    };
-
-    const testMediumCriticalExternalAttributionPackageInfo: PackageInfo = {
-      attributionConfidence: DiscreteConfidence.High,
-      packageVersion: '2.0',
-      packageName: 'medium critical package',
-      licenseText: 'license text of medium critical package',
-      criticality: Criticality.Medium,
-    };
-
-    const testNonCriticalExternalAttributionPackageInfo: PackageInfo = {
-      attributionConfidence: DiscreteConfidence.High,
-      packageVersion: '3.0',
-      packageName: 'non critical package',
-      licenseText: 'license text of non critical package',
-    };
-
-    const testNotRelevantForCriticalViewAttributionPackageInfo: PackageInfo = {
-      attributionConfidence: DiscreteConfidence.High,
-      packageVersion: '4.0',
-      packageName: 'not relevant for critical view package',
-      licenseText: 'license text of not relevant for critical view package',
-    };
-
-    const testExternalAttributions: Attributions = {
-      [testHighlyCriticalExternalAttributionUuid]:
-        testHighlyCriticalExternalAttributionPackageInfo,
-      [testMediumCriticalExternalAttributionUuid]:
-        testMediumCriticalExternalAttributionPackageInfo,
-      [testNonCriticalExternalAttributionUuid]:
-        testNonCriticalExternalAttributionPackageInfo,
-      [testNotRelevantForCriticalViewAttributionUuid]:
-        testNotRelevantForCriticalViewAttributionPackageInfo,
-    };
-
-    const testResourcesToManualAttributions: ResourcesToAttributions = {
-      '/folder1/': [testManualAttributionUuid_1],
-      '/folder2/file1': [testManualAttributionUuid_1],
-      '/file1': [testManualAttributionUuid_2],
-    };
-    const testResourcesToExternalAttributions: ResourcesToAttributions = {
-      '/folder1/file2': [testNotRelevantForCriticalViewAttributionUuid],
-      '/folder2/file2': [testNonCriticalExternalAttributionUuid],
-      '/folder3/file1': [testMediumCriticalExternalAttributionUuid],
-      '/folder3/file2': [testHighlyCriticalExternalAttributionUuid],
-    };
-
-    const testStore = createTestAppStore();
-
-    testStore.dispatch(setResources(testResources));
-    testStore.dispatch(
-      setManualData(testManualAttributions, testResourcesToManualAttributions),
-    );
-    testStore.dispatch(
-      setExternalData(
-        testExternalAttributions,
-        testResourcesToExternalAttributions,
-      ),
-    );
-    testStore.dispatch(setResolvedExternalAttributions(new Set()));
-    testStore.dispatch(setAttributionBreakpoints(new Set()));
-    testStore.dispatch(setFilesWithChildren(new Set()));
-
+  it('renders in criticality view', async () => {
     renderComponentWithStore(<TopProgressBar />, {
-      store: testStore,
+      actions: [
+        setVariable<ProgressBarData>(WORKER_REDUX_KEYS.OVERALL_PROGRESS_DATA, {
+          fileCount: 6,
+          filesWithHighlyCriticalExternalAttributionsCount: 1,
+          filesWithMediumCriticalExternalAttributionsCount: 1,
+          filesWithManualAttributionCount: 1,
+          filesWithOnlyExternalAttributionCount: 3,
+          filesWithOnlyPreSelectedAttributionCount: 1,
+          resourcesWithMediumCriticalExternalAttributions: [],
+          resourcesWithNonInheritedExternalAttributionOnly: [],
+          resourcesWithHighlyCriticalExternalAttributions: [],
+        }),
+      ],
+    });
+
+    await userEvent.click(screen.getByRole('checkbox'), {
+      advanceTimers: jest.runOnlyPendingTimersAsync,
     });
-    const criticalityStateSwitch = screen.getByLabelText(
-      'CriticalityStateSwitch',
-    ).children[0] as HTMLElement;
-    fireEvent.click(criticalityStateSwitch);
-    const progressBar = screen.getByLabelText('TopProgressBar');
-    fireEvent.mouseOver(progressBar);
-    act(() => {
-      jest.runAllTimers();
+    await userEvent.hover(screen.getByLabelText('TopProgressBar'), {
+      advanceTimers: jest.runOnlyPendingTimersAsync,
     });
+
     expect(
       screen.getByText(
         /Number of files with signals and no attributions \(3\)/,
       ),
     ).toBeInTheDocument();
     expect(
-      screen.getByText(/containing highly-critical signals: 1/) &&
-        screen.getByText(/containing medium-critical signals: 1/) &&
-        screen.getByText(/containing only non-critical signals: 1/),
-    ).toBeDefined();
+      screen.getByText(/containing highly-critical signals: 1/),
+    ).toBeInTheDocument();
+    expect(
+      screen.getByText(/containing medium-critical signals: 1/),
+    ).toBeInTheDocument();
+    expect(
+      screen.getByText(/containing only non-critical signals: 1/),
+    ).toBeInTheDocument();
   });
 });
diff --git a/src/Frontend/Components/ResourceDetailsViewer/__tests__/ResourceDetailsViewer.test.tsx b/src/Frontend/Components/ResourceDetailsViewer/__tests__/ResourceDetailsViewer.test.tsx
index 527c5945..a6b2abb6 100644
--- a/src/Frontend/Components/ResourceDetailsViewer/__tests__/ResourceDetailsViewer.test.tsx
+++ b/src/Frontend/Components/ResourceDetailsViewer/__tests__/ResourceDetailsViewer.test.tsx
@@ -192,41 +192,6 @@ describe('The ResourceDetailsViewer', () => {
     expect(screen.getByText('JQuery'));
   });
 
-  it('renders Contained External Packages', () => {
-    const { store } = renderComponentWithStore(<ResourceDetailsViewer />);
-    const externalAttributions: Attributions = {
-      uuid_2: {
-        packageName: 'JQuery',
-        packageVersion: '1.0',
-      },
-      uuid_1: {
-        packageName: 'Another Package',
-      },
-    };
-    const resourcesToExternalAttributions: ResourcesToAttributions = {
-      '/test_id/': ['uuid_1'],
-      '/test_id/subdirectory': ['uuid_2'],
-    };
-    act(() => {
-      store.dispatch(
-        loadFromFile(
-          getParsedInputFileEnrichedWithTestData({
-            resources: { test_id: { subdirectory: 1 } },
-            externalAttributions,
-            resourcesToExternalAttributions,
-          }),
-        ),
-      );
-      store.dispatch(setSelectedResourceId('/test_id/'));
-      store.dispatch(
-        setTemporaryDisplayPackageInfo(EMPTY_DISPLAY_PACKAGE_INFO),
-      );
-    });
-
-    expect(screen.getByText('Signals in Folder Content'));
-    expect(screen.getByText('JQuery, 1.0'));
-  });
-
   it('selects an external package and a manual package, showing the right info', () => {
     const manualAttributions = {
       uuid_1: testTemporaryDisplayPackageInfo,
diff --git a/src/Frontend/test-helpers/package-panel-helpers.ts b/src/Frontend/test-helpers/package-panel-helpers.ts
index c5fbef27..3b233260 100644
--- a/src/Frontend/test-helpers/package-panel-helpers.ts
+++ b/src/Frontend/test-helpers/package-panel-helpers.ts
@@ -2,19 +2,7 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { fireEvent, getByText, Screen } from '@testing-library/react';
-
-import { getPackagePanel } from './general-test-helpers';
-
-export function expectPackageInPackagePanel(
-  screen: Screen,
-  packageName: string,
-  packagePanelName: string,
-): void {
-  const packagesPanel = getPackagePanel(screen, packagePanelName);
-  // eslint-disable-next-line testing-library/prefer-screen-queries
-  expect(getByText(packagesPanel, packageName)).toBeInTheDocument();
-}
+import { fireEvent, Screen } from '@testing-library/react';
 
 export function clickOnTab(screen: Screen, tabLabel: string): void {
   fireEvent.click(screen.getByLabelText(tabLabel));
diff --git a/src/Frontend/test-helpers/render-component-with-store.tsx b/src/Frontend/test-helpers/render-component-with-store.tsx
index b2d91b00..b6a336fc 100644
--- a/src/Frontend/test-helpers/render-component-with-store.tsx
+++ b/src/Frontend/test-helpers/render-component-with-store.tsx
@@ -21,8 +21,13 @@ export function createTestAppStore(): EnhancedTestStore {
 
 export function renderComponentWithStore(
   component: ReactElement,
-  { store = createTestAppStore() }: { store?: EnhancedTestStore } = {},
+  {
+    store = createTestAppStore(),
+    actions,
+  }: { store?: EnhancedTestStore; actions?: Array<AnyAction> } = {},
 ) {
+  actions?.forEach(store.dispatch);
+
   return {
     store,
     ...render(component, {
diff --git a/src/Frontend/util/__tests__/maybe-pluralize.test.ts b/src/Frontend/util/__tests__/maybe-pluralize.test.ts
new file mode 100644
index 00000000..7afeb75d
--- /dev/null
+++ b/src/Frontend/util/__tests__/maybe-pluralize.test.ts
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { maybePluralize } from '../maybe-pluralize';
+
+describe('maybePluralize', () => {
+  it('uses provided plural', () => {
+    expect(maybePluralize(2, 'Deputy', { pluralNoun: 'Deputies' })).toEqual(
+      '2 Deputies',
+    );
+  });
+
+  it('makes plural by adding s if no plural is provided', () => {
+    expect(maybePluralize(2, 'User')).toEqual('2 Users');
+  });
+
+  it('uses plural when value is zero', () => {
+    expect(maybePluralize(0, 'User')).toEqual('0 Users');
+  });
+
+  it('does not add suffix when value is 1', () => {
+    expect(maybePluralize(1, 'User')).toEqual('1 User');
+  });
+});
diff --git a/src/Frontend/util/__tests__/use-user-setting.test.ts b/src/Frontend/util/__tests__/use-user-setting.test.ts
index 13547ba3..d4554793 100644
--- a/src/Frontend/util/__tests__/use-user-setting.test.ts
+++ b/src/Frontend/util/__tests__/use-user-setting.test.ts
@@ -2,9 +2,9 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { faker } from '@faker-js/faker';
 import { act, waitFor } from '@testing-library/react';
 
+import { faker } from '../../../shared/Faker';
 import { ElectronAPI } from '../../../shared/shared-types';
 import { renderHook } from '../../test-helpers/render-component-with-store';
 import { useUserSetting } from '../use-user-setting';
diff --git a/src/Frontend/util/__tests__/use-variable.test.ts b/src/Frontend/util/__tests__/use-variable.test.ts
index 77d4346c..1b577700 100644
--- a/src/Frontend/util/__tests__/use-variable.test.ts
+++ b/src/Frontend/util/__tests__/use-variable.test.ts
@@ -2,9 +2,9 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { faker } from '@faker-js/faker';
 import { act } from '@testing-library/react';
 
+import { faker } from '../../../shared/Faker';
 import { renderHook } from '../../test-helpers/render-component-with-store';
 import { useVariable } from '../use-variable';
 
diff --git a/src/Frontend/web-workers/__tests__/signals-worker.test.ts b/src/Frontend/web-workers/__tests__/signals-worker.test.ts
new file mode 100644
index 00000000..0575f725
--- /dev/null
+++ b/src/Frontend/web-workers/__tests__/signals-worker.test.ts
@@ -0,0 +1,181 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { faker } from '../../../shared/Faker';
+import { SignalsWorker, SignalsWorkerOutput } from '../signals-worker';
+
+describe('SignalsWorker', () => {
+  it('dispatches autocomplete signals when dependencies are met', () => {
+    const dispatch = jest.fn();
+    const worker = new SignalsWorker(dispatch, {
+      externalData: faker.opossum.externalAttributionData(),
+      manualData: faker.opossum.manualAttributionData(),
+      resolvedExternalAttributions: new Set<string>(),
+      sources: faker.opossum.externalAttributionSources(),
+    });
+
+    worker.processInput({
+      name: 'resourceId',
+      data: faker.opossum.resourceName(),
+    });
+
+    expect(dispatch).toHaveBeenCalledWith<[SignalsWorkerOutput]>({
+      name: 'autocompleteSignals',
+      data: expect.any(Array),
+    });
+  });
+
+  it('does not dispatch autocomplete signals when dependencies are missing', () => {
+    const dispatch = jest.fn();
+    new SignalsWorker(dispatch, {
+      externalData: faker.opossum.externalAttributionData(),
+      manualData: faker.opossum.manualAttributionData(),
+      resolvedExternalAttributions: new Set<string>(),
+      sources: faker.opossum.externalAttributionSources(),
+    });
+
+    expect(dispatch).not.toHaveBeenCalledWith<[SignalsWorkerOutput]>({
+      name: 'autocompleteSignals',
+      data: expect.any(Array),
+    });
+  });
+
+  it('dispatches attributions in folder content when dependencies are met', () => {
+    const dispatch = jest.fn();
+    const worker = new SignalsWorker(dispatch, {
+      manualData: faker.opossum.manualAttributionData(),
+    });
+
+    worker.processInput({
+      name: 'resourceId',
+      data: faker.opossum.resourceName(),
+    });
+
+    expect(dispatch).toHaveBeenCalledWith<[SignalsWorkerOutput]>({
+      name: 'attributionsInFolderContent',
+      data: expect.any(Object),
+    });
+  });
+
+  it('does not dispatch attributions in folder content when dependencies are missing', () => {
+    const dispatch = jest.fn();
+    new SignalsWorker(dispatch, {
+      manualData: faker.opossum.manualAttributionData(),
+    });
+
+    expect(dispatch).not.toHaveBeenCalledWith<[SignalsWorkerOutput]>({
+      name: 'attributionsInFolderContent',
+      data: expect.any(Object),
+    });
+  });
+
+  it('dispatches signals in folder content when dependencies are met', () => {
+    const dispatch = jest.fn();
+    const worker = new SignalsWorker(dispatch, {
+      externalData: faker.opossum.externalAttributionData(),
+      resolvedExternalAttributions: new Set<string>(),
+      attributionsToHashes: {},
+    });
+
+    worker.processInput({
+      name: 'resourceId',
+      data: faker.opossum.resourceName(),
+    });
+
+    expect(dispatch).toHaveBeenCalledWith<[SignalsWorkerOutput]>({
+      name: 'signalsInFolderContent',
+      data: expect.any(Object),
+    });
+  });
+
+  it('does not dispatch signals in folder content when dependencies are missing', () => {
+    const dispatch = jest.fn();
+    new SignalsWorker(dispatch, {
+      externalData: faker.opossum.externalAttributionData(),
+      resolvedExternalAttributions: new Set<string>(),
+      attributionsToHashes: {},
+    });
+
+    expect(dispatch).not.toHaveBeenCalledWith<[SignalsWorkerOutput]>({
+      name: 'signalsInFolderContent',
+      data: expect.any(Object),
+    });
+  });
+
+  it('dispatches overall progress data when dependencies are met', () => {
+    const dispatch = jest.fn();
+    const worker = new SignalsWorker(dispatch, {
+      attributionBreakpoints: new Set<string>(),
+      externalData: faker.opossum.externalAttributionData(),
+      filesWithChildren: new Set<string>(),
+      manualData: faker.opossum.manualAttributionData(),
+      resolvedExternalAttributions: new Set<string>(),
+    });
+
+    worker.processInput({
+      name: 'resources',
+      data: faker.opossum.resources(),
+    });
+
+    expect(dispatch).toHaveBeenCalledWith<[SignalsWorkerOutput]>({
+      name: 'overallProgressData',
+      data: expect.any(Object),
+    });
+  });
+
+  it('does not dispatch overall progress data when dependencies are missing', () => {
+    const dispatch = jest.fn();
+    new SignalsWorker(dispatch, {
+      attributionBreakpoints: new Set<string>(),
+      externalData: faker.opossum.externalAttributionData(),
+      filesWithChildren: new Set<string>(),
+      manualData: faker.opossum.manualAttributionData(),
+      resolvedExternalAttributions: new Set<string>(),
+    });
+
+    expect(dispatch).not.toHaveBeenCalledWith<[SignalsWorkerOutput]>({
+      name: 'overallProgressData',
+      data: expect.any(Object),
+    });
+  });
+
+  it('dispatches folder progress data when dependencies are met', () => {
+    const dispatch = jest.fn();
+    const worker = new SignalsWorker(dispatch, {
+      attributionBreakpoints: new Set<string>(),
+      externalData: faker.opossum.externalAttributionData(),
+      filesWithChildren: new Set<string>(),
+      manualData: faker.opossum.manualAttributionData(),
+      resolvedExternalAttributions: new Set<string>(),
+      resources: faker.opossum.resources(),
+    });
+
+    worker.processInput({
+      name: 'resourceId',
+      data: faker.opossum.resourceName(),
+    });
+
+    expect(dispatch).toHaveBeenCalledWith<[SignalsWorkerOutput]>({
+      name: 'folderProgressData',
+      data: expect.any(Object),
+    });
+  });
+
+  it('does not dispatch folder progress data when dependencies are missing', () => {
+    const dispatch = jest.fn();
+    new SignalsWorker(dispatch, {
+      attributionBreakpoints: new Set<string>(),
+      externalData: faker.opossum.externalAttributionData(),
+      filesWithChildren: new Set<string>(),
+      manualData: faker.opossum.manualAttributionData(),
+      resolvedExternalAttributions: new Set<string>(),
+      resources: faker.opossum.resources(),
+    });
+
+    expect(dispatch).not.toHaveBeenCalledWith<[SignalsWorkerOutput]>({
+      name: 'folderProgressData',
+      data: expect.any(Object),
+    });
+  });
+});
diff --git a/src/Frontend/web-workers/__tests__/use-signals-worker.test.ts b/src/Frontend/web-workers/__tests__/use-signals-worker.test.ts
new file mode 100644
index 00000000..e89f9019
--- /dev/null
+++ b/src/Frontend/web-workers/__tests__/use-signals-worker.test.ts
@@ -0,0 +1,107 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { act } from '@testing-library/react';
+
+import { faker } from '../../../shared/Faker';
+import { View } from '../../enums/enums';
+import { setProjectMetadata } from '../../state/actions/resource-actions/all-views-simple-actions';
+import { setSelectedResourceId } from '../../state/actions/resource-actions/audit-view-simple-actions';
+import { setView } from '../../state/actions/view-actions/view-actions';
+import { renderHook } from '../../test-helpers/render-component-with-store';
+import { SignalsWorkerInput } from '../signals-worker';
+import { useSignalsWorker } from '../use-signals-worker';
+
+const mockPostMessage = jest.fn();
+const mockTerminate = jest.fn();
+
+class Worker {
+  url: string;
+  postMessage = mockPostMessage;
+  terminate = mockTerminate;
+
+  constructor(scriptURL: string) {
+    this.url = scriptURL;
+  }
+}
+
+describe('useSignalsWorker', () => {
+  beforeEach(() => {
+    Object.defineProperty(window, 'Worker', {
+      writable: true,
+      value: Worker,
+    });
+  });
+
+  it('sets resource ID when in audit view and a resource ID is present', () => {
+    const metadata = faker.opossum.metadata();
+    const resourceId = faker.opossum.resourceName();
+    renderHook(useSignalsWorker, {
+      actions: [
+        setProjectMetadata(metadata),
+        setView(View.Audit),
+        setSelectedResourceId(resourceId),
+      ],
+    });
+
+    expect(mockPostMessage).toHaveBeenCalledWith<[SignalsWorkerInput]>({
+      name: 'resourceId',
+      data: resourceId,
+    });
+  });
+
+  it('does not set resource ID when not in audit view', () => {
+    const metadata = faker.opossum.metadata();
+    const resourceId = faker.opossum.resourceName();
+    renderHook(useSignalsWorker, {
+      actions: [
+        setProjectMetadata(metadata),
+        setView(View.Attribution),
+        setSelectedResourceId(resourceId),
+      ],
+    });
+
+    expect(mockPostMessage).not.toHaveBeenCalledWith<[SignalsWorkerInput]>({
+      name: 'resourceId',
+      data: resourceId,
+    });
+  });
+
+  it('does not set resource ID when no resource ID is present', () => {
+    const metadata = faker.opossum.metadata();
+    const resourceId = faker.opossum.resourceName();
+    renderHook(useSignalsWorker, {
+      actions: [setProjectMetadata(metadata), setView(View.Audit)],
+    });
+
+    expect(mockPostMessage).not.toHaveBeenCalledWith<[SignalsWorkerInput]>({
+      name: 'resourceId',
+      data: resourceId,
+    });
+  });
+
+  it('does not re-initialize worker when project ID is stable', () => {
+    const metadata = faker.opossum.metadata();
+    renderHook(useSignalsWorker, {
+      actions: [setProjectMetadata(metadata)],
+    });
+
+    expect(mockTerminate).not.toHaveBeenCalled();
+  });
+
+  it('re-initializes worker when project ID changes', () => {
+    const metadata = faker.opossum.metadata();
+    const { store } = renderHook(useSignalsWorker, {
+      actions: [setProjectMetadata(metadata)],
+    });
+
+    act(() => {
+      store.dispatch(
+        setProjectMetadata({ ...metadata, projectId: faker.string.sample() }),
+      );
+    });
+
+    expect(mockTerminate).toHaveBeenCalledTimes(1);
+  });
+});
diff --git a/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts b/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts
new file mode 100644
index 00000000..3c2b32b5
--- /dev/null
+++ b/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts
@@ -0,0 +1,247 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { faker } from '../../../../shared/Faker';
+import { SignalWithCount } from '../../../../shared/shared-types';
+import { getAutocompleteSignals } from '../get-autocomplete-signals';
+
+describe('getAutocompleteSignals', () => {
+  it('deduplicates data and ignores data with invalid PURL', () => {
+    const [resourceName1, resourceName2, resourceName3, resourceName4] =
+      faker.opossum.resourceNames({
+        count: 4,
+      });
+    const [attributionId1, attribution1] = faker.opossum.externalAttribution();
+    const [attributionId2, attribution2] = faker.opossum.externalAttribution({
+      packageType: undefined,
+    });
+    const [attributionId3, attribution3] = faker.opossum.externalAttribution();
+
+    const signals = getAutocompleteSignals({
+      externalData: faker.opossum.externalAttributionData({
+        resourcesToAttributions: faker.opossum.resourcesToAttributions({
+          [faker.opossum.folderPath(resourceName1)]: [attributionId1],
+          [faker.opossum.filePath(resourceName1, resourceName2)]: [
+            attributionId1,
+          ],
+          [faker.opossum.filePath(resourceName1, resourceName3)]: [
+            attributionId2,
+          ],
+          [faker.opossum.folderPath(resourceName4)]: [attributionId3],
+        }),
+        attributions: faker.opossum.externalAttributions({
+          [attributionId1]: attribution1,
+          [attributionId2]: attribution2,
+          [attributionId3]: attribution3,
+        }),
+        resourcesWithAttributedChildren:
+          faker.opossum.resourcesWithAttributedChildren({
+            attributedChildren: {
+              '0': new Set([1, 2]),
+            },
+            pathsToIndices: {
+              [faker.opossum.folderPath(resourceName1)]: 0,
+              [faker.opossum.filePath(resourceName1, resourceName2)]: 1,
+              [faker.opossum.filePath(resourceName1, resourceName3)]: 2,
+            },
+            paths: [
+              faker.opossum.folderPath(resourceName1),
+              faker.opossum.filePath(resourceName1, resourceName2),
+              faker.opossum.filePath(resourceName1, resourceName3),
+            ],
+          }),
+      }),
+      manualData: faker.opossum.manualAttributionData(),
+      resolvedExternalAttributions: new Set(),
+      resourceId: faker.opossum.folderPath(resourceName1),
+      sources: faker.opossum.externalAttributionSources(),
+    });
+
+    expect(signals).toHaveLength(1);
+    expect(signals).toEqual<Array<SignalWithCount>>([
+      { ...attribution1, count: 2 },
+    ]);
+  });
+
+  it('ranks data by count', () => {
+    const [resourceName1, resourceName2, resourceName3] =
+      faker.opossum.resourceNames({
+        count: 3,
+      });
+    const [attributionId1, attribution1] = faker.opossum.externalAttribution();
+    const [attributionId2, attribution2] = faker.opossum.externalAttribution();
+
+    const signals = getAutocompleteSignals({
+      externalData: faker.opossum.externalAttributionData({
+        resourcesToAttributions: faker.opossum.resourcesToAttributions({
+          [faker.opossum.folderPath(resourceName1)]: [attributionId1],
+          [faker.opossum.filePath(resourceName1, resourceName2)]: [
+            attributionId1,
+          ],
+          [faker.opossum.filePath(resourceName1, resourceName3)]: [
+            attributionId2,
+          ],
+        }),
+        attributions: faker.opossum.externalAttributions({
+          [attributionId1]: attribution1,
+          [attributionId2]: attribution2,
+        }),
+        resourcesWithAttributedChildren:
+          faker.opossum.resourcesWithAttributedChildren({
+            attributedChildren: {
+              '0': new Set([1, 2]),
+            },
+            pathsToIndices: {
+              [faker.opossum.folderPath(resourceName1)]: 0,
+              [faker.opossum.filePath(resourceName1, resourceName2)]: 1,
+              [faker.opossum.filePath(resourceName1, resourceName3)]: 2,
+            },
+            paths: [
+              faker.opossum.folderPath(resourceName1),
+              faker.opossum.filePath(resourceName1, resourceName2),
+              faker.opossum.filePath(resourceName1, resourceName3),
+            ],
+          }),
+      }),
+      manualData: faker.opossum.manualAttributionData(),
+      resolvedExternalAttributions: new Set(),
+      resourceId: faker.opossum.folderPath(resourceName1),
+      sources: faker.opossum.externalAttributionSources(),
+    });
+
+    expect(signals).toHaveLength(2);
+    expect(signals).toEqual<Array<SignalWithCount>>([
+      { ...attribution1, count: 2 },
+      { ...attribution2, count: 1 },
+    ]);
+  });
+
+  it('ranks data by preferred and then was-preferred', () => {
+    const [resourceName1, resourceName2, resourceName3] =
+      faker.opossum.resourceNames({
+        count: 3,
+      });
+    const [attributionId1, attribution1] = faker.opossum.externalAttribution({
+      preferred: false,
+      wasPreferred: false,
+    });
+    const [attributionId2, attribution2] = faker.opossum.externalAttribution({
+      preferred: false,
+      wasPreferred: true,
+    });
+    const [attributionId3, attribution3] = faker.opossum.externalAttribution({
+      preferred: true,
+      wasPreferred: undefined,
+    });
+
+    const signals = getAutocompleteSignals({
+      externalData: faker.opossum.externalAttributionData({
+        resourcesToAttributions: faker.opossum.resourcesToAttributions({
+          [faker.opossum.folderPath(resourceName1)]: [attributionId1],
+          [faker.opossum.folderPath(resourceName1, resourceName2)]: [
+            attributionId2,
+          ],
+          [faker.opossum.filePath(resourceName1, resourceName2, resourceName3)]:
+            [attributionId3],
+        }),
+        attributions: faker.opossum.externalAttributions({
+          [attributionId1]: attribution1,
+          [attributionId2]: attribution2,
+          [attributionId3]: attribution3,
+        }),
+        resourcesWithAttributedChildren:
+          faker.opossum.resourcesWithAttributedChildren({
+            attributedChildren: {
+              '0': new Set([1, 2]),
+            },
+            pathsToIndices: {
+              [faker.opossum.folderPath(resourceName1)]: 0,
+              [faker.opossum.folderPath(resourceName1, resourceName2)]: 1,
+              [faker.opossum.filePath(
+                resourceName1,
+                resourceName2,
+                resourceName3,
+              )]: 2,
+            },
+            paths: [
+              faker.opossum.folderPath(resourceName1),
+              faker.opossum.folderPath(resourceName1, resourceName2),
+              faker.opossum.filePath(
+                resourceName1,
+                resourceName2,
+                resourceName3,
+              ),
+            ],
+          }),
+      }),
+      manualData: faker.opossum.manualAttributionData(),
+      resolvedExternalAttributions: new Set(),
+      resourceId: faker.opossum.folderPath(resourceName1),
+      sources: faker.opossum.externalAttributionSources(),
+    });
+
+    expect(signals).toHaveLength(3);
+    expect(signals).toEqual<Array<SignalWithCount>>([
+      { ...attribution3, count: 1 },
+      { ...attribution2, count: 1 },
+      { ...attribution1, count: 1 },
+    ]);
+  });
+
+  it('ranks data by source', () => {
+    const source1 = faker.opossum.externalAttributionSource({ priority: 1 });
+    const source2 = faker.opossum.externalAttributionSource({ priority: 2 });
+    const [resourceName1, resourceName2] = faker.opossum.resourceNames({
+      count: 2,
+    });
+    const [attributionId1, attribution1] = faker.opossum.externalAttribution({
+      source: faker.opossum.source({ name: source1.name }),
+    });
+    const [attributionId2, attribution2] = faker.opossum.externalAttribution({
+      source: faker.opossum.source({ name: source2.name }),
+    });
+
+    const signals = getAutocompleteSignals({
+      externalData: faker.opossum.externalAttributionData({
+        resourcesToAttributions: faker.opossum.resourcesToAttributions({
+          [faker.opossum.folderPath(resourceName1)]: [attributionId1],
+          [faker.opossum.filePath(resourceName1, resourceName2)]: [
+            attributionId2,
+          ],
+        }),
+        attributions: faker.opossum.externalAttributions({
+          [attributionId1]: attribution1,
+          [attributionId2]: attribution2,
+        }),
+        resourcesWithAttributedChildren:
+          faker.opossum.resourcesWithAttributedChildren({
+            attributedChildren: {
+              '0': new Set([1]),
+            },
+            pathsToIndices: {
+              [faker.opossum.folderPath(resourceName1)]: 0,
+              [faker.opossum.filePath(resourceName1, resourceName2)]: 1,
+            },
+            paths: [
+              faker.opossum.folderPath(resourceName1),
+              faker.opossum.filePath(resourceName1, resourceName2),
+            ],
+          }),
+      }),
+      manualData: faker.opossum.manualAttributionData(),
+      resolvedExternalAttributions: new Set(),
+      resourceId: faker.opossum.folderPath(resourceName1),
+      sources: faker.opossum.externalAttributionSources({
+        [source1.name]: source1,
+        [source2.name]: source2,
+      }),
+    });
+
+    expect(signals).toHaveLength(2);
+    expect(signals).toEqual<Array<SignalWithCount>>([
+      { ...attribution2, count: 1 },
+      { ...attribution1, count: 1 },
+    ]);
+  });
+});
diff --git a/src/e2e-tests/__tests__/saving-attributions.test.ts b/src/e2e-tests/__tests__/saving-attributions.test.ts
index cd3a5063..4da9e63a 100644
--- a/src/e2e-tests/__tests__/saving-attributions.test.ts
+++ b/src/e2e-tests/__tests__/saving-attributions.test.ts
@@ -14,7 +14,7 @@ const [attributionId1, packageInfo1] = faker.opossum.manualAttribution({
 });
 const [attributionId2, packageInfo2] = faker.opossum.manualAttribution({
   packageType: undefined,
-  licenseName: license1.shortName,
+  licenseName: license1.fullName,
 });
 
 test.use({
@@ -52,7 +52,7 @@ test('adds a new third-party attribution in audit view', async ({
 }) => {
   const newPackageInfo = faker.opossum.manualPackageInfo({
     attributionConfidence: DiscreteConfidence.High,
-    licenseName: license1.shortName,
+    licenseName: license1.fullName,
     packageType: undefined,
   });
   await resourceBrowser.goto(resourceName1);
diff --git a/src/e2e-tests/page-objects/AttributionDetails.ts b/src/e2e-tests/page-objects/AttributionDetails.ts
index 3cd95ea0..4fc5924a 100644
--- a/src/e2e-tests/page-objects/AttributionDetails.ts
+++ b/src/e2e-tests/page-objects/AttributionDetails.ts
@@ -100,9 +100,12 @@ export class AttributionDetails {
       { exact: true },
     );
     this.copyright = this.node.getByLabel('Copyright', { exact: true });
-    this.licenseName = this.node.getByRole('combobox', {
-      name: 'License Name',
-    });
+    this.licenseName = this.node.getByLabel(
+      text.attributionColumn.licenseName,
+      {
+        exact: true,
+      },
+    );
     this.licenseText = this.node.getByLabel(
       'License Text (to appear in attribution document)',
       { exact: true },
@@ -437,9 +440,7 @@ export class AttributionDetails {
   }
 
   async selectLicense(license: RawFrequentLicense): Promise<void> {
-    await this.window
-      .getByText(`${license.shortName} - ${license.fullName}`)
-      .click();
+    await this.window.getByText(license.fullName).click();
   }
 
   async removeAuditingLabel(
diff --git a/setupTests.ts b/src/shared/setupTests.ts
similarity index 82%
rename from setupTests.ts
rename to src/shared/setupTests.ts
index d51fab1a..78fa0b2c 100644
--- a/setupTests.ts
+++ b/src/shared/setupTests.ts
@@ -5,15 +5,8 @@
 // SPDX-License-Identifier: Apache-2.0
 import '@testing-library/jest-dom';
 
-import { ElectronAPI } from './src/shared/shared-types';
+import { ElectronAPI } from './shared-types';
 
-// We suppress webworker errors that are due to jest not supporting these.
-const SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_INFO = [
-  'Web worker error in workers context',
-  'Error in rendering folder progress bar',
-  'Error in rendering top progress bar',
-  'Error in ResourceDetailsTab',
-];
 // We suppress the recharts warning that is due to our mocking in tests.
 const SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_WARN = [
   'The width(0) and height(0) of chart should be greater than 0,',
@@ -24,9 +17,7 @@ const SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_ERROR = [
   'should be wrapped into act(...)',
 ];
 
-jest.mock('./src/Frontend/web-workers/get-new-accordion-workers');
-jest.mock('./src/Frontend/web-workers/get-new-progress-bar-workers');
-jest.mock('./src/ElectronBackend/main/logger.ts');
+jest.mock('../ElectronBackend/main/logger.ts');
 
 const originalResizeObserver = window.ResizeObserver;
 const originalConsoleError = console.error;
@@ -60,7 +51,6 @@ beforeAll(() => {
 
   mockResizeObserver();
 
-  mockConsoleImplementation(SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_INFO, 'info');
   mockConsoleImplementation(SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_WARN, 'warn');
   mockConsoleImplementation(SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_ERROR, 'error');
 });