Skip to content

Conversation

gildub
Copy link
Collaborator

@gildub gildub commented Sep 22, 2025

Addresses the addition of the licenses column from the following issue:
Fix https://issues.redhat.com/browse/TC-2834

Summary by Sourcery

Add a Licenses column to the package listings with expandable license details and refactor vulnerability fetching and rendering to use a shared package data provider.

New Features:

  • Add Licenses column to the package table showing license counts and an expandable list of license names

Enhancements:

  • Introduce WithPackage component for fetching package details by ID
  • Refactor vulnerability display to accept PurlDetails object and simplify rendering
  • Enable compound row expansion in the package table for license details
  • Update SBOM package list to use WithPackage for fetching package data and rendering vulnerabilities

Copy link
Contributor

sourcery-ai bot commented Sep 22, 2025

Reviewer's Guide

This PR introduces a new licenses column to the package table (showing counts and expanding to full lists), adds a WithPackage HOC for fetching package details, refactors vulnerability hooks/components to accept full package objects, updates the table context to support expansion for licenses, and simplifies query function syntax.

Entity relationship diagram for package and licenses

erDiagram
    PURL_DETAILS {
      uuid string PK
      licenses License[]
    }
    LICENSE {
      license_name string
    }
    PURL_DETAILS ||--o{ LICENSE : has
Loading

Class diagram for WithPackage and PackageVulnerabilities refactor

classDiagram
    class WithPackage {
      +packageId: string
      +children(pkg, isFetching, fetchError): ReactNode
    }
    class PackageVulnerabilities {
      +pkg: PurlDetails | undefined
    }
    class useVulnerabilitiesOfPackage {
      +pkg: PurlDetails | undefined
      +data
    }
    class useVulnerabilitiesOfPackageId {
      +packageId: string
      +data
      +isFetching
      +fetchError
    }
    WithPackage --> "1" PackageVulnerabilities : provides pkg
    PackageVulnerabilities --> "1" useVulnerabilitiesOfPackage : uses
    WithPackage --> "1" useFetchPackageById : uses
    useVulnerabilitiesOfPackageId --> "1" useFetchPackageById : uses
Loading

File-Level Changes

Change Details Files
Add a licenses column with expandable license lists in the package table
  • Imported List, ListItem and WithPackage
  • Added a new Th for "licenses" and wired up compound expansion props
  • Rendered license count in the cell and pluralized label
  • Displayed full license list inside ExpandableRowContent when expanded
client/src/app/pages/package-list/package-table.tsx
Introduce WithPackage HOC for unified package data fetching
  • Created WithPackage component wrapping useFetchPackageById
  • Exposed pkg, isFetching and fetchError to children render prop
  • Replaced direct fetch calls in table and SBOM view with WithPackage
client/src/app/components/WithPackage.tsx
client/src/app/pages/sbom-details/packages-by-sbom.tsx
Refactor vulnerability hook and component to accept PurlDetails
  • Renamed original hook to useVulnerabilitiesOfPackageId(packageId)
  • Added new useVulnerabilitiesOfPackage(pkg) that returns only data
  • Updated PackageVulnerabilities to take pkg, remove loading/error wrappers
  • Swapped calls in table and details pages to new hooks and props
client/src/app/hooks/domain-controls/useVulnerabilitiesOfPackage.ts
client/src/app/pages/package-list/components/PackageVulnerabilities.tsx
client/src/app/pages/package-details/vulnerabilities-by-package.tsx
Enable table expansion and register licenses in context
  • Added "licenses" to PackageSearchContext columnKeys and labels
  • Enabled isExpansionEnabled and set expandableVariant to "compound"
client/src/app/pages/package-list/package-context.tsx
Streamline query functions in package queries
  • Converted queryFn to concise arrow body without explicit return
  • Removed unnecessary braces in useFetchPackages and useFetchPackagesBySbomId
client/src/app/queries/packages.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • Remove the commented-out NavLink and raw license count in PackageTable and consolidate all link/display logic into the new PackageLicenses component for clarity.
  • Consider batch-fetching license data alongside the package list instead of using useFetchPackageById for each row to avoid N+1 API calls and improve performance.
  • Verify that your generatePath calls use the correct route parameters and adjust the spelling between “License” and “Licence” for consistency in the UI.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Remove the commented-out NavLink and raw license count in PackageTable and consolidate all link/display logic into the new PackageLicenses component for clarity.
- Consider batch-fetching license data alongside the package list instead of using useFetchPackageById for each row to avoid N+1 API calls and improve performance.
- Verify that your generatePath calls use the correct route parameters and adjust the spelling between “License” and “Licence” for consistency in the UI.

## Individual Comments

### Comment 1
<location> `client/src/app/pages/package-list/components/PackageLicenses.tsx:32` </location>
<code_context>
+            packageId: pkg?.uuid || "",
+          })}
+      >
+        {(pkg?.licenses?.length ?? 0)} {(pkg?.licenses?.length ?? 0) > 1 ? "Licences" : "Licence"}
+      </NavLink>
+    </LoadingWrapper>
</code_context>

