diff --git a/.github/workflows/build-and-e2e-test.yml b/.github/workflows/build-and-e2e-test.yml index 099a30bc1..efcbc4b2e 100644 --- a/.github/workflows/build-and-e2e-test.yml +++ b/.github/workflows/build-and-e2e-test.yml @@ -51,6 +51,6 @@ jobs: - uses: actions/upload-artifact@v4 if: failure() with: - name: artifacts - path: src/e2e-tests/artifacts + name: artifacts-${{ matrix.os }} + path: src/e2e-tests/artifacts-${{ matrix.os }} retention-days: 30 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index debe73e60..fa55ec63b 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -5,38 +5,35 @@ SPDX-FileCopyrightText: TNG Technology Consulting GmbH SPDX-License-Identifier: CC0-1.0 --> -# User's Guide +# User Guide ## Table of contents -1. [How to get & run OpossumUI](#get_and_run_OpossumUI) - 1. [Get the latest release](#get_latest_release) - 2. [Running the app](#running_the_app) -2. [Working with OpossumUI](#working_with_OpossumUI) - 1. [Opossum file format](#dot_opossum) - 2. [Json files](#json_files) - 3. [Opening a file](#opening_a_file) - 4. [Search](#search) - 5. [Locate signals](#locator) - 6. [Project Metadata](#project_metadata) - 7. [Project Statistics](#project_statistics) - 8. [Exporting Formats](#exporting_formats) - 9. [Attributions](#attributions) - 10. [Top Bar](#top_bar) - 11. [Audit View](#audit_view) - 12. [Attribution View](#attribution_view) - 13. [Report View](#report_view) - 14. [Preferred Attributions](#preferred_attributions) - -## How to get & run OpossumUI - -### Get the latest release - -Download the latest release for your OS from [Github](https://github.com/opossum-tool/OpossumUI/releases/latest). +- [Getting Started](#getting-started) + - [Get the latest release](#get-the-latest-release) + - [Running the app](#running-the-app) +- [Working with OpossumUI](#working-with-opossumui) + - [Opossum file format](#opossum-file-format) + - [JSON files](#json-files) + - [Opening a file](#opening-a-file) + - [Project Metadata](#project-metadata) + - [Project Statistics](#project-statistics) + - [Exporting Formats](#exporting-formats) + - [Attributions](#attributions) + - [Top Bar](#top-bar) + - [Audit View](#audit-view) + - [Report View](#report-view) + - [Preferred Attributions](#preferred-attributions) + +## Getting Started + +### Get the latest release + +Download the latest release for your OS from [GitHub](https://github.com/opossum-tool/OpossumUI/releases/latest). To check if your installation is up to date, open the `Help` menu and select `Check for updates`. -### Running the app +### Running the app #### Linux @@ -50,15 +47,15 @@ Run _OpossumUI_ in _OpossumUI-for-mac.zip_. Run _OpossumUI-for-win.exe_ to install the OpossumUI. Then open _OpossumUI_ from the start menu. -## Working with OpossumUI +## Working with OpossumUI -### Opossum file format +### Opossum file format Files with a `.opossum` extension are zip-archives which contain an _input.json_ (must be provided) together with an _output.json_ (optional). An output file will be automatically created and added to the archive after opening the archive if there is no such file yet. -### Json files +### JSON files -Two .json files are used by the app to store data: +Two .JSON files are used by the app to store data: - an input file, that must be provided, - an output file, which is created by the app when saving for the first time if not already present. @@ -66,49 +63,22 @@ Two .json files are used by the app to store data: The output file must be in the same folder as the input file and called `[NAME_OF_THE_FIRST_FILE]_attributions.json` to be recognized by the app. -### Opening a File - -To open the input file in the app, click the _Open File_ button on the left of the top bar (or on the entry in the -_File_ menu with the same name). +### Opening a File -![integration](./docs/user_guide_screenshots/open_file.png) +To open the input file in the app, click the _Open File_ button on the left of the top bar (or on the entry in the _File_ menu with the same name). If you try to open a _.json_ file, a popup will be shown which asks whether you would like to create a `.opossum` file and proceed (recommended) or continue working with the old format (two separate _.json_ files). -### Search - -To search for a path, press `CTRL + F` or open the `Edit` menu and select `Search for Files and Folders`. - -![integration](./docs/user_guide_screenshots/search.png) - -### Locate signals - -To locate signals, press `CTRL + L` or open the `Edit` menu and select `Locate Signals`. This will open the Locate Signals popup. -From here, you can choose which signals you want to locate in the resource tree. You can search for signals by string, criticality -or choose one or multiple existing license name(s). Once selected, the locations of matching signals will be highlighted in the resource tree. - -Note: When searching for matching resources, the input of the search field, the selected criticality and the selected -license names are linked with **and**, while different license names in the license search field are linked with **or**. -That is, when searching for a license name using the search field (and the checkbox) and additionally selecting -another license name, only the resources matching the license name **and** the term in the search field will be highlighted -while when searching for multiple license names, all resources matching **one of these** licenses will be highlighted. - -![integration](./docs/user_guide_screenshots/locator.png) - -You can also locate signals by license from the [Project Statistics](#project_statistics) popup, by clicking on the locator icon next to each license. - -![integration](./docs/user_guide_screenshots/locator_project_statistics.png) - -### Project Metadata +### Project Metadata To view project metadata, open the `File` menu and select `Project Metadata`. -### Project Statistics +### Project Statistics To view project statistics, open the `File` menu and select `Project Statistics`. This opens a popup that shows various tables and pie charts summarizing the state of the project. -### Exporting Formats +### Exporting Formats It is possible to directly export data to files. The following formats are available: @@ -120,71 +90,55 @@ It is possible to directly export data to files. The following formats are avail To generate a document, open the `File` menu and select `Export`. -![integration](./docs/user_guide_screenshots/exports.png) - -### Attributions +### Attributions -The basic building block of license/attribution information in the opossumUI is the **Attribution**. An **Attribution** -isn't only a software package with name & version (or purl) and copyright, distributed under one or more licenses. It -can in principle be any file which has a copyright or is distributed under a license. **The purpose of the opossumUI is to +The basic building block of license/attribution information in the OpossumUI is the **Attribution**. An **Attribution** +isn't only a software package with name & version (or PURL) and copyright, distributed under one or more licenses. It +can in principle be any file which has a copyright or is distributed under a license. **The purpose of the OpossumUI is to link resources to the corresponding attributions, with an emphasis on correct licensing and copyright information.** -In the opossumUI, a distinction between **signals** and **attributions** is made: +In the OpossumUI, a distinction between **signals** and **attributions** is made: -- **attributions** are attribution information that are created in the current run of the opossumUI. They are stored in +- **attributions** are attribution information that are created in the current run of the OpossumUI. They are stored in the output file, together with the resources they have been linked to, -- **signals** are attribution information that have been linked to a resource before the current opossumUI run. They can - come from automatic tools or previous run of the opossumUI. They have a **source** and can be used as starting point for +- **signals** are attribution information that have been linked to a resource before the current OpossumUI run. They can + come from automatic tools or previous run of the OpossumUI. They have a **source** and can be used as starting point for assigning attributions. -### Top Bar +### Top Bar + +![integration](./docs/user_guide_screenshots/top_bar.png) -In the `Top Bar`, the following elements are present. From left to right: +In the `Top Bar`, the following elements are present: -- the _Open File_ button (read _Open File_ section to learn more about opening a file), -- the `Progress Bar` (shown only if a file is currently opened), +- the _Open File_ button, +- the `Progress Bar`, - the `Progress Bar Toggle`, - the `View Switch`, -- the app version. - -The `Progress Bar` indicates how many files have manually received an attribution (dark green), how many have an -automatically **pre-selected** attribution (lighter green with gradient), and how many files have a signal, but have not -yet received an attribution (orange), with respect to the total number of files. Hovering on the bar shows a tooltip -containing all 4 numbers. Clicking on the bar navigates to a file that has a signal, but no attribution. +- the app version, +- the path bar. -![integration](./docs/user_guide_screenshots/top_bar.png) +The `Progress Bar` indicates how many files have manually received an attribution (dark green), how many have an automatically **pre-selected** attribution (lighter green with gradient), and how many files have a signal, but have not yet received an attribution (orange), with respect to the total number of files. Hovering on the bar shows a tooltip containing all four numbers. Clicking on the bar navigates to a file that has a signal, but no attribution. -Clicking the `Progress Bar Toggle` replaces the `Progress Bar` by the `Critical Signals Progress Bar`. The -`Critical Signals Progress Bar` indicates how many files have a highly critical signal but no attribution (red), -a medium critical signal but no attribution (orange) with respect to the total number of files not having an attribution. -Hovering on the bar shows a tooltip containing all 4 numbers. Clicking on the bar navigates to a file that has a critical signal, +Clicking the `Progress Bar Toggle` replaces the `Progress Bar` by the `Critical Signals Progress Bar`. The `Critical Signals Progress Bar` indicates how many files have a highly critical signal but no attribution (red), a critical signal but no attribution (orange) with respect to the total number of files not having an attribution. Hovering on the bar shows a tooltip containing all 4 numbers. Clicking on the bar navigates to a file that has a critical signal, but no attribution. -The `View Switch` allows to change between the `Audit View`, the `Attribution View`, and the `Report View` (the views -are described in more detail in the respective sections). +The `View Switch` allows to change between the `Audit View` and the `Report View`. -The app version is crucial to allow the development team to reproduce bugs: please always include it in -screenshots/videos/emails documenting a bug. +The `path bar` shows the path of the currently selected resource. It also provides opens to navigate back and forth in the selection history, to copy the path to the clipboard, and, if possible, to open the resource's source repository in a browser. -### Audit View +### Audit View ![integration](./docs/user_guide_screenshots/audit_view.png) -**Resource** is the generic name used throughout the app to indicate a file or a folder (as in many cases they are -treated the same). The `Audit View` focuses on the navigation through the resources to add/edit/remove attributions -while seeing which signals have been found by the remote tools. The page has two main components: - -- a `Resource Tree` on the left, -- a `Selected Resource Panel` on the center right (shown only if a resource has been selected in the `Resource Tree`). +**Resource** is the generic name used throughout the app to indicate a file or a folder (as in many cases they are treated the same). The `Audit View` focuses on the navigation through the resources to add/edit/remove attributions while seeing which signals have been found by the remote tools. The page has three main components: -#### Folders and inferred attributions +- a resource browser on the left, +- panels to list attributions and signals on the selected resource in the middle, +- and attribution details, if an attribution or signal has been selected, on the right. -In the case that a folder receives an attribution this attribution is also inferred to all its children that -do not have their own attribution. Therefore, adding an attribution to a folder affects its children if these -are not attributed themselves. The inference stops once a folder or a file is hit that has a differing attribution. +#### Resource Browser -#### Resource Tree - -In the `Resource Tree` resources can be selected. **Icons** help to find information in the folder +In the resource browser, resources can be selected for assigning attributions or inspecting signals. **Icons** help to find information in the folder structure: - a **file icon** ![integration](./docs/user_guide_screenshots/file_icon.png) indicates that the resource is a file, @@ -194,8 +148,6 @@ structure: Furthermore, no attribution is inferred beyond such a breakpoint), - a **exclamation mark** ![integration](./docs/user_guide_screenshots/has_signals_icon.png) indicates the presence of signals attached to the resource. -![integration](./docs/user_guide_screenshots/filetree.png) - The coloring scheme reads as follows: - **red** indicates the presence of signals but no attribution for the resource itself, @@ -205,158 +157,99 @@ The coloring scheme reads as follows: - **grey** indicates the absence of both, signals and attribution, in children, - **blue** indicates the presence of signals in children but no attribution of the resource itself. -#### Selected Resource Panel +Please note that in case a resource has an attribution, this attribution also applies to all of its children that do not have attributions of their own. Therefore, adding an attribution to a folder affects its children if these are not attributed themselves. The inference stops once a folder or a file is hit that has a differing attribution. -The `Selected Resource Panel` shows the path of the selected resource at the top. If the input file contains information -about the location of the file (`baseUrlsForSources`) an icon to externally open the file is shown. +At the bottom of the resource browser you will find a panel listing resources linked to the selected attribution or signal. -Below the path, the element is divided into two columns. In the `Attribution Selection Column`, in the center of the -screen, attributions and signals related to the selected resource are listed. In the `Attribution Details Column` on -the right, additional information is shown for the selected attribution/signal. +#### Attributions Panel -![integration](./docs/user_guide_screenshots/selected_resource_panel.png) +The attributions panel lists attributions as they relate to the selected resource. The possible relationships, by which attributions are grouped into tabs, are: -##### Attribution Selection Column +- **on the selected resource:** attributions directly assigned to the selected resource +- **on children of the selected resource:** attributions assigned to resources contained in the selected resource +- **on parents of the selected resource:** attributions assigned to resources containing the selected resource +- **unrelated:** attributions that are not directly or indirectly linked to the selected resource -In the `Attribution Selection Column` the following sub-panels may be present: +Besides searching, sorting, and filtering attributions according to your needs, you can also perform any of the following actions: -- `Attribution Sub-Panel` (always shown), -- `Signals Sub-Panel` (accessible via the `LOCAL` tab), -- `Attributions in Folder Content Sub-Panel` (accessible via the `LOCAL` tab), -- `Signals in Folder Content Sub-Panel` (accessible via the `LOCAL` tab), -- `Add to Attribution Sub-Panel` (accessible via the `GLOBAL` tab). +- **create new attribution:** creates a new attribution on the selected resource from scratch +- **link as attribution on selected resource:** links the selected attributions to the selected resource (only available if some of the selected attributions are not already linked) +- **confirm:** confirms any of the selected attributions which are pre-selected (P) +- **replace:** enters replacement mode during which you can select a replacement for the selected attributions +- **delete:** deletes the selected attributions -The `Attributions Sub-Panel` shows a list of all attributions that are assigned to the selected resource. -**Pre-selected** attributions are signaled by an `P` icon. They can be confirmed, therefore being considered -attributions in all views and in the progress bar. However, that is not a requirement. **Pre-selected** and -attributions are both written in the output file. Clicking on one of the -attributions, shows the details of that attribution in the `Attribution Details Column`. Clicking on _Add new -attribution_ shows a blank `Attribution Details Column` that allows for adding a new attribution to the list of -attributions, upon saving. If the shown attributions are inferred from a containing folder, they cannot be modified. -Instead, the -_OVERRIDE PARENT_ button can be clicked for creating new attributions for the selected resource. (Note that attributions -are not saved separately, if they are identical to the attributions of a containing folder and can thus be inferred.) +#### Signals Panel -The `Signals Sub-Panel`, `Attributions in Folder Content Sub-Panel` and `Signals in Folder Content Sub-Panel` show lists -of the signals of the selected resource and the attributions and signals of the resources contained within the selected -folder. Clicking on the one of the listed items, shows the details of the respective attribution/signal in the -`Attribution Details Column`. By clicking the **+ icon** of an item, the respective attribution/signal can be added to -the attributions of the selected resource. In the `Signals Sub-Panel` signals that were used to create the pre-selected -attributions are shown with a `P` icon, even if the relative attributions have been deleted. The cards in the -` ... in folder content` sub-panels also show the number of resources in the folder that are linked to the shown -attribution. +The signals panel lists signals as they relate to the selected resource. The possible relationships, by which attributions are grouped into tabs, are: -Similar signals that deviate only by `comment`, `attributionConfidence`, `originIds`, or the `preSelected` flag are merged into a single signal with multiple comments according to the comments of the individual merged signals. When adding a merged signal to the attributions of the current resource, the comment of the resulting attribution is empty. Relevant parts can be copied from the merged signal. +- **on the selected resource:** attributions directly assigned to the selected resource +- **on children of the selected resource:** attributions assigned to resources contained in the selected resource -The `Add to Attribution Sub-Panel` allows to add an existing attribution to the attributions of the selected resource. -As in the other panels, the details of the attributions can be shown by clicking on the respective list item, while the -attribution can be added by clicking on the corresponding **+ icon**. +Besides searching, sorting, and filtering signals according to your needs, you can also perform any of the following actions: -#### Attribution Details Column +- **link as attribution on selected resource:** converts the selected signals to attributions and links them to the selected resource +- **delete:** soft-deletes the selected signals, i.e., hides them from the list +- **restore:** restores the selected soft-deleted signals (only available when you include the deleted signals via the show/hide button) +- **show/hide deleted signals:** shows/hides the soft-deleted signals -The `Attribution Details Column` is used in the `Audit View` and in the `Attribution View` (see next section) to show -details of the selected attribution and to edit and save the information of the selected attribution. Note that inferred -attribution information and signals cannot be edited. +#### Attribution Details -IMPORTANT: Some fields in the column have special meanings/behaviors: +The attribution details show the attributes of the selected attribution or signal. It is here that you can edit and save details of attributions as well. Signals can never be edited (only hidden). -- _PURL_: If provided, package name and version are extracted from it, and the corresponding fields are not editable. A - basic validity check is done on the purl: if the purl text is red it means it is invalid and saving is prevented. -- _License Text_: It will appear in the attributions document. It will be automatically filled in for licenses suggested - in the license name dropdown. -- _Exclude From Notice Checkbox_: If checked, the relative attribution will not be shown in the notice document. - In the case of first party code, the respective flag should be preferred. - _Exclude From Notice Checkbox_ should be used only if: - - the content of the attribution does not need attribution or - - the attribution isn't an actual attribution or - - it was globally decided that this attribution does not need attribution (e.g. it is proprietary but bought for the - whole company). -- _Comment / Comments_: In the case of an ordinary signal or an attribution, the comment textbox is displaying a single comment. In the case of a merged signal, the comment textbox is displaying multiple comments according to the comments of the individual merged signals. -- _Needs Review Checkbox_: This checkbox can be used to signal to another OpossumUI user that an attribution needs further review. - The state of the checkbox is persisted when saving the attribution, so it can e.g. be used for a typical QA workflow. +The attributes are divided into three categories: -The `Attribution Details Column`, if editable, shows the following buttons: +- **auditing options:** certain annotations and tags that facilitate the auditing process +- **package coordinates:** attributes used to uniquely identify the package +- **legal information:** attributes used to describe the OSS licensing aspects of the package -- _SAVE_, saves the edited information for the selected resource only, removing the **pre-selected** attribute if - present. -- _SAVE GLOBALLY_, (shown only if the attribution of the selected resource is also linked to other resources) saves the - changes for all the linked resources. The same can also be done by pressing _Ctrl + S_. -- _CONFIRM_, removes the **pre-selected** attribute from the attribution for the selected resource only. -- _CONFIRM GLOBALLY_, (shown only if the attribution of the selected resource is also linked to other resources) removes - the **pre-selected** attribute from the attribution for all linked resources. -- _..._, opens a menu with the following buttons: - - _Revert_, discards the changes, - - _Delete_, deletes the attribution of the selected resource only. - - _Delete Globally_, (shown only if the attribution of the selected resource is also linked to other resources) - deletes the attribution for all the linked resources. +##### Auditing Options -The _SAVE_ / _SAVE GLOBALLY_ and _Revert_ buttons are disabled if no change has been made. - -When all fields except for the _confidence_ field are empty, pressing the _SAVE_ or the _SAVE GLOBALLY_ button deletes -the respective attribution. - -The `Attribution Details Column`, when a signal is selected, shows the _HIDE_ button. It can be used to hide the given -signal in the App for the current input/output files, and it will not have any consequence in the DB. When clicking _HIDE_ for a merged signal, all individual signals that make up the merged signal are hidden. +- _Exclude From Notice_: If chosen, the relative attribution will not be shown in the notice document. In the case of first party code, the respective flag should be used. _Exclude From Notice_ should be used only if: + - the content of the attribution does not need attribution or + - the attribution isn't an actual attribution or + - it was globally decided that this attribution does not need attribution (e.g. it is proprietary but bought for the whole company). +- _Needs Review by QA_: This flag can be used to signal to another OpossumUI user, for example someone performing quality assurance, that an attribution needs review. +- _Needs Follow-Up_: This flag can be used to indicate that the attribution requires follow-up, usually with the development team, as it would be part of a blacklist. +- _Confidence_: This field is used to indicate the confidence in the correctness of the attribution. It is a emoticon on a scale of 1 to 5. You can also filter for attributions with low confidence in the attributions panel. -### Attribution View +##### Package Coordinates -![integration](./docs/user_guide_screenshots/attribution_view.png) +These coordinates serve to uniquely identify the package. In particular, package name and package type are required information from which a PURL ("Package URL") is automatically generated. Some package types also require the presence of a namespace. For example, GitHub and Maven packages require a namespace, while NPM packages do not. -In the `Attribution View` all attributions are listed and can be viewed and edited. The page is in structure similar to -the `Audit View` and has two main components: +Also try to fill the repository URL of the attribution as it often helps to automatically compute the correct license information from it. -- an `Attribution List` on the left, -- a `Selected Attribution Panel` on the center right (shown only if an attribution has been selected from the list). +Be aware that different package versions may result in different license information. Thus, providing a version whenever possible is also very helpful. -#### Attribution List +##### Legal Information -All existing attributions are listed and can be selected. **Pre-selected** -attributions are signaled by an `P` icon. They can be confirmed, which converts them into attributions -in all views and in the progress bar. However, that is not a requirement. **Pre-selected** and manual -attributions are both written in the output file. On top there is an icon for opening the filter section. By clicking -on it, a dropdown will be shown with filters that allows for filtering for attributions marked for follow-up, first -party, third party, and other aspects. Additionally, the attribution view has a multi-select mode. +Copyright and license name are the most important part of an attribution when it comes to OSS compliance. However, if you are dealing with first-party code, then please select this option in this section. You then no longer will be asked to supply copyright and license name. -#### Selected Attribution Panel +##### Comparing an Attribution to its Original Signal -The `Selected Attribution Panel` looks much like the `Selected Resource Panel`. The main differences are: +If an attribution originates from a signal, a `Compare to Original Signal` icon button will be displayed in the row of buttons at the bottom of the attribution details. Clicking this button opens a popup where the package coordinates and legal information of both the current attribution and its original signal are displayed side-by-side. Attributes that have changed are highlighted by colored outlines. -- Only information for the **selected attribution** are shown, in a fashion almost identical to - the `Selected Resource Panel`. They are always editable. -- The _SAVE_ and _Delete_ buttons allow saving/deleting the selected attribution. Note that the changes affect multiple - resources if the selected attribution is linked to multiple resources. -- A `Resource List` shows the path of all resources linked to the selected attribution. Clicking on a path shows the - selected resource in the `Audit View`. +You can revert individual attributes to their original state by pressing the arrow button inside the field. The action can be undone using the same button, which will point in the opposite direction after a revert. Additionally, all changes can be reverted at once by pressing the `Revert All` button of the popup. Changes are applied to the attribution once the `Apply Changes` button is pressed, which also closes the popup. -### Report View +### Report View ![integration](./docs/user_guide_screenshots/report_view.png) -In the `Report View` all attributions are shown in a table to provide an overview. On top there is a dropdown list with -filters that allows for filtering for attributions marked for follow-up, first party and not first party. The last two -are mutually exclusive. +In the `Report View` all attributions together with most of their attributes are shown in a table to provide a scrollable overview. As in the attributions panel, you can filter attributions by pressing the funnel icon in the top-left corner. -Clicking on the _edit_ buttons in the _name_ columns, navigates to the respective attribution in the `attribution view`. +Clicking on the _edit_ buttons in the _name_ columns, navigates to the respective attribution in the `Audit View`. -### Preferred Attributions +### Preferred Attributions -In the audit view, an attributions can be marked as preferred, to indicate that it is preferred over the displayed signals. This feature does not have any immediate effect on the signals displayed in OpossumUI; instead, it is intended to give additional information to tools that consume `.opossum` files. A preferred attribution will store origin IDs of signals visible to the user when it was marked as preferred. +In the `Audit View`, an attribution can be marked as preferred to indicate that it is preferred over the displayed signals. This feature does not have any immediate effect on the signals displayed in OpossumUI. Instead, it is intended to give additional information to tools that consume `.opossum` files. A preferred attribution will store origin IDs of signals visible to the user when it was marked as preferred. -Only signals with a source marked as `isRelevantForPreference` can be preferred over. If no signal source has this flag set, then the feature is disabled. +Only signals with a source marked as `isRelevantForPreference` can be preferred over. If no signal source has this flag set, the feature is disabled. -To mark an attribution as preferred, choose an attribution in the audit view, and open the auditing options menu. You will see an option to mark the attribution as preferred. When an attribution is marked as preferred, `preferred = true` is written to the `.opossum` file, and the origin IDs of all visible signals relevant for preference are written in the field `preferredOverOriginIds`. Preferred attributions are displayed with a star icon. +To mark an attribution as preferred, choose an attribution in the `Audit View`, and open the auditing options menu. You will see an option to mark the attribution as preferred. When an attribution is marked as preferred, `preferred = true` is written to the `.opossum` file, and the origin IDs of all visible signals relevant for preference are written in the field `preferredOverOriginIds`. Preferred attributions are displayed with a star icon. -Note that you are only able to mark an attribution as preferred (or unmark if it was preferred beforehand) if you are in -"QA Mode". To enable this mode click the item "QA Mode" in the `View` submenu. +Note that you are only able to mark an attribution as preferred (or unmark it if it was preferred beforehand) if you are in "QA Mode". To enable this mode click the item "QA Mode" in the `View` submenu. ![disabled_qa_mode](./docs/user_guide_screenshots/disabled_qa_mode.png) If "QA Mode" is enabled the icon will change as in the screenshot below. ![enabled_qa_mode](./docs/user_guide_screenshots/enabled_qa_mode.png) - -### Comparing an Attribution to its original Signal - -If an attribution originates from a signal, a `Compare to Original` button will be displayed in the row of buttons below the details of the currently selected attribution. Clicking this button opens a popup, where the package coordinates and legal information of both the current attribution and its original signal are displayed side by side. Fields that have changed are highlighted by colored outlines. - -You can revert individual fields by pressing the arrow button inside the field. The action can be undone using the same button, which will point in the opposite direction after a revert. Additionally, all changes can be reverted at once by pressing the `Revert All` button of the popup. Changes are applied to the attribution once the `Apply Changes` button is pressed, which also closes the popup. To save the changes, you can, for example, use the standard `Save` button. diff --git a/docs/user_guide_screenshots/attribution_view.png b/docs/user_guide_screenshots/attribution_view.png deleted file mode 100644 index da290d2b5..000000000 Binary files a/docs/user_guide_screenshots/attribution_view.png and /dev/null differ diff --git a/docs/user_guide_screenshots/audit_view.png b/docs/user_guide_screenshots/audit_view.png index a9272e049..d6e69069a 100644 Binary files a/docs/user_guide_screenshots/audit_view.png and b/docs/user_guide_screenshots/audit_view.png differ diff --git a/docs/user_guide_screenshots/exports.png b/docs/user_guide_screenshots/exports.png deleted file mode 100644 index da1e0839e..000000000 Binary files a/docs/user_guide_screenshots/exports.png and /dev/null differ diff --git a/docs/user_guide_screenshots/filetree.png b/docs/user_guide_screenshots/filetree.png deleted file mode 100644 index 8af733eb7..000000000 Binary files a/docs/user_guide_screenshots/filetree.png and /dev/null differ diff --git a/docs/user_guide_screenshots/locator.png b/docs/user_guide_screenshots/locator.png deleted file mode 100644 index 051503a9c..000000000 Binary files a/docs/user_guide_screenshots/locator.png and /dev/null differ diff --git a/docs/user_guide_screenshots/locator_project_statistics.png b/docs/user_guide_screenshots/locator_project_statistics.png deleted file mode 100644 index fa86649fa..000000000 Binary files a/docs/user_guide_screenshots/locator_project_statistics.png and /dev/null differ diff --git a/docs/user_guide_screenshots/open_file.png b/docs/user_guide_screenshots/open_file.png deleted file mode 100644 index 5e50f17f7..000000000 Binary files a/docs/user_guide_screenshots/open_file.png and /dev/null differ diff --git a/docs/user_guide_screenshots/report_view.png b/docs/user_guide_screenshots/report_view.png index d9601d305..860c4cfac 100644 Binary files a/docs/user_guide_screenshots/report_view.png and b/docs/user_guide_screenshots/report_view.png differ diff --git a/docs/user_guide_screenshots/search.png b/docs/user_guide_screenshots/search.png deleted file mode 100644 index 159628514..000000000 Binary files a/docs/user_guide_screenshots/search.png and /dev/null differ diff --git a/docs/user_guide_screenshots/selected_resource_panel.png b/docs/user_guide_screenshots/selected_resource_panel.png deleted file mode 100644 index 6017f1817..000000000 Binary files a/docs/user_guide_screenshots/selected_resource_panel.png and /dev/null differ diff --git a/docs/user_guide_screenshots/top_bar.png b/docs/user_guide_screenshots/top_bar.png index 888c853da..bacd93075 100644 Binary files a/docs/user_guide_screenshots/top_bar.png and b/docs/user_guide_screenshots/top_bar.png differ diff --git a/package.json b/package.json index 04234594a..952d57511 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", + "@fontsource-variable/karla": "^5.0.20", "@mui/icons-material": "^5.15.12", "@mui/material": "^5.15.12", "@mui/system": "^5.15.12", @@ -15,7 +16,6 @@ "@tanstack/react-query": "^5.24.8", "compare-versions": "^6.1.0", "dayjs": "^1.11.10", - "electron-devtools-installer": "^3.2.0", "electron-log": "^5.1.1", "electron-settings": "^4.0.2", "fast-csv": "^5.0.1", @@ -29,6 +29,7 @@ "re-resizable": "^6.9.11", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.12", "react-hot-toast": "^2.4.1", "react-hotkeys-hook": "^4.5.0", "react-redux": "^9.1.0", @@ -56,7 +57,6 @@ "@testing-library/user-event": "^14.5.2", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/babel__core": "^7.20.5", - "@types/electron-devtools-installer": "^2.2.5", "@types/jest": "^29.5.12", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.14.202", diff --git a/public/icons/wave.svg b/public/icons/wave.svg new file mode 100644 index 000000000..012a24556 --- /dev/null +++ b/public/icons/wave.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/renovate.json5 b/renovate.json5 index 330b4b5f2..042c64464 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -30,7 +30,6 @@ 'dotenv', 'electron', 'electron-builder', - 'electron-devtools-installer', 'electron-log', 'electron-playwright-helpers', 'electron-settings', @@ -49,6 +48,7 @@ 'prettier', 'prop-types', 'proxy-memoize', + 'react-error-boundary', 'react-hot-toast', 'react-hotkeys-hook', 'react-virtuoso', @@ -99,6 +99,13 @@ matchUpdateTypes: ['minor', 'patch'], automerge: true, }, + { + matchPackagePrefixes: ['@fontsource-variable/'], + groupName: 'fontsource dependencies', + groupSlug: 'fontsource', + matchUpdateTypes: ['minor', 'patch'], + automerge: true, + }, { matchPackagePrefixes: ['@tanstack/'], groupName: 'Tanstack dependencies', diff --git a/src/ElectronBackend/app.ts b/src/ElectronBackend/app.ts index 8dc95d96d..819552c2f 100644 --- a/src/ElectronBackend/app.ts +++ b/src/ElectronBackend/app.ts @@ -3,10 +3,6 @@ // // SPDX-License-Identifier: Apache-2.0 import { app } from 'electron'; -import installExtension, { - REACT_DEVELOPER_TOOLS, - REDUX_DEVTOOLS, -} from 'electron-devtools-installer'; import { main } from './main/main'; @@ -15,13 +11,3 @@ app.on('ready', main); app.on('window-all-closed', () => { app.quit(); }); - -app.on('ready', () => { - if (!app.isPackaged) { - [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS].forEach((extension) => { - installExtension(extension) - .then((name) => console.log(`Added Extension: ${name}`)) - .catch((err) => console.log('An error occurred: ', err)); - }); - } -}); diff --git a/src/ElectronBackend/input/__tests__/importFromFile.test.ts b/src/ElectronBackend/input/__tests__/importFromFile.test.ts index 9f1dd359f..8f53b94ab 100644 --- a/src/ElectronBackend/input/__tests__/importFromFile.test.ts +++ b/src/ElectronBackend/input/__tests__/importFromFile.test.ts @@ -8,11 +8,7 @@ import * as zlib from 'zlib'; import { EMPTY_PROJECT_METADATA } from '../../../Frontend/shared-constants'; import { AllowedFrontendChannels } from '../../../shared/ipc-channels'; -import { - Criticality, - DiscreteConfidence, - ParsedFileContent, -} from '../../../shared/shared-types'; +import { Criticality, ParsedFileContent } from '../../../shared/shared-types'; import { writeFile, writeOpossumFile } from '../../../shared/write-file'; import { faker } from '../../../testing/Faker'; import { @@ -433,7 +429,7 @@ describe('Test of loading function', () => { comment: 'some comment', copyright: '(c) first party', preSelected: true, - attributionConfidence: DiscreteConfidence.Low, + attributionConfidence: 17, id: manualAttributionUuid, }, }, diff --git a/src/ElectronBackend/input/__tests__/parseFile.test.ts b/src/ElectronBackend/input/__tests__/parseFile.test.ts index 530812cb2..266bd7eb3 100644 --- a/src/ElectronBackend/input/__tests__/parseFile.test.ts +++ b/src/ElectronBackend/input/__tests__/parseFile.test.ts @@ -124,9 +124,9 @@ const correctOutput: OpossumOutputFile = { resolvedExternalAttributions: [], }; -const correctParsedOuput: ParsedOpossumOutputFile = { +const correctParsedOutput: ParsedOpossumOutputFile = { ...correctOutput, - resolvedExternalAttributions: new Set(), + resolvedExternalAttributions: [], }; describe('parseOpossumFile', () => { @@ -154,7 +154,7 @@ describe('parseOpossumFile', () => { opossumFilePath, )) as ParsedOpossumInputAndOutput; expect(parsingResult.input).toStrictEqual(correctInput); - expect(parsingResult.output).toStrictEqual(correctParsedOuput); + expect(parsingResult.output).toStrictEqual(correctParsedOutput); }); it('returns JSONParsingError on an incorrect .opossum file', async () => { @@ -248,7 +248,7 @@ describe('parseOutputJsonFile', () => { const attributions = parseOutputJsonFile(attributionPath); - expect(attributions).toStrictEqual(correctParsedOuput); + expect(attributions).toStrictEqual(correctParsedOutput); }); it('throws when reading an incorrect file', async () => { @@ -272,7 +272,7 @@ describe('parseOutputJsonFile', () => { 'cff9095a-5c24-46e6-b84d-cc8596b17c58', ); const parsedFileContentWithWrongProjectId: ParsedOpossumOutputFile = set( - cloneDeep(correctParsedOuput), + cloneDeep(correctParsedOutput), 'metadata.projectId', 'cff9095a-5c24-46e6-b84d-cc8596b17c58', ); diff --git a/src/ElectronBackend/input/importFromFile.ts b/src/ElectronBackend/input/importFromFile.ts index 590d81cf4..56b2ebe08 100644 --- a/src/ElectronBackend/input/importFromFile.ts +++ b/src/ElectronBackend/input/importFromFile.ts @@ -11,7 +11,6 @@ import { v4 as uuid4 } from 'uuid'; import { AllowedFrontendChannels } from '../../shared/ipc-channels'; import { Attributions, - DiscreteConfidence, ParsedFileContent, ResourcesToAttributions, } from '../../shared/shared-types'; @@ -177,11 +176,11 @@ export async function loadInputAndOutputFromFilePath( attributionsToResources: externalAttributionsToResources, }, frequentLicenses, - resolvedExternalAttributions: parsedOutputData.resolvedExternalAttributions, - attributionBreakpoints: new Set( - parsedInputData.attributionBreakpoints ?? [], + resolvedExternalAttributions: new Set( + parsedOutputData.resolvedExternalAttributions, ), - filesWithChildren: new Set(parsedInputData.filesWithChildren ?? []), + attributionBreakpoints: new Set(parsedInputData.attributionBreakpoints), + filesWithChildren: new Set(parsedInputData.filesWithChildren), baseUrlsForSources: sanitizeRawBaseUrlsForSources( parsedInputData.baseUrlsForSources, ), @@ -261,12 +260,6 @@ function createJsonOutputFile( delete packageInfo.source; delete packageInfo.preferred; delete packageInfo.preferredOverOriginIds; - if (packageInfo.attributionConfidence !== undefined) { - packageInfo.attributionConfidence = - packageInfo.attributionConfidence >= DiscreteConfidence.High - ? DiscreteConfidence.High - : DiscreteConfidence.Low; - } const newUUID = uuid4(); manualAttributions[newUUID] = packageInfo; diff --git a/src/ElectronBackend/input/parseFile.ts b/src/ElectronBackend/input/parseFile.ts index 7622b7705..b9f485ce9 100644 --- a/src/ElectronBackend/input/parseFile.ts +++ b/src/ElectronBackend/input/parseFile.ts @@ -31,52 +31,52 @@ export async function parseOpossumFile( ): Promise< ParsedOpossumInputAndOutput | JsonParsingError | InvalidDotOpossumFileError > { - let parsedInputData: unknown; - let parsedOutputData: unknown = null; - let jsonParsingError: JsonParsingError | null = null; - let invalidDotOpossumFileError: InvalidDotOpossumFileError | null = null; + let parsedInputData: ParsedOpossumInputFile; + let parsedOutputData: ParsedOpossumOutputFile | null = null; const zip: fflate.Unzipped = await readZipAsync(opossumFilePath); + if (!zip[INPUT_FILE_NAME]) { - invalidDotOpossumFileError = { + return { filesInArchive: Object.keys(zip) .map((fileName) => `'${fileName}'`) .join(', '), type: 'invalidDotOpossumFileError', - }; - } else { - getGlobalBackendState().inputFileRaw = zip[INPUT_FILE_NAME]; - const inputJson = fflate.strFromU8(zip[INPUT_FILE_NAME]); + } satisfies InvalidDotOpossumFileError; + } + + getGlobalBackendState().inputFileRaw = zip[INPUT_FILE_NAME]; + + try { + parsedInputData = JSON.parse(fflate.strFromU8(zip[INPUT_FILE_NAME])); + jsonSchemaValidator.validate( + parsedInputData, + OpossumInputFileSchema, + validationOptions, + ); + } catch (err) { + return { + message: `Error: ${opossumFilePath} does not contain a valid input file.\n Original error message: ${err?.toString()}`, + type: 'jsonParsingError', + } satisfies JsonParsingError; + } + + if (zip[OUTPUT_FILE_NAME]) { try { - parsedInputData = parseAndValidateJson(inputJson, OpossumInputFileSchema); + const outputJson = fflate.strFromU8(zip[OUTPUT_FILE_NAME]); + parsedOutputData = parseOutputJsonContent(outputJson, opossumFilePath); } catch (err) { - jsonParsingError = { - message: `Error: ${opossumFilePath} does not contain a valid input file.\n Original error message: ${err?.toString()}`, + return { + message: `Error: ${opossumFilePath} does not contain a valid output file.\n${err?.toString()}`, type: 'jsonParsingError', - }; - } - - if (zip[OUTPUT_FILE_NAME]) { - try { - const outputJson = fflate.strFromU8(zip[OUTPUT_FILE_NAME]); - parsedOutputData = parseOutputJsonContent(outputJson, opossumFilePath); - } catch (err) { - jsonParsingError = { - message: `Error: ${opossumFilePath} does not contain a valid output file.\n${err?.toString()}`, - type: 'jsonParsingError', - }; - } + } satisfies JsonParsingError; } } - return jsonParsingError - ? jsonParsingError - : invalidDotOpossumFileError - ? invalidDotOpossumFileError - : { - input: parsedInputData as ParsedOpossumInputFile, - output: parsedOutputData as ParsedOpossumOutputFile, - }; + return { + input: parsedInputData, + output: parsedOutputData, + }; } async function readZipAsync(opossumFilePath: string): Promise { @@ -160,34 +160,17 @@ export function parseOutputJsonContent( fileContent: string, filePath: fs.PathLike, ): ParsedOpossumOutputFile { - let outputJsonContent; try { - outputJsonContent = parseAndValidateJson( - fileContent, + const jsonContent = JSON.parse(fileContent); + jsonSchemaValidator.validate( + jsonContent, OpossumOutputFileSchema, + validationOptions, ); + return jsonContent; } catch (err) { throw new Error( `Error: ${filePath.toString()} contains an invalid output file.\n Original error message: ${err?.toString()}`, ); } - - const resolvedExternalAttributions = ( - outputJsonContent as Record - ).resolvedExternalAttributions; - return { - ...(outputJsonContent as Record), - resolvedExternalAttributions: resolvedExternalAttributions - ? new Set(resolvedExternalAttributions as Array) - : new Set(), - } as ParsedOpossumOutputFile; -} - -function parseAndValidateJson( - content: string, - schema: typeof OpossumInputFileSchema | typeof OpossumOutputFileSchema, -): unknown { - const jsonContent = JSON.parse(content); - jsonSchemaValidator.validate(jsonContent, schema, validationOptions); - return jsonContent; } diff --git a/src/ElectronBackend/input/parseInputData.ts b/src/ElectronBackend/input/parseInputData.ts index 526c5f73e..666307989 100644 --- a/src/ElectronBackend/input/parseInputData.ts +++ b/src/ElectronBackend/input/parseInputData.ts @@ -223,6 +223,7 @@ export function serializeAttributions( count, followUp, id, + relation, resources, source, suffix, diff --git a/src/ElectronBackend/main/main.ts b/src/ElectronBackend/main/main.ts index 8e61217b8..f45a17fff 100644 --- a/src/ElectronBackend/main/main.ts +++ b/src/ElectronBackend/main/main.ts @@ -58,6 +58,12 @@ export async function main(): Promise { }, ); + ipcMain.handle(IpcChannel.Quit, () => { + mainWindow.close(); + }); + ipcMain.handle(IpcChannel.Relaunch, () => { + mainWindow.reload(); + }); ipcMain.handle( IpcChannel.ConvertInputFile, getConvertInputFileToDotOpossumAndOpenListener(mainWindow), diff --git a/src/ElectronBackend/main/menu.ts b/src/ElectronBackend/main/menu.ts index 249083ffe..784feb907 100644 --- a/src/ElectronBackend/main/menu.ts +++ b/src/ElectronBackend/main/menu.ts @@ -258,37 +258,6 @@ export async function createMenu(mainWindow: BrowserWindow): Promise { accelerator: 'CmdOrCtrl+A', role: 'selectAll', }, - { type: 'separator' }, - { - icon: getIconBasedOnTheme( - 'icons/search-white.png', - 'icons/search-black.png', - ), - label: 'Search for Files and Directories', - accelerator: 'CmdOrCtrl+F', - click(): void { - if (isFileLoaded(getGlobalBackendState())) { - webContents.send(AllowedFrontendChannels.ShowSearchPopup, { - showSearchPopup: true, - }); - } - }, - }, - { - icon: getIconBasedOnTheme( - 'icons/location-searching-white.png', - 'icons/location-searching-black.png', - ), - label: 'Locate Signals', - accelerator: 'CmdOrCtrl+L', - click(): void { - if (isFileLoaded(getGlobalBackendState())) { - webContents.send(AllowedFrontendChannels.ShowLocatorPopup, { - showSearchPopup: true, - }); - } - }, - }, ], }, { @@ -356,7 +325,7 @@ export async function createMenu(mainWindow: BrowserWindow): Promise { ); void UserSettings.set('qaMode', false); }, - visible: qaMode, + visible: !!qaMode, }, ], }, diff --git a/src/ElectronBackend/main/user-settings.ts b/src/ElectronBackend/main/user-settings.ts index e4347509d..d6805d61c 100644 --- a/src/ElectronBackend/main/user-settings.ts +++ b/src/ElectronBackend/main/user-settings.ts @@ -4,26 +4,14 @@ // SPDX-License-Identifier: Apache-2.0 import { BrowserWindow } from 'electron'; import settings from 'electron-settings'; -import { isEqual } from 'lodash'; import { AllowedFrontendChannels } from '../../shared/ipc-channels'; import { UserSettings as IUserSettings } from '../../shared/shared-types'; export class UserSettings { public static async init() { - const current: Partial = await settings.get(); - const reset = process.argv.includes('--reset'); - - const updated = { - ...current, - showProjectStatistics: reset - ? false - : current.showProjectStatistics ?? true, - qaMode: reset ? false : current.qaMode ?? false, - } satisfies IUserSettings; - - if (!isEqual(current, updated)) { - await settings.set(updated); + if (process.argv.includes('--reset')) { + await settings.set({}); } } diff --git a/src/ElectronBackend/preload.ts b/src/ElectronBackend/preload.ts index fbf0391f1..ea8dc6120 100644 --- a/src/ElectronBackend/preload.ts +++ b/src/ElectronBackend/preload.ts @@ -9,6 +9,8 @@ import { IpcChannel } from '../shared/ipc-channels'; import { ElectronAPI } from '../shared/shared-types'; const electronAPI: ElectronAPI = { + quit: () => ipcRenderer.invoke(IpcChannel.Quit), + relaunch: () => ipcRenderer.invoke(IpcChannel.Relaunch), openLink: (link) => ipcRenderer.invoke(IpcChannel.OpenLink, { link }), openFile: () => ipcRenderer.invoke(IpcChannel.OpenFile), deleteFile: () => ipcRenderer.invoke(IpcChannel.DeleteFile), diff --git a/src/ElectronBackend/types/types.ts b/src/ElectronBackend/types/types.ts index 6477b82c3..29d7066a1 100644 --- a/src/ElectronBackend/types/types.ts +++ b/src/ElectronBackend/types/types.ts @@ -62,7 +62,7 @@ export interface ParsedOpossumOutputFile { }; manualAttributions: RawAttributions; resourcesToAttributions: ResourcesToAttributions; - resolvedExternalAttributions: Set; + resolvedExternalAttributions: Array | undefined; } export interface ParsedOpossumInputAndOutput { diff --git a/src/Frontend/Components/AggregatedAttributionsPanel/AccordionPanel.tsx b/src/Frontend/Components/AggregatedAttributionsPanel/AccordionPanel.tsx deleted file mode 100644 index 8a781fb75..000000000 --- a/src/Frontend/Components/AggregatedAttributionsPanel/AccordionPanel.tsx +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import MuiAccordion from '@mui/material/Accordion'; -import MuiAccordionDetails from '@mui/material/AccordionDetails'; -import MuiAccordionSummary from '@mui/material/AccordionSummary'; -import MuiTypography from '@mui/material/Typography'; -import { isEmpty } from 'lodash'; -import { ReactElement, useMemo, useState } from 'react'; - -import { Attributions } from '../../../shared/shared-types'; -import { PackagePanelTitle } from '../../enums/enums'; -import { PackagePanel } from '../PackagePanel/PackagePanel'; - -const classes = { - expansionPanelExpanded: { - margin: '0px !important', - }, - expansionPanelSummary: { - minHeight: '24px !important', - '& div.MuiAccordionSummary-content': { - margin: '0px', - }, - '& div.MuiAccordionSummary-expandIcon': { - padding: '6px 12px', - }, - padding: '0 12px', - }, - expansionPanelDetails: { - height: '100%', - padding: '0 12px 16px', - }, - expansionPanelTransition: { - '& div.MuiCollapse-root': { transition: 'none' }, - }, -}; - -interface AccordionPanelProps { - attributions: Attributions; - title: PackagePanelTitle; - isAddToPackageEnabled: boolean; - ['aria-label']?: string; -} - -export function AccordionPanel(props: AccordionPanelProps): ReactElement { - const [expanded, setExpanded] = useState(false); - - useMemo(() => { - setExpanded(!isEmpty(props.attributions)); - }, [props.attributions]); - - function handleExpansionChange( - _: React.ChangeEvent, - targetExpansionState: boolean, - ): void { - setExpanded(targetExpansionState); - } - - return ( - - } - > - {props.title} - - - - - - ); -} diff --git a/src/Frontend/Components/AggregatedAttributionsPanel/AggregatedAttributionsPanel.tsx b/src/Frontend/Components/AggregatedAttributionsPanel/AggregatedAttributionsPanel.tsx deleted file mode 100644 index 989c9132c..000000000 --- a/src/Frontend/Components/AggregatedAttributionsPanel/AggregatedAttributionsPanel.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import { memo, ReactElement } from 'react'; - -import { PackagePanelTitle } from '../../enums/enums'; -import { useAppSelector } from '../../state/hooks'; -import { getExternalData } from '../../state/selectors/all-views-resource-selectors'; -import { getSelectedResourceId } from '../../state/selectors/audit-view-resource-selectors'; -import { - useAttributionsInFolderContent, - useSignalsInFolderContent, -} from '../../state/variables/use-attributions-in-folder-content'; -import { isIdOfResourceWithChildren } from '../../util/can-resource-have-children'; -import { getExternalAttributionIdsWithCount } from '../../util/get-contained-packages'; -import { AccordionPanel } from './AccordionPanel'; -import { SyncAccordionPanel } from './SyncAccordionPanel'; - -interface AggregatedAttributionsPanelProps { - isAddToPackageEnabled: boolean; -} - -export const AggregatedAttributionsPanel = memo( - (props: AggregatedAttributionsPanelProps): ReactElement => { - const externalData = useAppSelector(getExternalData); - const selectedResourceId = useAppSelector(getSelectedResourceId); - - const [attributionsInFolderContent] = useAttributionsInFolderContent(); - const [signalsInFolderContent] = useSignalsInFolderContent(); - - return ( - <> - - getExternalAttributionIdsWithCount( - externalData.resourcesToAttributions[selectedResourceId] || [], - ) - } - attributions={externalData.attributions} - isAddToPackageEnabled={props.isAddToPackageEnabled} - aria-label={'signals panel'} - /> - {isIdOfResourceWithChildren(selectedResourceId) ? ( - <> - - - - ) : null} - - ); - }, -); diff --git a/src/Frontend/Components/AggregatedAttributionsPanel/SyncAccordionPanel.tsx b/src/Frontend/Components/AggregatedAttributionsPanel/SyncAccordionPanel.tsx deleted file mode 100644 index 1aaf3003c..000000000 --- a/src/Frontend/Components/AggregatedAttributionsPanel/SyncAccordionPanel.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import { useMemo } from 'react'; - -import { Attributions, PackageInfo } from '../../../shared/shared-types'; -import { PackagePanelTitle } from '../../enums/enums'; -import { useSignalSorting } from '../../state/variables/use-active-sorting'; -import { AttributionIdWithCount } from '../../types/types'; -import { sortAttributions } from '../../util/sort-attributions'; -import { AccordionPanel } from './AccordionPanel'; - -interface SyncAccordionPanelProps { - title: PackagePanelTitle; - getAttributionIdsWithCount(): Array; - attributions: Attributions; - isAddToPackageEnabled: boolean; - ['aria-label']?: string; -} - -export function SyncAccordionPanel(props: SyncAccordionPanelProps) { - const { signalSorting } = useSignalSorting(); - - const attributions = useMemo( - () => - sortAttributions({ - sorting: signalSorting, - attributions: props - .getAttributionIdsWithCount() - .map(({ attributionId, count }) => ({ - ...props.attributions[attributionId], - count, - })), - }), - [props, signalSorting], - ); - - return ( - - ); -} diff --git a/src/Frontend/Components/AllAttributionsPanel/AllAttributionsPanel.tsx b/src/Frontend/Components/AllAttributionsPanel/AllAttributionsPanel.tsx deleted file mode 100644 index 40019dd18..000000000 --- a/src/Frontend/Components/AllAttributionsPanel/AllAttributionsPanel.tsx +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import MuiPaper from '@mui/material/Paper'; -import { ReactElement, useMemo } from 'react'; - -import { Attributions } from '../../../shared/shared-types'; -import { PackagePanelTitle } from '../../enums/enums'; -import { OpossumColors } from '../../shared-styles'; -import { selectPackageCardInAuditViewOrOpenUnsavedPopup } from '../../state/actions/popup-actions/popup-actions'; -import { addToSelectedResource } from '../../state/actions/resource-actions/save-actions'; -import { useAppDispatch } from '../../state/hooks'; -import { useSignalSorting } from '../../state/variables/use-active-sorting'; -import { PackageCardConfig } from '../../types/types'; -import { sortAttributions } from '../../util/sort-attributions'; -import { PackageCard } from '../PackageCard/PackageCard'; -import { PackageList } from '../PackageList/PackageList'; - -const classes = { - root: { - padding: '10px', - backgroundColor: OpossumColors.white, - display: 'flex', - flexDirection: 'column', - flex: 1, - }, -}; - -interface AllAttributionsPanelProps { - displayPackageInfos: Attributions; - selectedPackageCardId?: string; - isAddToPackageEnabled: boolean; -} - -export function AllAttributionsPanel( - props: AllAttributionsPanelProps, -): ReactElement { - const dispatch = useAppDispatch(); - const { signalSorting } = useSignalSorting(); - - function getPackageCard(packageCardId: string): ReactElement | null { - const displayPackageInfo = props.displayPackageInfos[packageCardId]; - - function onCardClick(): void { - dispatch( - selectPackageCardInAuditViewOrOpenUnsavedPopup( - PackagePanelTitle.AllAttributions, - packageCardId, - displayPackageInfo, - ), - ); - } - - function onAddClick(): void { - dispatch(addToSelectedResource(displayPackageInfo)); - } - - const cardConfig: PackageCardConfig = { - isSelected: packageCardId === props.selectedPackageCardId, - isPreSelected: Boolean(displayPackageInfo.preSelected), - }; - - return ( - - ); - } - - const sortedAttributions = useMemo( - () => - Object.values( - sortAttributions({ - attributions: props.displayPackageInfos, - sorting: signalSorting, - }), - ), - [props.displayPackageInfos, signalSorting], - ); - - return ( - - - - ); -} diff --git a/src/Frontend/Components/AllAttributionsPanel/__tests__/AllAttributionsPanel.test.tsx b/src/Frontend/Components/AllAttributionsPanel/__tests__/AllAttributionsPanel.test.tsx deleted file mode 100644 index 5ba94c487..000000000 --- a/src/Frontend/Components/AllAttributionsPanel/__tests__/AllAttributionsPanel.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import { screen } from '@testing-library/react'; - -import { Attributions } from '../../../../shared/shared-types'; -import { setSelectedResourceId } from '../../../state/actions/resource-actions/audit-view-simple-actions'; -import { loadFromFile } from '../../../state/actions/resource-actions/load-actions'; -import { getParsedInputFileEnrichedWithTestData } from '../../../test-helpers/general-test-helpers'; -import { renderComponent } from '../../../test-helpers/render'; -import { AllAttributionsPanel } from '../AllAttributionsPanel'; - -describe('The AllAttributionsPanel', () => { - const testManualAttributionUuid1 = '374ba87a-f68b-11ea-adc1-0242ac120002'; - const testManualAttributionUuid2 = '374bac4e-f68b-11ea-adc1-0242ac120002'; - const testManualAttributionUuid3 = '374bar8a-f68b-11ea-adc1-0242ac120002'; - const testManualAttributions: Attributions = { - [testManualAttributionUuid1]: { - packageVersion: '1.0', - packageName: 'Typescript', - licenseText: ' test License text', - id: testManualAttributionUuid1, - }, - [testManualAttributionUuid2]: { - packageVersion: '2.0', - packageName: 'React', - licenseText: ' test license text', - id: testManualAttributionUuid2, - }, - [testManualAttributionUuid3]: { - packageVersion: '3.0', - packageName: 'Vue', - licenseText: ' test license text', - id: testManualAttributionUuid3, - }, - }; - - it('renders non-empty list', () => { - const testDisplayPackageInfos: Attributions = { - [testManualAttributionUuid1]: { - packageName: 'name 1', - id: testManualAttributionUuid1, - }, - - [testManualAttributionUuid2]: { - packageName: 'name 2', - id: testManualAttributionUuid2, - }, - }; - renderComponent( - , - { - actions: [ - loadFromFile( - getParsedInputFileEnrichedWithTestData({ - manualAttributions: testDisplayPackageInfos, - }), - ), - ], - }, - ); - expect(screen.getByText('name 1')).toBeInTheDocument(); - expect(screen.getByText('name 2')).toBeInTheDocument(); - }); - - it('does not show resource attribution of selected resource and next attributed parent', () => { - const { store } = renderComponent( - , - { - actions: [ - loadFromFile( - getParsedInputFileEnrichedWithTestData({ - manualAttributions: testManualAttributions, - }), - ), - ], - }, - ); - - store.dispatch(setSelectedResourceId('/root/')); - expect(screen.getByText('Typescript, 1.0')).toBeInTheDocument(); - expect(screen.getByText('React, 2.0')).toBeInTheDocument(); - expect(screen.getByText('Vue, 3.0')).toBeInTheDocument(); - }); -}); diff --git a/src/Frontend/Components/App/App.style.ts b/src/Frontend/Components/App/App.style.ts new file mode 100644 index 000000000..08aa00938 --- /dev/null +++ b/src/Frontend/Components/App/App.style.ts @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import { createTheme, styled } from '@mui/material'; +import MuiBox from '@mui/material/Box'; +import MuiTypography from '@mui/material/Typography'; + +import { OpossumColors } from '../../shared-styles'; + +export const TitleTypography = styled(MuiTypography)({ + color: OpossumColors.mediumGrey, + opacity: 0.5, + marginBottom: '200px', + fontWeight: 900, + userSelect: 'none', +}); + +export const TitleContainer = styled(MuiBox)({ + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); + +export const ViewContainer = styled(MuiBox)({ + display: 'flex', + height: '100vh', + flexDirection: 'column', + background: OpossumColors.lightGrey, + backgroundImage: 'url("icons/wave.svg")', + backgroundPosition: 'bottom', + backgroundRepeat: 'no-repeat', +}); + +export const theme = createTheme({ + typography: { + fontFamily: ['Karla Variable', 'sans-serif'].join(','), + body1: { + fontSize: '14px', + lineHeight: '20px', + }, + body2: { + fontSize: '14px', + lineHeight: '18px', + }, + caption: { + fontSize: '12px', + lineHeight: '20px', + }, + }, + palette: { + primary: { + main: OpossumColors.darkBlue, + }, + secondary: { + main: OpossumColors.white, + contrastText: OpossumColors.darkGrey, + }, + error: { + main: OpossumColors.red, + }, + warning: { + main: OpossumColors.mediumOrange, + }, + success: { + main: OpossumColors.green, + contrastText: OpossumColors.white, + }, + }, + components: { + MuiSwitch: { + styleOverrides: { + switchBase: { + color: OpossumColors.lightestBlue, + }, + colorPrimary: { + '&.Mui-checked': { + color: OpossumColors.middleBlue, + }, + }, + track: { + opacity: 0.7, + backgroundColor: OpossumColors.lightestBlue, + '.Mui-checked.Mui-checked + &': { + opacity: 0.7, + backgroundColor: OpossumColors.middleBlue, + }, + }, + }, + }, + }, +}); diff --git a/src/Frontend/Components/App/App.tsx b/src/Frontend/Components/App/App.tsx index a83cb0ce7..9293bf2c6 100644 --- a/src/Frontend/Components/App/App.tsx +++ b/src/Frontend/Components/App/App.tsx @@ -2,134 +2,64 @@ // SPDX-FileCopyrightText: TNG Technology Consulting GmbH // // SPDX-License-Identifier: Apache-2.0 -import { createTheme } from '@mui/material'; -import MuiBox from '@mui/material/Box'; +import '@fontsource-variable/karla'; import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles'; -import { ReactElement } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import { View } from '../../enums/enums'; -import { OpossumColors } from '../../shared-styles'; import { useAppSelector } from '../../state/hooks'; +import { getResources } from '../../state/selectors/resource-selectors'; import { getSelectedView } from '../../state/selectors/view-selector'; +import { usePanelSizes } from '../../state/variables/use-panel-sizes'; import { useSignalsWorker } from '../../web-workers/use-signals-worker'; -import { AttributionView } from '../AttributionView/AttributionView'; import { AuditView } from '../AuditView/AuditView'; -import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary'; +import { ErrorFallback } from '../ErrorFallback/ErrorFallback'; import { GlobalPopup } from '../GlobalPopup/GlobalPopup'; import { ProcessPopup } from '../ProcessPopup/ProcessPopup'; import { ReportView } from '../ReportView/ReportView'; import { TopBar } from '../TopBar/TopBar'; +import { + theme, + TitleContainer, + TitleTypography, + ViewContainer, +} from './App.style'; -const classes = { - root: { - width: '100vw', - height: '100vh', - }, - panelDiv: { - display: 'flex', - height: 'calc(100vh - 36px)', - width: '100%', - overflow: 'hidden', - }, - spinner: { - margin: 'auto', - }, -}; - -const theme = createTheme({ - palette: { - primary: { - main: OpossumColors.darkBlue, - }, - secondary: { - main: OpossumColors.white, - }, - error: { - main: OpossumColors.red, - }, - warning: { - main: OpossumColors.orange, - }, - success: { - main: OpossumColors.green, - }, - }, - components: { - MuiTypography: { - styleOverrides: { - body1: { - fontSize: '0.85rem', - letterSpacing: '0.01071em', - }, - }, - }, - MuiInputBase: { - styleOverrides: { - root: { - fontSize: '0.85rem', - letterSpacing: '0.01071em', - }, - }, - }, - MuiFormLabel: { - styleOverrides: { - root: { - fontSize: '0.85rem', - letterSpacing: '0.01071em', - }, - }, - }, - MuiSwitch: { - styleOverrides: { - switchBase: { - color: OpossumColors.lightestBlue, - }, - colorPrimary: { - '&.Mui-checked': { - color: OpossumColors.middleBlue, - }, - }, - track: { - opacity: 0.7, - backgroundColor: OpossumColors.lightestBlue, - '.Mui-checked.Mui-checked + &': { - opacity: 0.7, - backgroundColor: OpossumColors.middleBlue, - }, - }, - }, - }, - }, -}); - -export function App(): ReactElement { - useSignalsWorker(); - +export function App() { + const resources = useAppSelector(getResources); const selectedView = useAppSelector(getSelectedView); - function getSelectedViewContainer(): ReactElement { - switch (selectedView) { - case View.Audit: - return ; - case View.Attribution: - return ; - case View.Report: - return ; - } - } + useSignalsWorker(); + usePanelSizes(); // pre-hydrate size of panels return ( - - - - - - + + + + + + - {getSelectedViewContainer()} - - - - + {renderView()} + + + + ); + + function renderView() { + if (!resources) { + return ( + + {'OpossumUI'} + + ); + } + + if (selectedView === View.Audit) { + return ; + } + + return ; + } } diff --git a/src/Frontend/Components/AttributionColumn/AttributionColumn.tsx b/src/Frontend/Components/AttributionColumn/AttributionColumn.tsx deleted file mode 100644 index c2f3334ea..000000000 --- a/src/Frontend/Components/AttributionColumn/AttributionColumn.tsx +++ /dev/null @@ -1,197 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// SPDX-FileCopyrightText: Nico Carl -// -// SPDX-License-Identifier: Apache-2.0 -import MuiBox from '@mui/material/Box'; -import MuiDialogContentText from '@mui/material/DialogContentText'; -import MuiToggleButton from '@mui/material/ToggleButton'; -import { ReactElement, useEffect } from 'react'; - -import { AllowedFrontendChannels } from '../../../shared/ipc-channels'; -import { text } from '../../../shared/text'; -import { AllowedSaveOperations, ButtonText, View } from '../../enums/enums'; -import { OpossumColors } from '../../shared-styles'; -import { - addResolvedExternalAttribution, - removeResolvedExternalAttribution, -} from '../../state/actions/resource-actions/audit-view-simple-actions'; -import { - saveManualAndResolvedAttributionsToFile, - setAllowedSaveOperations, -} from '../../state/actions/resource-actions/save-actions'; -import { useAppDispatch, useAppSelector } from '../../state/hooks'; -import { - getTemporaryDisplayPackageInfo, - wereTemporaryDisplayPackageInfoModified, -} from '../../state/selectors/all-views-resource-selectors'; -import { getResolvedExternalAttributions } from '../../state/selectors/audit-view-resource-selectors'; -import { getSelectedView } from '../../state/selectors/view-selector'; -import { - ResetStateListener, - useIpcRenderer, -} from '../../util/use-ipc-renderer'; -import { - ConfirmationDialog, - useConfirmationDialog, -} from '../ConfirmationDialog/ConfirmationDialog'; -import { WasPreferredIcon } from '../Icons/Icons'; -import { AttributionForm } from './AttributionForm'; -import { ButtonRow } from './ButtonRow'; - -const classes = { - root: { - display: 'flex', - flexDirection: 'column', - width: '100%', - }, - buttonsContainer: { - display: 'flex', - justifyContent: 'flex-end', - margin: '8px', - }, - showHideButton: { - height: '40px', - minWidth: '100px', - background: OpossumColors.lightBlue, - color: OpossumColors.black, - '&:hover': { - background: OpossumColors.lightBlueOnHover, - }, - '&.Mui-selected': { - background: OpossumColors.darkBlue, - color: OpossumColors.white, - }, - }, -}; - -interface AttributionColumnProps { - isEditable: boolean; - areButtonsHidden?: boolean; - showSaveGloballyButton?: boolean; - hideDeleteButtons?: boolean; - showParentAttributions?: boolean; - showHideButton?: boolean; - onSaveButtonClick?(): void; - onSaveGloballyButtonClick?(): void; - onDeleteButtonClick?(): void; - onDeleteGloballyButtonClick?(): void; - saveFileRequestListener(): void; -} - -export function AttributionColumn(props: AttributionColumnProps): ReactElement { - const dispatch = useAppDispatch(); - const resolvedExternalAttributions = useAppSelector( - getResolvedExternalAttributions, - ); - const temporaryDisplayPackageInfo = useAppSelector( - getTemporaryDisplayPackageInfo, - ); - const [confirmEditWasPreferredRef, confirmEditWasPreferred] = - useConfirmationDialog({ - skip: !temporaryDisplayPackageInfo.wasPreferred, - }); - const packageInfoWereModified = useAppSelector( - wereTemporaryDisplayPackageInfoModified, - ); - const view = useAppSelector(getSelectedView); - - useIpcRenderer( - AllowedFrontendChannels.SaveFileRequest, - () => props.saveFileRequestListener(), - [props.saveFileRequestListener], - ); - - const showHighlight = - view === View.Attribution && - !temporaryDisplayPackageInfo.firstParty && - !temporaryDisplayPackageInfo.excludeFromNotice; - - const selectedPackageIsResolved = resolvedExternalAttributions.has( - temporaryDisplayPackageInfo.id, - ); - - useEffect(() => { - dispatch( - setAllowedSaveOperations( - packageInfoWereModified || temporaryDisplayPackageInfo.preSelected - ? AllowedSaveOperations.All - : AllowedSaveOperations.None, - ), - ); - }, [ - dispatch, - packageInfoWereModified, - temporaryDisplayPackageInfo.preSelected, - ]); - - return ( - <> - - - - {props.showHideButton ? ( - renderShowHideButton() - ) : ( - - )} - - - {renderConfirmationDialog()} - - ); - - function renderShowHideButton() { - return ( - { - dispatch( - resolvedExternalAttributions.has(temporaryDisplayPackageInfo.id) - ? removeResolvedExternalAttribution( - temporaryDisplayPackageInfo.id, - ) - : addResolvedExternalAttribution(temporaryDisplayPackageInfo.id), - ); - dispatch(saveManualAndResolvedAttributionsToFile()); - }} - sx={classes.showHideButton} - aria-label={'resolve attribution'} - > - {selectedPackageIsResolved ? ButtonText.Unhide : ButtonText.Hide} - - ); - } - - function renderConfirmationDialog() { - return ( - - {text.modifyWasPreferredPopup.message} - - {'.'} - - } - title={text.modifyWasPreferredPopup.header} - /> - ); - } -} diff --git a/src/Frontend/Components/AttributionColumn/ButtonRow.tsx b/src/Frontend/Components/AttributionColumn/ButtonRow.tsx deleted file mode 100644 index c53311c81..000000000 --- a/src/Frontend/Components/AttributionColumn/ButtonRow.tsx +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import MuiBox from '@mui/material/Box'; -import { useMemo, useState } from 'react'; - -import { PackageInfo } from '../../../shared/shared-types'; -import { text } from '../../../shared/text'; -import { ButtonText } from '../../enums/enums'; -import { EMPTY_DISPLAY_PACKAGE_INFO } from '../../shared-constants'; -import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions'; -import { useAppDispatch, useAppSelector } from '../../state/hooks'; -import { - getExternalAttributions, - getIsGlobalSavingDisabled, - getIsSavingDisabled, - getManualDisplayPackageInfoOfSelected, - wereTemporaryDisplayPackageInfoModified, -} from '../../state/selectors/all-views-resource-selectors'; -import { Button, ButtonProps } from '../Button/Button'; -import { DiffPopup } from '../DiffPopup/DiffPopup'; -import { SplitButton } from '../SplitButton/SplitButton'; - -interface ButtonRowProps { - areButtonsHidden?: boolean; - packageInfo: PackageInfo; - onSaveButtonClick?(): void; - onSaveGloballyButtonClick?(): void; - onDeleteButtonClick?(): void; - onDeleteGloballyButtonClick?(): void; - showSaveGloballyButton?: boolean; - hideDeleteButtons?: boolean; - additionalActions?: Array; -} - -export function ButtonRow({ - packageInfo, - areButtonsHidden, - onDeleteButtonClick, - onDeleteGloballyButtonClick, - onSaveButtonClick, - onSaveGloballyButtonClick, - hideDeleteButtons, - showSaveGloballyButton, - additionalActions = [], -}: ButtonRowProps): React.ReactNode { - const dispatch = useAppDispatch(); - const packageInfoWereModified = useAppSelector( - wereTemporaryDisplayPackageInfoModified, - ); - const initialManualDisplayPackageInfo = useAppSelector( - getManualDisplayPackageInfoOfSelected, - ); - const isSavingDisabled = useAppSelector(getIsSavingDisabled); - const isGlobalSavingDisabled = useAppSelector(getIsGlobalSavingDisabled); - const externalAttributions = useAppSelector(getExternalAttributions); - const [isDiffPopupOpen, setIsDiffPopupOpen] = useState(false); - - const originalDisplayPackageInfo = useMemo( - () => - !!packageInfo.originIds?.length - ? Object.values(externalAttributions).find(({ originIds }) => - originIds?.some((id) => packageInfo.originIds?.includes(id)), - ) - : undefined, - [externalAttributions, packageInfo.originIds], - ); - - return ( - !areButtonsHidden && ( - - {renderSaveButton()} - {renderDeleteButton()} - {renderRevertButton()} - {renderCompareButton()} - {renderAdditionalActions()} - {renderDiffPopup()} - - ) - ); - - function renderSaveButton() { - return ( - { - onSaveButtonClick?.(); - }, - hidden: !onSaveButtonClick, - }, - { - buttonText: packageInfo.preSelected - ? ButtonText.ConfirmGlobally - : ButtonText.SaveGlobally, - disabled: isGlobalSavingDisabled, - onClick: () => { - onSaveGloballyButtonClick?.(); - }, - hidden: !onSaveGloballyButtonClick || !showSaveGloballyButton, - }, - ]} - /> - ); - } - - function renderDeleteButton() { - return ( - onDeleteButtonClick?.(), - hidden: !onDeleteButtonClick || hideDeleteButtons, - }, - { - buttonText: ButtonText.DeleteGlobally, - onClick: () => onDeleteGloballyButtonClick?.(), - hidden: - !onDeleteGloballyButtonClick || - hideDeleteButtons || - !showSaveGloballyButton, - }, - ]} - /> - ); - } - - function renderRevertButton() { - return ( - + )} + , + ); + + await userEvent.click(screen.getByRole('button', { name: 'click me' })); + + expect( + screen.getByRole('checkbox', { name: 'select all' }), + ).toHaveAttribute('data-indeterminate', 'true'); + }); + + it('checkbox is not indeterminate when no attributions are selected', () => { + const packageInfo1 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo2 = faker.opossum.packageInfo({ relation: 'resource' }); + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + }), + }, + setFilteredData, + ]; + renderComponent( + null} + useFilteredData={useFilteredData} + > + {(props) => ( + + )} + , + ); + + expect( + screen.getByRole('checkbox', { name: 'select all' }), + ).toHaveAttribute('data-indeterminate', 'false'); + }); + + it('checkbox is not indeterminate when all attributions of active relation are selected', async () => { + const packageInfo1 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo2 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo3 = faker.opossum.packageInfo({ relation: 'unrelated' }); + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + [packageInfo3.id]: packageInfo3, + }), + }, + setFilteredData, + ]; + renderComponent( + null} + useFilteredData={useFilteredData} + > + {(props) => ( + + )} + , + ); + + await userEvent.click(screen.getByRole('button', { name: 'click me' })); + + expect( + screen.getByRole('checkbox', { name: 'select all' }), + ).toHaveAttribute('data-indeterminate', 'false'); + }); + + it('selects all attributions', async () => { + const packageInfo1 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo2 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo3 = faker.opossum.packageInfo({ relation: 'unrelated' }); + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + [packageInfo3.id]: packageInfo3, + }), + }, + setFilteredData, + ]; + renderComponent( + null} + useFilteredData={useFilteredData} + > + {() => null} + , + ); + + await userEvent.click(screen.getByRole('checkbox', { name: 'select all' })); + + expect(screen.getByRole('checkbox', { name: 'select all' })).toBeChecked(); + }); + + it('resets multi-selected IDs when active relation changes', async () => { + const packageInfo1 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo2 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo3 = faker.opossum.packageInfo({ relation: 'unrelated' }); + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + [packageInfo3.id]: packageInfo3, + }), + }, + setFilteredData, + ]; + renderComponent( + null} + useFilteredData={useFilteredData} + > + {() => null} + , + ); + + await userEvent.click(screen.getByRole('checkbox', { name: 'select all' })); + await userEvent.click( + screen.getByRole('tab', { name: new RegExp(text.relations.unrelated) }), + ); + + expect( + screen.getByRole('checkbox', { name: 'select all' }), + ).not.toBeChecked(); + }); + + it('does not reset multi-selected IDs when active relation changes and in replacement mode', async () => { + const packageInfo1 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo2 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo3 = faker.opossum.packageInfo({ relation: 'unrelated' }); + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + [packageInfo3.id]: packageInfo3, + }), + }, + setFilteredData, + ]; + renderComponent( + null} + useFilteredData={useFilteredData} + > + {() => null} + , + { + actions: [ + setVariable>(ATTRIBUTION_IDS_FOR_REPLACEMENT, [ + packageInfo1.id, + ]), + ], + }, + ); + + await userEvent.click(screen.getByRole('checkbox', { name: 'select all' })); + await userEvent.click( + screen.getByRole('tab', { name: new RegExp(text.relations.unrelated) }), + ); + + expect( + screen.getByRole('checkbox', { name: 'select all' }), + ).toHaveAttribute('data-indeterminate', 'true'); + }); + + it('adjusts multi-selected IDs when previously visible attributions become invisible', async () => { + const packageInfo1 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo2 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo3 = faker.opossum.packageInfo({ relation: 'unrelated' }); + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + [packageInfo3.id]: packageInfo3, + }), + }, + setFilteredData, + ]; + const { rerender } = renderComponent( + null} + useFilteredData={useFilteredData} + > + {(props) => ( + + )} + , + { + actions: [ + setVariable>(ATTRIBUTION_IDS_FOR_REPLACEMENT, [ + packageInfo1.id, + ]), + ], + }, + ); + + await userEvent.click(screen.getByRole('button', { name: 'click me' })); + + expect( + screen.getByRole('checkbox', { name: 'select all' }), + ).toHaveAttribute('data-indeterminate', 'true'); + + const updatedUseFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo2.id]: packageInfo2, + [packageInfo3.id]: packageInfo3, + }), + }, + setFilteredData, + ]; + + rerender( + null} + useFilteredData={updatedUseFilteredData} + > + {() => null} + , + ); + + expect( + screen.getByRole('checkbox', { name: 'select all' }), + ).not.toBeChecked(); + expect( + screen.getByRole('checkbox', { name: 'select all' }), + ).toHaveAttribute('data-indeterminate', 'false'); + }); + + it('shows tabs corresponding to available attributions', () => { + const packageInfo1 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo2 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo3 = faker.opossum.packageInfo({ relation: 'unrelated' }); + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + [packageInfo3.id]: packageInfo3, + }), + }, + setFilteredData, + ]; + renderComponent( + null} + useFilteredData={useFilteredData} + > + {() => null} + , + ); + + expect( + screen.getByRole('tab', { name: new RegExp(text.relations.resource) }), + ).toBeInTheDocument(); + expect( + screen.getByRole('tab', { name: new RegExp(text.relations.unrelated) }), + ).toBeInTheDocument(); + expect( + screen.queryByRole('tab', { name: new RegExp(text.relations.children) }), + ).not.toBeInTheDocument(); + expect( + screen.getByRole('tab', { name: new RegExp(text.relations.resource) }), + ).toHaveAttribute('aria-selected', 'true'); + expect( + screen.getByRole('tab', { name: new RegExp(text.relations.unrelated) }), + ).toHaveAttribute('aria-selected', 'false'); + }); + + it('switches tabs', async () => { + const packageInfo1 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo2 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo3 = faker.opossum.packageInfo({ relation: 'unrelated' }); + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + [packageInfo3.id]: packageInfo3, + }), + }, + setFilteredData, + ]; + renderComponent( + null} + useFilteredData={useFilteredData} + > + {() => null} + , + ); + + await userEvent.click( + screen.getByRole('tab', { name: new RegExp(text.relations.unrelated) }), + ); + + expect( + screen.getByRole('tab', { name: new RegExp(text.relations.resource) }), + ).toHaveAttribute('aria-selected', 'false'); + expect( + screen.getByRole('tab', { name: new RegExp(text.relations.unrelated) }), + ).toHaveAttribute('aria-selected', 'true'); + }); + + it('sets active tab to the one containing the selected attribution', () => { + const packageInfo1 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo2 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo3 = faker.opossum.packageInfo({ relation: 'unrelated' }); + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + [packageInfo3.id]: packageInfo3, + }), + }, + setFilteredData, + ]; + renderComponent( + null} + useFilteredData={useFilteredData} + > + {() => null} + , + { actions: [setSelectedAttributionId(packageInfo3.id)] }, + ); + + expect( + screen.getByRole('tab', { name: new RegExp(text.relations.resource) }), + ).toHaveAttribute('aria-selected', 'false'); + expect( + screen.getByRole('tab', { name: new RegExp(text.relations.unrelated) }), + ).toHaveAttribute('aria-selected', 'true'); + }); + + it('resets active tab when active relation no longer available', () => { + const packageInfo1 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo2 = faker.opossum.packageInfo({ relation: 'resource' }); + const packageInfo3 = faker.opossum.packageInfo({ relation: 'unrelated' }); + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + [packageInfo3.id]: packageInfo3, + }), + }, + setFilteredData, + ]; + const { rerender } = renderComponent( + null} + useFilteredData={useFilteredData} + > + {() => null} + , + { actions: [setSelectedAttributionId(packageInfo3.id)] }, + ); + + expect( + screen.getByRole('tab', { name: new RegExp(text.relations.resource) }), + ).toHaveAttribute('aria-selected', 'false'); + + const updatedUseFilteredData: UseFilteredData = () => [ + { + ...initialFilteredAttributions, + attributions: faker.opossum.attributions({ + [packageInfo1.id]: packageInfo1, + [packageInfo2.id]: packageInfo2, + }), + }, + setFilteredData, + ]; + rerender( + null} + useFilteredData={updatedUseFilteredData} + > + {() => null} + , + ); + + expect( + screen.getByRole('tab', { name: new RegExp(text.relations.resource) }), + ).toHaveAttribute('aria-selected', 'true'); + }); + + it('renders no tabs when there are no attributions', () => { + const setFilteredData = jest.fn(); + const useFilteredData: UseFilteredData = () => [ + { ...initialFilteredAttributions, attributions: {} }, + setFilteredData, + ]; + renderComponent( + null} + useFilteredData={useFilteredData} + > + {() => null} + , + ); + + expect(screen.queryByRole('tab')).not.toBeInTheDocument(); + }); +}); diff --git a/src/Frontend/Components/AttributionPanels/SignalsPanel/DeleteButton/DeleteButton.tsx b/src/Frontend/Components/AttributionPanels/SignalsPanel/DeleteButton/DeleteButton.tsx new file mode 100644 index 000000000..6aedc459b --- /dev/null +++ b/src/Frontend/Components/AttributionPanels/SignalsPanel/DeleteButton/DeleteButton.tsx @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import DeleteIcon from '@mui/icons-material/Delete'; +import MuiIconButton from '@mui/material/IconButton'; +import MuiTooltip from '@mui/material/Tooltip'; +import { useMemo } from 'react'; + +import { text } from '../../../../../shared/text'; +import { addResolvedExternalAttributionAndSave } from '../../../../state/actions/resource-actions/save-actions'; +import { useAppDispatch, useAppSelector } from '../../../../state/hooks'; +import { getResolvedExternalAttributions } from '../../../../state/selectors/resource-selectors'; +import { PackagesPanelChildrenProps } from '../../PackagesPanel/PackagesPanel'; + +export const DeleteButton: React.FC = ({ + selectedAttributionIds, +}) => { + const dispatch = useAppDispatch(); + const resolvedExternalAttributionIds = useAppSelector( + getResolvedExternalAttributions, + ); + const someSelectedAttributionsAreVisible = useMemo( + () => + !!selectedAttributionIds.length && + selectedAttributionIds.some( + (id) => !resolvedExternalAttributionIds.has(id), + ), + [resolvedExternalAttributionIds, selectedAttributionIds], + ); + + return ( + { + dispatch(addResolvedExternalAttributionAndSave(selectedAttributionIds)); + }} + > + + + + + ); +}; diff --git a/src/Frontend/Components/AttributionPanels/SignalsPanel/IncludeExcludeButton/IncludeExcludeButton.tsx b/src/Frontend/Components/AttributionPanels/SignalsPanel/IncludeExcludeButton/IncludeExcludeButton.tsx new file mode 100644 index 000000000..74c1fb116 --- /dev/null +++ b/src/Frontend/Components/AttributionPanels/SignalsPanel/IncludeExcludeButton/IncludeExcludeButton.tsx @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import VisibilityIcon from '@mui/icons-material/Visibility'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; +import MuiIconButton from '@mui/material/IconButton'; +import MuiTooltip from '@mui/material/Tooltip'; +import MuiBox from '@mui/system/Box'; + +import { text } from '../../../../../shared/text'; +import { useAreHiddenSignalsVisible } from '../../../../state/variables/use-are-hidden-signals-visible'; + +export const IncludeExcludeButton: React.FC = () => { + const [areHiddenSignalsVisible, setAreHiddenSignalsVisible] = + useAreHiddenSignalsVisible(); + const label = areHiddenSignalsVisible + ? text.packageLists.hideDeleted + : text.packageLists.showDeleted; + + return ( + setAreHiddenSignalsVisible((prev) => !prev)} + > + + + {areHiddenSignalsVisible ? : } + + + + ); +}; diff --git a/src/Frontend/Components/AttributionPanels/SignalsPanel/LinkButton/LinkButton.tsx b/src/Frontend/Components/AttributionPanels/SignalsPanel/LinkButton/LinkButton.tsx new file mode 100644 index 000000000..d6202c8e7 --- /dev/null +++ b/src/Frontend/Components/AttributionPanels/SignalsPanel/LinkButton/LinkButton.tsx @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import CallMergeIcon from '@mui/icons-material/CallMerge'; +import MuiIconButton from '@mui/material/IconButton'; +import MuiTooltip from '@mui/material/Tooltip'; + +import { text } from '../../../../../shared/text'; +import { addToSelectedResource } from '../../../../state/actions/resource-actions/save-actions'; +import { useAppDispatch, useAppSelector } from '../../../../state/hooks'; +import { getIsSelectedResourceBreakpoint } from '../../../../state/selectors/resource-selectors'; +import { PackagesPanelChildrenProps } from '../../PackagesPanel/PackagesPanel'; + +export const LinkButton: React.FC = ({ + attributions, + selectedAttributionId, + selectedAttributionIds, + setMultiSelectedAttributionIds, +}) => { + const dispatch = useAppDispatch(); + const isSelectedResourceBreakpoint = useAppSelector( + getIsSelectedResourceBreakpoint, + ); + + return ( + { + attributions && + selectedAttributionIds.forEach((attributionId) => { + dispatch( + addToSelectedResource( + attributions[attributionId], + selectedAttributionId, + ), + ); + }); + setMultiSelectedAttributionIds([]); + }} + > + + + + + ); +}; diff --git a/src/Frontend/Components/AttributionPanels/SignalsPanel/RestoreButton/RestoreButton.tsx b/src/Frontend/Components/AttributionPanels/SignalsPanel/RestoreButton/RestoreButton.tsx new file mode 100644 index 000000000..7dae8636a --- /dev/null +++ b/src/Frontend/Components/AttributionPanels/SignalsPanel/RestoreButton/RestoreButton.tsx @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import RestoreFromTrashIcon from '@mui/icons-material/RestoreFromTrash'; +import MuiIconButton from '@mui/material/IconButton'; +import MuiTooltip from '@mui/material/Tooltip'; +import { useMemo } from 'react'; + +import { text } from '../../../../../shared/text'; +import { removeResolvedExternalAttributionAndSave } from '../../../../state/actions/resource-actions/save-actions'; +import { useAppDispatch, useAppSelector } from '../../../../state/hooks'; +import { getResolvedExternalAttributions } from '../../../../state/selectors/resource-selectors'; +import { useAreHiddenSignalsVisible } from '../../../../state/variables/use-are-hidden-signals-visible'; +import { PackagesPanelChildrenProps } from '../../PackagesPanel/PackagesPanel'; + +export const RestoreButton: React.FC = ({ + selectedAttributionIds, +}) => { + const dispatch = useAppDispatch(); + const resolvedExternalAttributionIds = useAppSelector( + getResolvedExternalAttributions, + ); + const [areHiddenSignalsVisible] = useAreHiddenSignalsVisible(); + const someSelectedAttributionsAreHidden = useMemo( + () => + !!selectedAttributionIds.length && + selectedAttributionIds.some((id) => + resolvedExternalAttributionIds.has(id), + ), + [resolvedExternalAttributionIds, selectedAttributionIds], + ); + + if (!areHiddenSignalsVisible) { + return null; + } + + return ( + { + dispatch( + removeResolvedExternalAttributionAndSave(selectedAttributionIds), + ); + }} + > + + + + + ); +}; diff --git a/src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsList/SignalsList.style.ts b/src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsList/SignalsList.style.ts new file mode 100644 index 000000000..d64b63990 --- /dev/null +++ b/src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsList/SignalsList.style.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import { styled } from '@mui/material'; +import MuiTypography from '@mui/material/Typography'; + +export const GroupName = styled(MuiTypography)({ + flex: 1, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + marginTop: '1px', + userSelect: 'none', +}); diff --git a/src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsList/SignalsList.tsx b/src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsList/SignalsList.tsx new file mode 100644 index 000000000..e42b7d26e --- /dev/null +++ b/src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsList/SignalsList.tsx @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import MuiDivider from '@mui/material/Divider'; +import { groupBy as _groupBy, orderBy as _orderBy, without } from 'lodash'; +import { useMemo } from 'react'; + +import { TRANSITION } from '../../../../shared-styles'; +import { changeSelectedAttributionOrOpenUnsavedPopup } from '../../../../state/actions/popup-actions/popup-actions'; +import { useAppDispatch, useAppSelector } from '../../../../state/hooks'; +import { + getExternalAttributionSources, + getResolvedExternalAttributions, +} from '../../../../state/selectors/resource-selectors'; +import { useAttributionIdsForReplacement } from '../../../../state/variables/use-attribution-ids-for-replacement'; +import { GroupedList } from '../../../GroupedList/GroupedList'; +import { SourceIcon } from '../../../Icons/Icons'; +import { PackageCard } from '../../../PackageCard/PackageCard'; +import { PackagesPanelChildrenProps } from '../../PackagesPanel/PackagesPanel'; +import { GroupName } from './SignalsList.style'; + +export const SignalsList: React.FC = ({ + attributions, + activeAttributionIds, + selectedAttributionId, + contentHeight, + loading, + setMultiSelectedAttributionIds, + multiSelectedAttributionIds, +}) => { + const dispatch = useAppDispatch(); + const resolvedExternalAttributionIds = useAppSelector( + getResolvedExternalAttributions, + ); + const sources = useAppSelector(getExternalAttributionSources); + + const [attributionIdsForReplacement] = useAttributionIdsForReplacement(); + const groupedIds = useMemo( + () => + attributions && + activeAttributionIds && + _groupBy( + _orderBy( + activeAttributionIds, + (id) => { + const attribution = attributions[id]; + return ( + attribution && + (attribution.source && sources[attribution.source.name])?.priority + ); + }, + 'desc', + ), + (id) => { + const attribution = attributions[id]; + return ( + attribution?.source && + (sources[attribution.source.name]?.name || attribution.source.name) + ); + }, + ), + [activeAttributionIds, attributions, sources], + ); + + return ( + ( + <> + + {sourceName} + + )} + loading={loading} + sx={{ transition: TRANSITION, height: contentHeight }} + /> + ); + + function renderAttributionCard(attributionId: string) { + const attribution = attributions?.[attributionId]; + + if (!attribution) { + return null; + } + + return ( + <> + { + selectedAttributionId !== attributionId && + dispatch( + changeSelectedAttributionOrOpenUnsavedPopup(attribution), + ); + }} + cardConfig={{ + selected: attributionId === selectedAttributionId, + resolved: resolvedExternalAttributionIds.has(attributionId), + }} + packageInfo={attribution} + checkbox={{ + checked: multiSelectedAttributionIds.includes(attributionId), + disabled: !!attributionIdsForReplacement.length, + onChange: (event) => { + setMultiSelectedAttributionIds( + event.target.checked + ? [...multiSelectedAttributionIds, attributionId] + : without(multiSelectedAttributionIds, attributionId), + ); + !selectedAttributionId && + dispatch( + changeSelectedAttributionOrOpenUnsavedPopup(attribution), + ); + }, + }} + /> + + + ); + } +}; diff --git a/src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsPanel.tsx b/src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsPanel.tsx new file mode 100644 index 000000000..0bd5d44c4 --- /dev/null +++ b/src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsPanel.tsx @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import MuiBox from '@mui/material/Box'; + +import { SIGNAL_FILTERS } from '../../../shared-constants'; +import { useAttributionIdsForReplacement } from '../../../state/variables/use-attribution-ids-for-replacement'; +import { useFilteredSignals } from '../../../state/variables/use-filtered-data'; +import { PackagesPanel } from '../PackagesPanel/PackagesPanel'; +import { DeleteButton } from './DeleteButton/DeleteButton'; +import { IncludeExcludeButton } from './IncludeExcludeButton/IncludeExcludeButton'; +import { LinkButton } from './LinkButton/LinkButton'; +import { RestoreButton } from './RestoreButton/RestoreButton'; +import { SignalsList } from './SignalsList/SignalsList'; + +export function SignalsPanel() { + const [attributionIdsForReplacement] = useAttributionIdsForReplacement(); + + return ( + ( + <> + + + + + + + )} + useFilteredData={useFilteredSignals} + testId={'signals-panel'} + > + {(props) => } + + ); +} diff --git a/src/Frontend/Components/AttributionPanels/SignalsPanel/__tests__/SignalsPanel.test.tsx b/src/Frontend/Components/AttributionPanels/SignalsPanel/__tests__/SignalsPanel.test.tsx new file mode 100644 index 000000000..1349ca9f1 --- /dev/null +++ b/src/Frontend/Components/AttributionPanels/SignalsPanel/__tests__/SignalsPanel.test.tsx @@ -0,0 +1,328 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import { screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { ResourcesToAttributions } from '../../../../../shared/shared-types'; +import { text } from '../../../../../shared/text'; +import { faker } from '../../../../../testing/Faker'; +import { ROOT_PATH } from '../../../../shared-constants'; +import { setProjectMetadata } from '../../../../state/actions/resource-actions/all-views-simple-actions'; +import { + setResolvedExternalAttributions, + setSelectedResourceId, +} from '../../../../state/actions/resource-actions/audit-view-simple-actions'; +import { loadFromFile } from '../../../../state/actions/resource-actions/load-actions'; +import { setVariable } from '../../../../state/actions/variables-actions/variables-actions'; +import { + getResolvedExternalAttributions, + getResourcesToManualAttributions, + getSelectedAttributionId, +} from '../../../../state/selectors/resource-selectors'; +import { + FILTERED_SIGNALS, + FilteredData, + initialFilteredAttributions, +} from '../../../../state/variables/use-filtered-data'; +import { getParsedInputFileEnrichedWithTestData } from '../../../../test-helpers/general-test-helpers'; +import { renderComponent } from '../../../../test-helpers/render'; +import { SignalsPanel } from '../SignalsPanel'; + +describe('SignalsPanel', () => { + it('selects signal on card click', async () => { + const packageInfo = faker.opossum.packageInfo(); + const externalAttributions = faker.opossum.attributions({ + [packageInfo.id]: packageInfo, + }); + const { store } = renderComponent(, { + actions: [ + loadFromFile( + getParsedInputFileEnrichedWithTestData({ + externalAttributions, + }), + ), + setProjectMetadata(faker.opossum.metadata()), + setVariable(FILTERED_SIGNALS, { + ...initialFilteredAttributions, + attributions: { + [packageInfo.id]: packageInfo, + }, + }), + ], + }); + + await userEvent.click( + screen.getByText( + `${packageInfo.packageName}, ${packageInfo.packageVersion}`, + ), + ); + + expect(getSelectedAttributionId(store.getState())).toBe(packageInfo.id); + }); + + it('selects signal on checkbox click', async () => { + const packageInfo = faker.opossum.packageInfo(); + const externalAttributions = faker.opossum.attributions({ + [packageInfo.id]: packageInfo, + }); + const { store } = renderComponent(, { + actions: [ + loadFromFile( + getParsedInputFileEnrichedWithTestData({ + externalAttributions, + }), + ), + setProjectMetadata(faker.opossum.metadata()), + setVariable(FILTERED_SIGNALS, { + ...initialFilteredAttributions, + attributions: { + [packageInfo.id]: packageInfo, + }, + }), + ], + }); + + await userEvent.click( + within( + screen.getByLabelText( + `package card ${packageInfo.packageName}, ${packageInfo.packageVersion}`, + ), + ).getByRole('checkbox'), + ); + + expect(getSelectedAttributionId(store.getState())).toBe(packageInfo.id); + }); + + it('deletes selected signal', async () => { + const packageInfo = faker.opossum.packageInfo(); + const externalAttributions = faker.opossum.attributions({ + [packageInfo.id]: packageInfo, + }); + const { store } = renderComponent(, { + actions: [ + loadFromFile( + getParsedInputFileEnrichedWithTestData({ + externalAttributions, + }), + ), + setProjectMetadata(faker.opossum.metadata()), + setVariable(FILTERED_SIGNALS, { + ...initialFilteredAttributions, + attributions: { + [packageInfo.id]: packageInfo, + }, + }), + ], + }); + + await userEvent.click( + screen.getByText( + `${packageInfo.packageName}, ${packageInfo.packageVersion}`, + ), + ); + await userEvent.click( + screen.getByRole('button', { name: text.packageLists.delete }), + ); + + expect( + getResolvedExternalAttributions(store.getState()).has(packageInfo.id), + ).toBe(true); + }); + + it('disables delete button when selected signal is already deleted', async () => { + const packageInfo = faker.opossum.packageInfo(); + const externalAttributions = faker.opossum.attributions({ + [packageInfo.id]: packageInfo, + }); + renderComponent(, { + actions: [ + loadFromFile( + getParsedInputFileEnrichedWithTestData({ + externalAttributions, + }), + ), + setResolvedExternalAttributions(new Set([packageInfo.id])), + setProjectMetadata(faker.opossum.metadata()), + setVariable(FILTERED_SIGNALS, { + ...initialFilteredAttributions, + attributions: { + [packageInfo.id]: packageInfo, + }, + }), + ], + }); + + await userEvent.click( + screen.getByText( + `${packageInfo.packageName}, ${packageInfo.packageVersion}`, + ), + ); + + expect( + screen.getByRole('button', { name: text.packageLists.delete }), + ).toBeDisabled(); + }); + + it('restores selected signal', async () => { + const packageInfo = faker.opossum.packageInfo(); + const externalAttributions = faker.opossum.attributions({ + [packageInfo.id]: packageInfo, + }); + const { store } = renderComponent(, { + actions: [ + loadFromFile( + getParsedInputFileEnrichedWithTestData({ + externalAttributions, + }), + ), + setResolvedExternalAttributions(new Set([packageInfo.id])), + setProjectMetadata(faker.opossum.metadata()), + setVariable(FILTERED_SIGNALS, { + ...initialFilteredAttributions, + attributions: { + [packageInfo.id]: packageInfo, + }, + }), + ], + }); + + await userEvent.click( + screen.getByRole('button', { name: text.packageLists.showDeleted }), + ); + await userEvent.click( + screen.getByText( + `${packageInfo.packageName}, ${packageInfo.packageVersion}`, + ), + ); + await userEvent.click( + screen.getByRole('button', { name: text.packageLists.restore }), + ); + + expect( + getResolvedExternalAttributions(store.getState()).has(packageInfo.id), + ).toBe(false); + }); + + it('disables restore button when selected signal is not deleted', async () => { + const packageInfo = faker.opossum.packageInfo(); + const externalAttributions = faker.opossum.attributions({ + [packageInfo.id]: packageInfo, + }); + renderComponent(, { + actions: [ + loadFromFile( + getParsedInputFileEnrichedWithTestData({ + externalAttributions, + }), + ), + setProjectMetadata(faker.opossum.metadata()), + setVariable(FILTERED_SIGNALS, { + ...initialFilteredAttributions, + attributions: { + [packageInfo.id]: packageInfo, + }, + }), + ], + }); + + await userEvent.click( + screen.getByRole('button', { name: text.packageLists.showDeleted }), + ); + await userEvent.click( + screen.getByText( + `${packageInfo.packageName}, ${packageInfo.packageVersion}`, + ), + ); + + expect( + screen.getByRole('button', { name: text.packageLists.restore }), + ).toBeDisabled(); + }); + + it('links selected attribution', async () => { + const filePath = faker.system.filePath(); + const packageInfo = faker.opossum.packageInfo(); + const externalAttributions = faker.opossum.attributions({ + [packageInfo.id]: packageInfo, + }); + const { store } = renderComponent(, { + actions: [ + loadFromFile( + getParsedInputFileEnrichedWithTestData({ + externalAttributions, + resourcesToExternalAttributions: { + [filePath]: [packageInfo.id], + }, + }), + ), + setProjectMetadata(faker.opossum.metadata()), + setVariable(FILTERED_SIGNALS, { + ...initialFilteredAttributions, + attributions: { + [packageInfo.id]: packageInfo, + }, + }), + ], + }); + + expect( + getResourcesToManualAttributions(store.getState()), + ).toEqual({}); + + await userEvent.click( + screen.getByText( + `${packageInfo.packageName}, ${packageInfo.packageVersion}`, + ), + ); + await userEvent.click( + screen.getByRole('button', { name: text.packageLists.linkAsAttribution }), + ); + + expect( + getResourcesToManualAttributions(store.getState()), + ).toEqual({ + [ROOT_PATH]: [expect.any(String)], + }); + }); + + it('disables link button when selected resource is a breakpoint', async () => { + const filePath = faker.system.filePath(); + const packageInfo = faker.opossum.packageInfo(); + const externalAttributions = faker.opossum.attributions({ + [packageInfo.id]: packageInfo, + }); + renderComponent(, { + actions: [ + loadFromFile( + getParsedInputFileEnrichedWithTestData({ + externalAttributions, + attributionBreakpoints: new Set([filePath]), + resourcesToExternalAttributions: { + [filePath]: [packageInfo.id], + }, + }), + ), + setSelectedResourceId(filePath), + setProjectMetadata(faker.opossum.metadata()), + setVariable(FILTERED_SIGNALS, { + ...initialFilteredAttributions, + attributions: { + [packageInfo.id]: packageInfo, + }, + }), + ], + }); + + await userEvent.click( + screen.getByText( + `${packageInfo.packageName}, ${packageInfo.packageVersion}`, + ), + ); + + expect( + screen.getByRole('button', { name: text.packageLists.linkAsAttribution }), + ).toBeDisabled(); + }); +}); diff --git a/src/Frontend/Components/AttributionView/AttributionView.tsx b/src/Frontend/Components/AttributionView/AttributionView.tsx deleted file mode 100644 index c31671608..000000000 --- a/src/Frontend/Components/AttributionView/AttributionView.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import MuiBox from '@mui/material/Box'; - -import { OpossumColors } from '../../shared-styles'; -import { AttributionDetailsViewer } from '../AttributionDetailsViewer/AttributionDetailsViewer'; -import { AttributionList } from '../AttributionList/AttributionList'; - -const classes = { - root: { - width: '100%', - display: 'flex', - backgroundColor: OpossumColors.white, - }, -}; - -export function AttributionView() { - return ( - - - - - ); -} diff --git a/src/Frontend/Components/AuditView/AuditView.style.ts b/src/Frontend/Components/AuditView/AuditView.style.ts new file mode 100644 index 000000000..2cf327b87 --- /dev/null +++ b/src/Frontend/Components/AuditView/AuditView.style.ts @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import { styled } from '@mui/material'; +import MuiBox from '@mui/material/Box'; + +export const ColumnsContainer = styled(MuiBox)({ + width: '100%', + height: '100%', + display: 'flex', + overflow: 'hidden', +}); diff --git a/src/Frontend/Components/AuditView/AuditView.tsx b/src/Frontend/Components/AuditView/AuditView.tsx index 517963444..a2c42d20f 100644 --- a/src/Frontend/Components/AuditView/AuditView.tsx +++ b/src/Frontend/Components/AuditView/AuditView.tsx @@ -2,16 +2,21 @@ // SPDX-FileCopyrightText: TNG Technology Consulting GmbH // // SPDX-License-Identifier: Apache-2.0 -import { Fragment, ReactElement } from 'react'; - +import { AttributionDetails } from '../AttributionDetails/AttributionDetails'; +import { AttributionPanels } from '../AttributionPanels/AttributionPanels'; +import { PathBar } from '../PathBar/PathBar'; import { ResourceBrowser } from '../ResourceBrowser/ResourceBrowser'; -import { ResourceDetailsViewer } from '../ResourceDetailsViewer/ResourceDetailsViewer'; +import { ColumnsContainer } from './AuditView.style'; -export function AuditView(): ReactElement { +export const AuditView: React.FC = () => { return ( - - - - + <> + + + + + + + ); -} +}; diff --git a/src/Frontend/Components/AttributionView/__tests__/AttributionView.test.tsx b/src/Frontend/Components/AuditView/__tests__/AuditView.test.tsx similarity index 78% rename from src/Frontend/Components/AttributionView/__tests__/AttributionView.test.tsx rename to src/Frontend/Components/AuditView/__tests__/AuditView.test.tsx index 551806b4e..a07652c08 100644 --- a/src/Frontend/Components/AttributionView/__tests__/AttributionView.test.tsx +++ b/src/Frontend/Components/AuditView/__tests__/AuditView.test.tsx @@ -5,29 +5,30 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { text } from '../../../../shared/text'; import { faker } from '../../../../testing/Faker'; -import { ButtonText, View } from '../../../enums/enums'; +import { View } from '../../../enums/enums'; import { setProjectMetadata } from '../../../state/actions/resource-actions/all-views-simple-actions'; import { loadFromFile } from '../../../state/actions/resource-actions/load-actions'; import { setVariable } from '../../../state/actions/variables-actions/variables-actions'; import { navigateToView } from '../../../state/actions/view-actions/view-actions'; import { - FILTERED_ATTRIBUTIONS, - FilteredAttributions, + FILTERED_ATTRIBUTIONS_AUDIT, + FilteredData, initialFilteredAttributions, -} from '../../../state/variables/use-filtered-attributions'; +} from '../../../state/variables/use-filtered-data'; import { getParsedInputFileEnrichedWithTestData } from '../../../test-helpers/general-test-helpers'; import { renderComponent } from '../../../test-helpers/render'; -import { AttributionView } from '../AttributionView'; +import { AuditView } from '../AuditView'; -describe('The Attribution View', () => { +describe('AuditView', () => { it('renders', async () => { const resourceName = faker.opossum.resourceName(); const packageInfo = faker.opossum.packageInfo(); const manualAttributions = faker.opossum.attributions({ [packageInfo.id]: packageInfo, }); - renderComponent(, { + renderComponent(, { actions: [ loadFromFile( getParsedInputFileEnrichedWithTestData({ @@ -42,13 +43,13 @@ describe('The Attribution View', () => { }), ), setProjectMetadata(faker.opossum.metadata()), - setVariable(FILTERED_ATTRIBUTIONS, { + setVariable(FILTERED_ATTRIBUTIONS_AUDIT, { ...initialFilteredAttributions, attributions: { [packageInfo.id]: packageInfo, }, }), - navigateToView(View.Attribution), + navigateToView(View.Audit), ], }); @@ -59,8 +60,8 @@ describe('The Attribution View', () => { ); expect( - screen.getByRole('button', { name: ButtonText.Save }), + screen.getByRole('button', { name: text.attributionColumn.save }), ).toBeInTheDocument(); - expect(screen.getByText(resourceName)).toBeInTheDocument(); + expect(screen.getAllByText(resourceName)).not.toHaveLength(0); }); }); diff --git a/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx b/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx index b0da8b865..e623321d4 100644 --- a/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx +++ b/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx @@ -19,37 +19,32 @@ export const TagsContainer = styled('div')({ export const Input = styled(MuiTextField, { shouldForwardProp: (name: string) => - !['highlight', 'numberOfEndAdornments'].includes(name), + !['error', 'numberOfEndAdornments', 'background'].includes(name), })<{ - highlight: 'default' | 'dark' | undefined; + background?: string; + error?: boolean; numberOfEndAdornments: number; -}>(({ highlight, numberOfEndAdornments }) => ({ +}>(({ background, error, numberOfEndAdornments }) => ({ '& .MuiInputLabel-root': { - backgroundColor: highlight - ? highlight === 'default' - ? OpossumColors.lightOrange - : OpossumColors.darkOrange - : OpossumColors.white, + backgroundColor: + background || (error ? OpossumColors.lightOrange : OpossumColors.white), padding: '0px 3px', fontSize: '13px', top: '1px', }, '& .MuiInputBase-root': { - backgroundColor: highlight - ? highlight === 'default' - ? OpossumColors.lightOrange - : OpossumColors.darkOrange - : OpossumColors.white, + backgroundColor: + background || (error ? OpossumColors.lightOrange : OpossumColors.white), borderRadius: '0px', display: 'flex', flexWrap: 'wrap', alignItems: 'center', - gap: '4px', + gap: '8px', minHeight: '36.67px', paddingTop: '6px', paddingBottom: '6px', - paddingLeft: '14px', - paddingRight: `calc(14px + ${numberOfEndAdornments} * 28px)`, + paddingLeft: '12px', + paddingRight: `calc(12px + ${numberOfEndAdornments} * 28px)`, }, '& .MuiInputBase-input': { flex: 1, diff --git a/src/Frontend/Components/Autocomplete/Autocomplete.tsx b/src/Frontend/Components/Autocomplete/Autocomplete.tsx index 53b5a5c10..875c99cc2 100644 --- a/src/Frontend/Components/Autocomplete/Autocomplete.tsx +++ b/src/Frontend/Components/Autocomplete/Autocomplete.tsx @@ -2,11 +2,16 @@ // SPDX-FileCopyrightText: TNG Technology Consulting GmbH // // SPDX-License-Identifier: Apache-2.0 -import { SxProps } from '@mui/material'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import ClearIcon from '@mui/icons-material/Clear'; +import { TextFieldProps as MuiTextFieldProps, 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 MuiIconButton, { + IconButtonProps as MuiIconButtonProps, +} from '@mui/material/IconButton'; import { TextFieldProps as MuiInputProps } from '@mui/material/TextField'; +import MuiTooltip from '@mui/material/Tooltip'; import useMuiAutocomplete, { AutocompleteHighlightChangeReason, AutocompleteInputChangeReason, @@ -17,8 +22,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { VirtuosoHandle } from 'react-virtuoso'; import { ensureArray } from '../../util/ensure-array'; -import { ClearButton } from '../ClearButton/ClearButton'; -import { PopupIndicator } from '../PopupIndicator/PopupIndicator'; import { Container, EndAdornmentContainer, @@ -45,16 +48,21 @@ type AutocompleteProps< | 'renderOptionStartIcon' > & { ['aria-label']?: string; + background?: string; endAdornment?: React.ReactNode | Array; - highlight?: 'default' | 'dark'; + error?: boolean; inputProps?: MuiInputProps; onInputChange?: ( event: React.SyntheticEvent | undefined, value: string, reason: AutocompleteInputChangeReason, ) => void; + placeholder?: string; + hidePopupIndicator?: boolean; + startAdornment?: React.ReactNode; sx?: SxProps; - title: string; + title?: string; + variant?: MuiTextFieldProps['variant']; }; export function Autocomplete< @@ -63,24 +71,29 @@ export function Autocomplete< DisableClearable extends boolean | undefined, FreeSolo extends boolean | undefined, >({ + background, disableClearable, disableListWrap = true, disabled, endAdornment, + error, freeSolo, getOptionKey, getOptionLabel, groupBy, groupProps, - highlight, + hidePopupIndicator, inputProps: customInputProps, multiple, optionText, + placeholder, readOnly, renderOptionEndIcon, renderOptionStartIcon, + startAdornment, sx, title, + variant = 'outlined', ...props }: AutocompleteProps) { const [open, setOpen] = useState(false); @@ -147,7 +160,7 @@ export function Autocomplete< groupedOptionsRef.current = groupedOptions as Array; }, [groupedOptions]); - const hasPopupIndicator = !freeSolo; + const hasPopupIndicator = !freeSolo && !hidePopupIndicator; const hasClearButton = !disableClearable && !disabled && !readOnly && containsValue; const isPopupOpen = !!groupedOptions.length && popupOpen; @@ -156,18 +169,16 @@ export function Autocomplete< return ( <> - + { // https://github.com/mui/material-ui/issues/21129 @@ -207,14 +227,16 @@ export function Autocomplete< const label = getOptionLabel?.(option) ?? option; return ( - event.stopPropagation()} - data-testid={`tag-${label}`} - /> + + event.stopPropagation()} + data-testid={`tag-${label}`} + sx={{ cursor: 'default' }} + /> + ); }); } @@ -230,13 +252,28 @@ export function Autocomplete< onMouseDown={(event) => event.stopPropagation()} > {hasClearButton && ( - + + + )} {hasPopupIndicator && ( - + > + + )} {endAdornment} diff --git a/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx b/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx index ff44e2b38..5b14ce319 100644 --- a/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx +++ b/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx @@ -10,6 +10,7 @@ import { AutocompleteFreeSoloValueMapping, UseAutocompleteRenderedOption, } from '@mui/material/useAutocomplete'; +import { SxProps } from '@mui/system'; import { groupBy as _groupBy } from 'lodash'; import { forwardRef, useMemo, useState } from 'react'; import { GroupedVirtuoso, Virtuoso, VirtuosoHandle } from 'react-virtuoso'; @@ -43,6 +44,7 @@ export type ListboxProps< { closePopper }: { closePopper: () => void }, ) => React.ReactNode; optionText: { + sx?: SxProps; primary: ( option: Value | AutocompleteFreeSoloValueMapping, ) => React.ReactNode; @@ -118,9 +120,8 @@ export const Listbox = forwardRef( return ( renderOption({ option, index })} - overscan={4} - increaseViewportBy={2} + increaseViewportBy={20} totalListHeightChanged={setHeight} /> ); @@ -184,14 +184,14 @@ export const Listbox = forwardRef( selected={optionProps['aria-selected'] as boolean} disabled={optionProps['aria-disabled'] as boolean} key={getOptionKey?.(option) ?? key} - sx={{ gap: '12px' }} + sx={{ gap: '12px', ...optionText.sx }} dense > {renderOptionStartIcon?.(option, { closePopper })} { - const RENDER_ALLOWANCE = 200; // allow time for the UI to render the process popup before resource intensive task - await new Promise((resolve) => setTimeout(resolve, RENDER_ALLOWANCE)); + function getFollowUpExportListener(): void { const followUpAttributions = pick( manualData.attributions, Object.keys(manualData.attributions).filter( @@ -94,13 +91,13 @@ export function BackendCommunication(): ReactElement | null { manualData.attributionsToResources, manualData.resourcesToAttributions, resources || {}, - getAttributionBreakpointCheck(attributionBreakpoints), - getFileWithChildrenCheck(filesWithChildren), + attributionBreakpoints, + filesWithChildren, ); const followUpAttributionsWithFormattedResources = removeSlashesFromFilesWithChildren( followUpAttributionsWithResources, - getFileWithChildrenCheck(filesWithChildren), + filesWithChildren, ); window.electronAPI.exportFile({ @@ -156,7 +153,7 @@ export function BackendCommunication(): ReactElement | null { const bomAttributionsWithFormattedResources = removeSlashesFromFilesWithChildren( bomAttributionsWithResources, - getFileWithChildrenCheck(filesWithChildren), + filesWithChildren, ); window.electronAPI.exportFile({ @@ -184,24 +181,6 @@ export function BackendCommunication(): ReactElement | null { } } - function showSearchPopupListener( - _: IpcRendererEvent, - showSearchPopUp: boolean, - ): void { - if (showSearchPopUp) { - dispatch(openPopup(PopupType.FileSearchPopup)); - } - } - - function showLocatorPopupListener( - _: IpcRendererEvent, - showLocatePopUp: boolean, - ): void { - if (showLocatePopUp) { - dispatch(openPopup(PopupType.LocatorPopup)); - } - } - function showProjectMetadataPopupListener( _: IpcRendererEvent, showProjectMetadataPopup: boolean, @@ -220,15 +199,6 @@ export function BackendCommunication(): ReactElement | null { } } - function showChangedInputFilePopupListener( - _: IpcRendererEvent, - showChangedInputFilePopup: boolean, - ): void { - if (showChangedInputFilePopup) { - dispatch(openPopup(PopupType.ChangedInputFilePopup)); - } - } - function showUpdateAppPopupListener( _: IpcRendererEvent, showUpdateAppPopup: boolean, @@ -246,7 +216,7 @@ export function BackendCommunication(): ReactElement | null { dispatch( setBaseUrlsForSources({ ...baseUrlsForSources, - '/': baseURLForRootArgs.baseURLForRoot, + [ROOT_PATH]: baseURLForRootArgs.baseURLForRoot, }), ); } @@ -279,26 +249,11 @@ export function BackendCommunication(): ReactElement | null { console[level](`${dayjs(date).format('HH:mm:ss.SSS')} ${message}`), [dispatch], ); - useIpcRenderer( - AllowedFrontendChannels.ShowSearchPopup, - showSearchPopupListener, - [dispatch], - ); - useIpcRenderer( - AllowedFrontendChannels.ShowLocatorPopup, - showLocatorPopupListener, - [dispatch], - ); useIpcRenderer( AllowedFrontendChannels.ShowProjectMetadataPopup, showProjectMetadataPopupListener, [dispatch], ); - useIpcRenderer( - AllowedFrontendChannels.ShowChangedInputFilePopup, - showChangedInputFilePopupListener, - [dispatch], - ); useIpcRenderer( AllowedFrontendChannels.ShowProjectStatisticsPopup, showProjectStatisticsPopupListener, diff --git a/src/Frontend/Components/Breadcrumbs/Breadcrumbs.tsx b/src/Frontend/Components/Breadcrumbs/Breadcrumbs.tsx deleted file mode 100644 index 823404c69..000000000 --- a/src/Frontend/Components/Breadcrumbs/Breadcrumbs.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import MuiBreadcrumbs from '@mui/material/Breadcrumbs'; -import MuiListItemButton from '@mui/material/ListItemButton'; -import MuiTypography from '@mui/material/Typography'; -import { SxProps } from '@mui/system'; -import { ReactElement } from 'react'; - -import { OpossumColors } from '../../shared-styles'; - -const classes = { - breadcrumbs: { - color: OpossumColors.black, - '.MuiBreadcrumbs-separator': { - margin: '0 2px', - }, - }, - breadcrumbsButton: { - padding: '1px 4px', - backgroundColor: OpossumColors.lightestBlue, - '&:loading': { - backgroundColor: OpossumColors.lightestBlue, - }, - '&.Mui-selected': { - '&:hover': { - backgroundColor: OpossumColors.lightestBlue, - }, - backgroundColor: OpossumColors.lightestBlue, - }, - '&.Mui-disabled': { - opacity: 1, - }, - }, - breadcrumbsSelected: { - fontWeight: 'bold', - }, -}; - -interface BreadcrumbsProps { - selectedId?: string; - onClick: (id: string) => void; - idsToDisplayValues: Array<[string, string]>; - sx?: SxProps; - maxItems?: number; - separator?: React.ReactNode; -} - -export function Breadcrumbs(props: BreadcrumbsProps): ReactElement { - const ids: Array = props.idsToDisplayValues.map( - (idToDisplayValue) => idToDisplayValue[0], - ); - - return ( - - {ids.map((id, index) => ( - props.onClick(id)} - disableRipple={true} - disabled={ - !!props.selectedId && index >= ids.indexOf(props.selectedId) - } - > - - {props.idsToDisplayValues[index][1]} - - - ))} - - ); -} diff --git a/src/Frontend/Components/Breadcrumbs/__tests__/Breadcrumbs.test.tsx b/src/Frontend/Components/Breadcrumbs/__tests__/Breadcrumbs.test.tsx deleted file mode 100644 index 5bc4590a4..000000000 --- a/src/Frontend/Components/Breadcrumbs/__tests__/Breadcrumbs.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import { render, screen } from '@testing-library/react'; - -import { doNothing } from '../../../util/do-nothing'; -import { Breadcrumbs } from '../Breadcrumbs'; - -describe('Breadcrumbs', () => { - it('renders breadcrumbs', () => { - const testIdToSelectedValue: Array<[string, string]> = [ - ['package_id', 'package'], - ['version_id', 'version'], - ]; - render( - , - ); - - expect(screen.getByText('package')).toBeInTheDocument(); - expect(screen.getByText('version')).toBeInTheDocument(); - }); -}); diff --git a/src/Frontend/Components/Button/Button.tsx b/src/Frontend/Components/Button/Button.tsx deleted file mode 100644 index ea257a118..000000000 --- a/src/Frontend/Components/Button/Button.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import MuiButton, { ButtonProps as MuiButtonProps } from '@mui/material/Button'; -import MuiTooltip from '@mui/material/Tooltip'; -import { ReactElement } from 'react'; - -import { tooltipStyle } from '../../shared-styles'; - -export interface ButtonProps - extends Pick { - buttonText: string; - tooltipText?: string; - tooltipPlacement?: 'left' | 'right' | 'top' | 'bottom'; -} - -export function Button(props: ButtonProps): ReactElement { - return ( - - - - {props.buttonText} - - - - ); -} diff --git a/src/Frontend/Components/Button/__tests__/Button.test.tsx b/src/Frontend/Components/Button/__tests__/Button.test.tsx deleted file mode 100644 index 1b998883f..000000000 --- a/src/Frontend/Components/Button/__tests__/Button.test.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import { fireEvent, render, screen } from '@testing-library/react'; -import { noop } from 'lodash'; - -import { Button } from '../Button'; - -describe('Button', () => { - it('renders a button', () => { - render(