<issue_to_address>
**suggestion (typo):** The pluralization of 'Licence' uses 'Licences', which is a non-standard spelling.

Use 'Licenses' instead of 'Licences' to align with standard English and codebase conventions.

```suggestion
        {(pkg?.licenses?.length ?? 0)} {(pkg?.licenses?.length ?? 0) > 1 ? "Licenses" : "License"}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@gildub gildub force-pushed the add-licenses-to-packages branch 3 times, most recently from 459ae12 to 2ce32ba Compare September 22, 2025 14:49
Copy link

codecov bot commented Sep 22, 2025

Codecov Report

❌ Patch coverage is 76.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.68%. Comparing base (2e4217f) to head (89ee121).

Files with missing lines Patch % Lines
...lient/src/app/pages/package-list/package-table.tsx 50.00% 3 Missing and 1 partial ⚠️
...package-list/components/PackageVulnerabilities.tsx 33.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #740      +/-   ##
==========================================
- Coverage   60.17%   59.68%   -0.49%     
==========================================
  Files         155      156       +1     
  Lines        2684     2699      +15     
  Branches      607      613       +6     
==========================================
- Hits         1615     1611       -4     
- Misses        840      857      +17     
- Partials      229      231       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gildub gildub changed the title PackageTable: Add Licenses column [WIP] PackageTable: Add Licenses column Sep 22, 2025
@gildub gildub force-pushed the add-licenses-to-packages branch 2 times, most recently from aa13a49 to da42912 Compare September 24, 2025 21:18
@gildub gildub changed the title [WIP] PackageTable: Add Licenses column PackageTable: Add Licenses column Sep 24, 2025
@gildub gildub force-pushed the add-licenses-to-packages branch 4 times, most recently from e7438f4 to 7d79c9c Compare September 24, 2025 21:43
Copy link
Contributor

@carlosthe19916 carlosthe19916 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gildub thanks for the PR. Please read my comments below. Hopefully we will be able to address those issues. Let me know if you need me to expand on any of the points made.

Comment on lines 29 to 36
const renderLicenseWithMappings = (
license: string,
mappings: LicenseRefMapping[],
) => {
return mappings.reduce((prev, { license_id, license_name }) => {
return prev.replaceAll(license_id, license_name);
}, `${license}`);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the exact same function as https://github.com/guacsec/trustify-ui/blob/main/client/src/app/pages/sbom-details/packages-by-sbom.tsx#L43-L54

Maybe we could move it somewhere and reuse it in both places.

e.license_name,
item.licenses_ref_mapping,
)}{" "}
<Label isCompact>{e.license_type}</Label>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like in this PR #743 , UX requested to remove the type from the UI as it has no meaning for users. We can confirm that requirement internally with UX

refetch,
};
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see some potential problems here:

  • the file queries/packages has 2 functions to fetch Packages. useFetchPackages and useFetchPackagesDetails. There should not be 2 different implementations for fetching the same entity only one.
  • The new function useFetchPackagesDetails is calling the listPurl and for each element found, it calls getPurl using Promise.all. This is critical IMHO:
    • For the useFetchPackagesDetails to finish, it needs to wait for listPurl and then wait for ALL getPurl to pass. The UI won't be able to render the table of packages until each single getPurl is finished.
      • Take the Vulnerabilities column from the main branch as an example: the UI is able to render the package table even if the count of vulnerabilities hasn't finished.
    • useFetchPackagesDetails is implemented in a way of ALL or NOTHING. What if only one single getPurl fails, then the whole useFetchPackagesDetails will fail.

Let me expand a bit on technical requirements using the image below:

Image
  • The UI should be able to render the Package List table without the need to wait for its dependant requests to finish. That means, the table should be rendered without waiting for getPurl to finish.
  • If one or many getPurl fail, the table should still render the table with all packages, and render which columns were not being able to be fetch, rendering an error in the column that was not able to be fetched.
    • In my image above, all rows in the Vulnerability column fail, but it should be possible to handle cases when only part of them failed and the rest passed. E.g. Vulnerability column in row 1 shows data but Vulnerability column in row 2 shows error
    • Production environment proved high latency in many endpoint, forcing a single endpoint to depend on another endpoint should be avoided unless there is no escape from it. In this case, it should be possible not to have dependent requests.
    • The nature of Promise.all works as, wait for all promises inside the array, and then either success all of them or either fail if any of the promises fail. Under this specific context and use case we should not use it. Each row in the package table is independent and there is no reason to block certain rows just because the other rows are still being fetched. And again, keep in mind production environment are many more times slower. We experience already many times timeout errors. We need to handle data fetch orchestration efficiently.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@carlosthe19916, you're right regarding the PromiseAll() waiting for all items to fetched (or fail).
No worries, let me refactor.

@gildub gildub force-pushed the add-licenses-to-packages branch from 7d79c9c to 3d9df17 Compare September 25, 2025 16:15
@PhilipCattanach
Copy link

@gildub - Can you fix the failing tests please and let @carlosthe19916 know when this PR is available for him to review again. Thanks

@gildub gildub force-pushed the add-licenses-to-packages branch from 3d9df17 to ab300f5 Compare September 28, 2025 20:37
@gildub gildub force-pushed the add-licenses-to-packages branch from ab300f5 to 89ee121 Compare September 28, 2025 20:44
@gildub
Copy link
Collaborator Author

gildub commented Sep 28, 2025

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `client/src/app/pages/package-list/components/PackageVulnerabilities.tsx:16-18` </location>
<code_context>
-    useVulnerabilitiesOfPackage(packageId);
+  const { data } = useVulnerabilitiesOfPackage(pkg);
+
+  if (!data?.summary?.vulnerabilityStatus?.affected?.severities) {
+    return null;
+  }

</code_context>

<issue_to_address>
**suggestion:** Returning null may lead to empty table cells.

Consider displaying a placeholder like 'None' or a dash instead of returning null to make empty cells more informative.

```suggestion
  if (!data?.summary?.vulnerabilityStatus?.affected?.severities) {
    return <span>-</span>;
  }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@gildub
Copy link
Collaborator Author

gildub commented Sep 28, 2025

@sourcery-ai dismiss

Copy link
Contributor

@carlosthe19916 carlosthe19916 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gildub thanks for the PR. The code is in a better shape.
Please read my code request changes below. We are almost there!

If you need me to expand on any of the points feel free to add a comment

children: (
pkg: PurlDetails | undefined,
isFetching: boolean,
fetchError?: Error,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe fetchError should be AxiosError and not Error?

Comment on lines 7 to 25
interface PackageVulnerabilitiesProps {
packageId: string;
pkg: PurlDetails | undefined;
}

export const PackageVulnerabilities: React.FC<PackageVulnerabilitiesProps> = ({
packageId,
pkg,
}) => {
const { data, isFetching, fetchError } =
useVulnerabilitiesOfPackage(packageId);
const { data } = useVulnerabilitiesOfPackage(pkg);

if (!data?.summary?.vulnerabilityStatus?.affected?.severities) {
return null;
}

return (
<LoadingWrapper
isFetching={isFetching}
fetchError={fetchError}
isFetchingState={<Skeleton screenreaderText="Loading contents" />}
fetchErrorState={(error) => <TableCellError error={error} />}
>
<VulnerabilityGallery
severities={data.summary.vulnerabilityStatus.affected.severities}
/>
</LoadingWrapper>
<VulnerabilityGallery
severities={data.summary.vulnerabilityStatus.affected.severities}
/>
);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that now the package is injected as a property should not delete other functionally existing already.

Suggestion:

  • Make the PackageVulnerabilitiesProps to receive 3 properties.
    • pkg (you already have it)
    • isFetching: boolean
    • fetchError: AxiosError
  • Keep the whole original code written in the component.
  • The only difference will be that in the main branch the 3 properties above come from useVulnerabilitiesOfPackage but in this PR those 3 properties will be injected as properties of the component itself

modifier="breakWord"
{...getTdProps({ columnKey: "vulnerabilities" })}
>
{item.purl[0] && <PackageVulnerabilities pkg={pkg} />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • WithPackage exposes 3 properties already, pass all properties from WithPackage to PackageVulnerabilities. Meaning isFetching and fetchError
  • The conditional item.purl[0] && is not necessary anymore. It should be enough to directly render <PackageVulnerabilities/>. The parent <WithPackage /> is already doing the verification.

Comment on lines 113 to 187
@@ -112,14 +143,48 @@
/>
)}
</Td>
<WithPackage packageId={item.uuid}>
{(pkg) => (
<Td
width={20}
{...getTdProps({ columnKey: "vulnerabilities" })}
>
<PackageVulnerabilities pkg={pkg} />
</Td>
)}
</WithPackage>
</TableRowContentWithControls>
</Tr>
{isCellExpanded(item) ? (
<Tr isExpanded>
<Td
width={20}
{...getTdProps({ columnKey: "vulnerabilities" })}
{...getExpandedContentTdProps({
item,
})}
className={spacing.pLg}
>
<PackageVulnerabilities packageId={item.uuid} />
<ExpandableRowContent>
<div className={spacing.ptLg}>
{isCellExpanded(item, "licenses") ? (
<WithPackage packageId={item.uuid}>
{(pkg) => (
<List isPlain>
{pkg?.licenses?.map((license, idx) => (
<ListItem
key={`${license.license_name}-${idx}`}
>
{license.license_name}
</ListItem>
))}
</List>
)}
</WithPackage>
) : null}
</div>
</ExpandableRowContent>
</Td>
</TableRowContentWithControls>
</Tr>
</Tr>
) : null}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each time I click in the expanded area for licenses there is a new request for fetching the package details. Let's avoid that.

Suggestion:

  • Declare <WithPackage/> just below <Tbody /> so you wrap the whole table body thus you can pass all properties that WithPackage is exposing to all areas inside TBody. This way there is going to be no additional requests each time the user expands the License area.

Comment on lines +113 to +131
<WithPackage packageId={item.uuid}>
{(pkg) => (
<Td
width={10}
modifier="truncate"
{...getTdProps({
columnKey: "licenses",
isCompoundExpandToggle: true,
item,
rowIndex,
})}
>
{pkg?.licenses?.length ?? 0}{" "}
{(pkg?.licenses?.length ?? 0) > 1
? "Licenses"
: "License"}
</Td>
)}
</WithPackage>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there is an error while fetching Package? the current code won't render an error but will render 0 Licenses which is wrong and misleading.

Suggestion:

  • Wrap the <Td> tag with something like
<LoadingWrapper
      isFetching={isFetching}
      fetchError={fetchError}
      isFetchingState={<Skeleton screenreaderText="Loading contents" />}
      fetchErrorState={(error) => <TableCellError error={error} />}
    >
     // here write the same code you have for the `<Td/>`
    </LoadingWrapper>

this way if there is an error then the error will be rendered

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An additional step, definitely in a separate PR not here, would be to create a reusable component like TdWithError that receives the same properties as <Td/> but handles additionally isFetching and fetchError. But this is something we can do later

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants