Skip to content

Conversation

@matiasdaloia
Copy link
Collaborator

@matiasdaloia matiasdaloia commented Jul 29, 2025

Summary by CodeRabbit

  • New Features

    • Online Component Search dialog with backend search and license lookup; integrated into the Replace Component flow.
    • Keyboard-navigable table for faster list navigation and selection.
  • UI/UX Improvements

    • License picker now shows “Matched” and “Catalogued” groups; added shortcut badges, modal popovers and portal rendering.
    • Dialog state manager prevents keyboard navigation when blocking dialogs are open; select trigger height slightly adjusted.
  • Chores

    • Bumped Go/Node deps and CI workflow; build args updated.

@matiasdaloia matiasdaloia self-assigned this Jul 29, 2025
@matiasdaloia matiasdaloia requested a review from eeisegn July 29, 2025 09:03
@matiasdaloia matiasdaloia added the enhancement New feature or request label Jul 29, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 29, 2025

Walkthrough

Adds online component search and license lookup across backend and frontend: new entities and models, Scanoss API interface and HTTP implementation, service wiring and Wails bindings, frontend search/dialog/keyboard-table/dialog-state UI, utility and CI/dependency updates, generated Wails models, and removal of two backend tests.

Changes

Cohort / File(s) Summary
Backend Entities
backend/entities/component_search.go, backend/entities/common.go, backend/entities/license.go
Added component and license data models (ComponentSearchRequest, SearchedComponent, ComponentSearchResponse, ComponentRequest, StatusResponse, LicenseInfo, ComponentLicenseInfo, GetLicensesByPurlResponse).
Scanoss API Interface
backend/service/scanoss_api_service.go
New ScanossApiService interface with SearchComponents, GetLicensesByPurl, and SetContext.
Scanoss HTTP Impl
backend/service/scanoss_api_service_http_impl.go
New HTTP implementation ScanossApiServiceHttpImpl with constructor, SetContext, GetWithParams, SearchComponents, and GetLicensesByPurl; includes HTTPClient and QueryParams helpers.
Backend Service Wiring
backend/service/component_service.go, backend/service/component_service_impl.go, backend/service/license_service.go, backend/service/license_service_impl.go, main.go
Injected ScanossApiService into component and license services, added delegating methods (SearchComponents, GetLicensesByPurl), updated constructors, and wired service/context in main.go.
Backend Tests
backend/service/component_service_test.go
Removed two unit tests (TestInsertComponentFilterActions, TestGetDeclaredComponents).
Utilities
internal/utils/url_utils.go
Added RemoveProtocolFromURL(url string) string.
Frontend Online Search & Replace UI
frontend/src/components/OnlineComponentSearchDialog.tsx, frontend/src/components/ReplaceComponentDialog.tsx
Added OnlineComponentSearchDialog (debounced search, package scope) and integrated it into ReplaceComponentDialog; selection triggers license lookup and updates form state.
Frontend License Selector & UI
frontend/src/components/SelectLicenseList.tsx, frontend/src/components/ShortcutBadge.tsx
SelectLicenseList accepts matchedLicenses and renders a Matched group; added ShortcutBadge component.
Frontend UI Primitives
frontend/src/components/ui/table.tsx, frontend/src/components/ui/keyboard-table.tsx, frontend/src/components/ui/popover.tsx, frontend/src/components/ui/select.tsx, frontend/src/components/ui/dialog.tsx
Added table primitives and KeyboardTable with keyboard navigation; PopoverContent now uses a portal; adjusted SelectTrigger height; removed unused ScrollArea import.
Dialog State Context & App Wiring
frontend/src/contexts/DialogStateContext.tsx, frontend/src/components/Sidebar.tsx, frontend/src/main.tsx
Added DialogStateProvider, useDialogState, and useDialogRegistration; wrapped app and gated sidebar keyboard shortcuts when blocking dialogs are open.
Wails Models & Bindings
frontend/wailsjs/go/models.ts, frontend/wailsjs/go/service/*.{d.ts,js}
Generated frontend models for new entities and Wails bindings: added SearchComponents and GetLicensesByPurl bridge functions.
Frontend Misc & Config
frontend/package.json, frontend/package.json.md5, scanoss.json, CHANGELOG.md
Radix UI dependency bumps and checksum update; scanoss.json BOM/exclude/replacements adjusted; added changelog entry for 0.7.6.
Tooling / Modules / CI
go.mod, .github/workflows/test.yml, .github/workflows/release.yml
Updated indirect Go module versions, bumped GitHub Actions Go/Node versions, and adjusted build/integration workflow environment and APPARGS.

Sequence Diagram(s)

sequenceDiagram
  participant FE as OnlineComponentSearchDialog (FE)
  participant W as Wails Bridge
  participant CS as ComponentServiceImpl (BE)
  participant API as ScanossApiServiceHttpImpl
  participant SC as Scanoss API

  FE->>W: SearchComponents(request)
  W->>CS: SearchComponents(request)
  CS->>API: SearchComponents(request)
  API->>SC: GET /api/v2/components/search?...
  SC-->>API: 200 JSON (components[])
  API-->>CS: ComponentSearchResponse
  CS-->>W: ComponentSearchResponse
  W-->>FE: components[]
Loading
sequenceDiagram
  participant FE as ReplaceComponentDialog (FE)
  participant W as Wails Bridge
  participant LS as LicenseServiceImpl (BE)
  participant API as ScanossApiServiceHttpImpl
  participant SC as Scanoss API

  FE->>W: GetLicensesByPurl({purl,...})
  W->>LS: GetLicensesByPurl(request)
  LS->>API: GetLicensesByPurl(request)
  API->>SC: GET /api/v2/licenses/component?purl=...
  SC-->>API: 200 JSON (component licenses)
  API-->>LS: GetLicensesByPurlResponse
  LS-->>W: GetLicensesByPurlResponse
  W-->>FE: matchedLicenses
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

"I hop through code with nimble paws,
I stitch new routes and search online.
Dialogs, shortcuts, licenses I draw,
Components found, their PURLs shine.
A carrot cheer for every new line! 🥕"

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title concisely and accurately describes the primary feature added by this pull request—online component search—using clear and specific language without extraneous details or vague terms.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/mdaloia/sp-2882-add-component-search

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
internal/utils/url_utils.go (1)

5-13: Function works correctly but consider minor improvements.

The logic is sound and handles both HTTP and HTTPS protocols properly. Consider these enhancements:

  1. Naming convention: Go prefers URL over Url - consider RemoveProtocolFromURL
  2. Documentation: Add a comment describing the function's purpose
  3. Input validation: Consider handling edge cases like empty strings
+// RemoveProtocolFromURL removes the HTTP or HTTPS protocol prefix from a URL string.
+// If no protocol is found, returns the original string unchanged.
-func RemoveProtocolFromUrl(url string) string {
+func RemoveProtocolFromURL(url string) string {
+	if url == "" {
+		return url
+	}
	if after, ok := strings.CutPrefix(url, "http://"); ok {
		return after
	}
	if after, ok := strings.CutPrefix(url, "https://"); ok {
		return after
	}
	return url
}
frontend/src/components/OnlineComponentSearchDialog.tsx (2)

43-82: Consider externalizing the catalog packages configuration.

The large catalogPackages array might be better maintained in a separate configuration file or fetched from the backend to ensure consistency.

Consider moving this to a separate config file:

// config/catalogPackages.ts
export const catalogPackages = [
  'angular',
  'apache',
  // ... rest of packages
];

143-147: Consider responsive dialog sizing.

The dialog uses fixed viewport units (calc(80dvh), calc(60dvw)) which might not work well on very small screens.

Consider adding responsive breakpoints:

-      <DialogContent className="max-h-[calc(80dvh)] max-w-[calc(60dvw)] p-4">
+      <DialogContent className="max-h-[calc(80dvh)] max-w-[calc(60dvw)] p-4 sm:max-w-[calc(90vw)] md:max-w-[calc(60dvw)]">
backend/service/scanoss_api_service_grpc_impl.go (1)

52-68: Consider adding API key validation.

While the API key is properly transmitted over TLS, consider adding validation to ensure it meets expected format requirements (e.g., length, character set) to prevent potential injection attacks.

Add validation in the constructor:

 func NewScanossApiServiceGrpcImpl(
 	mapper mappers.ScanossApiMapper,
 ) ScanossApiService {
 	cfg := config.GetInstance()
+	apiKey := cfg.GetApiToken()
+	
+	// Validate API key format if present
+	if apiKey != "" && !isValidApiKey(apiKey) {
+		log.Error().Msg("Invalid API key format")
+		apiKey = ""
+	}
+	
 	service := &ScanossApiServiceGrpcImpl{
 		mapper:   mapper,
 		timeout:  30 * time.Second,
 		endpoint: utils.RemoveProtocolFromUrl(cfg.GetApiUrl()),
-		apiKey:   cfg.GetApiToken(),
+		apiKey:   apiKey,
 	}

Also add a validation function:

func isValidApiKey(key string) bool {
	// Implement validation based on expected API key format
	// e.g., check length, allowed characters, etc.
	return len(key) > 0 && len(key) <= 256
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1cb81d and 89d0508.

⛔ Files ignored due to path filters (2)
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (18)
  • backend/entities/component_search.go (1 hunks)
  • backend/mappers/scanoss_api_mapper.go (1 hunks)
  • backend/service/component_service.go (2 hunks)
  • backend/service/component_service_impl.go (2 hunks)
  • backend/service/component_service_test.go (0 hunks)
  • backend/service/scanoss_api_service.go (1 hunks)
  • backend/service/scanoss_api_service_grpc_impl.go (1 hunks)
  • frontend/src/components/ComponentSearchTable.tsx (1 hunks)
  • frontend/src/components/OnlineComponentSearchDialog.tsx (1 hunks)
  • frontend/src/components/ReplaceComponentDialog.tsx (7 hunks)
  • frontend/src/components/ui/select.tsx (1 hunks)
  • frontend/src/components/ui/table.tsx (1 hunks)
  • frontend/wailsjs/go/models.ts (3 hunks)
  • frontend/wailsjs/go/service/ComponentServiceImpl.d.ts (1 hunks)
  • frontend/wailsjs/go/service/ComponentServiceImpl.js (1 hunks)
  • go.mod (4 hunks)
  • internal/utils/url_utils.go (1 hunks)
  • main.go (1 hunks)
💤 Files with no reviewable changes (1)
  • backend/service/component_service_test.go
🧰 Additional context used
🧬 Code Graph Analysis (6)
backend/service/scanoss_api_service.go (2)
backend/entities/component_search.go (2)
  • ComponentSearchRequest (27-34)
  • ComponentSearchResponse (37-40)
frontend/wailsjs/go/models.ts (2)
  • ComponentSearchRequest (182-203)
  • ComponentSearchResponse (234-265)
backend/entities/component_search.go (2)
backend/entities/component.go (1)
  • Component (104-158)
frontend/wailsjs/go/models.ts (2)
  • SearchedComponent (218-233)
  • StatusResponse (204-217)
frontend/wailsjs/go/service/ComponentServiceImpl.d.ts (3)
frontend/wailsjs/go/service/ComponentServiceImpl.js (1)
  • SearchComponents (37-39)
backend/entities/component_search.go (2)
  • ComponentSearchRequest (27-34)
  • ComponentSearchResponse (37-40)
frontend/wailsjs/go/models.ts (2)
  • ComponentSearchRequest (182-203)
  • ComponentSearchResponse (234-265)
frontend/wailsjs/go/models.ts (1)
backend/entities/component_search.go (4)
  • ComponentSearchRequest (27-34)
  • StatusResponse (50-53)
  • SearchedComponent (43-47)
  • ComponentSearchResponse (37-40)
main.go (3)
backend/mappers/scanoss_api_mapper.go (1)
  • NewScanossApiMapper (40-42)
backend/service/scanoss_api_service_grpc_impl.go (1)
  • NewScanossApiServiceGrpcImpl (52-68)
backend/service/component_service_impl.go (1)
  • NewComponentServiceImpl (50-65)
frontend/src/components/ui/table.tsx (1)
frontend/src/lib/utils.ts (1)
  • cn (27-29)
🔇 Additional comments (55)
frontend/src/components/ui/select.tsx (1)

25-25: LGTM! Height adjustment improves visual consistency.

The change from h-8 to h-9 increases the SelectTrigger height slightly for better alignment with other form elements in the new component search dialogs.

frontend/wailsjs/go/service/ComponentServiceImpl.js (1)

37-39: LGTM! Auto-generated binding follows established pattern.

The new SearchComponents function is correctly generated and follows the same pattern as other service bindings in this file.

frontend/wailsjs/go/service/ComponentServiceImpl.d.ts (1)

21-21: LGTM! TypeScript definition is correctly typed.

The function signature properly uses entities.ComponentSearchRequest and returns Promise<entities.ComponentSearchResponse>, maintaining type safety between frontend and backend.

backend/service/component_service.go (2)

3-3: LGTM! Copyright year update is appropriate.


36-36: LGTM! Well-designed interface method addition.

The SearchComponents method follows Go conventions with proper parameter and return types, including error handling. The signature is consistent with other interface methods.

main.go (3)

90-90: LGTM! Proper mapper instantiation.

The scanossApiMapper is correctly instantiated using the factory method pattern.


93-93: LGTM! Proper service instantiation with dependency injection.

The scanossApiService is correctly instantiated with the mapper dependency injected through the constructor.


94-94: LGTM! Component service updated with new dependencies.

The component service constructor has been properly updated to include the new scanossApiService and maintains the correct parameter order as expected by the implementation.

backend/service/scanoss_api_service.go (1)

28-30: LGTM! Well-designed service interface.

The interface follows Go best practices with:

  • Single responsibility principle
  • Clear method signature with proper error handling
  • Abstraction of implementation details
  • Use of domain entities for strong typing
frontend/src/components/ComponentSearchTable.tsx (2)

7-13: LGTM! Well-typed component interface.

The component props are properly typed using the generated entities, and the callback pattern for component selection is appropriate.


24-37: LGTM! Proper table implementation with good UX.

The table implementation includes:

  • Semantic table structure with proper headers
  • Unique key using component.purl (appropriate for component search results)
  • Clickable rows with visual feedback (cursor-pointer)
  • External link icon for URLs enhancing user understanding
  • Clean, accessible markup
go.mod (2)

3-5: LGTM! Reasonable Go version upgrade.

Upgrading to Go 1.23.0 with toolchain 1.23.2 is appropriate for accessing latest language features and improvements.


15-20: Dependency Security Verification

  • google.golang.org/grpc v1.73.0: No known unpatched vulnerabilities as of July 2025; incorporates all previous HTTP/2 and CVE fixes.
  • github.com/scanoss/papi v0.12.0: No publicly reported security issues.

Both versions are current stable releases. Continue routine monitoring for new advisories.

frontend/wailsjs/go/models.ts (4)

182-203: LGTM! ComponentSearchRequest model properly generated.

The model correctly mirrors the backend Go struct with:

  • Required search field
  • Optional fields properly marked with ?
  • Standard createFrom/constructor pattern consistent with other models

204-217: LGTM! StatusResponse model is correctly structured.

Simple model with optional code and message fields matching the backend entity definition.


218-233: LGTM! SearchedComponent model properly defined.

All required fields (component, purl, url) are correctly typed as strings, matching the backend struct.


234-265: LGTM! ComponentSearchResponse model with proper nested object handling.

The model correctly:

  • Handles the components array with proper type conversion
  • Includes status field with nested object conversion
  • Uses the standard convertValues method for recursive object instantiation

The convertValues implementation properly handles arrays, objects, and primitive types.

backend/entities/component_search.go (4)

27-34: LGTM! Well-structured request entity with appropriate validation.

The ComponentSearchRequest struct is well-designed with proper JSON tags and validation. The required,min=1 validation on the search field ensures non-empty searches.


37-40: LGTM! Response structure follows good API design patterns.

The ComponentSearchResponse struct properly encapsulates both the data (components) and metadata (status) in a single response structure.


43-47: LGTM! Clean and focused component representation.

The SearchedComponent struct contains the essential fields needed for component identification and linking.


50-53: Confirm Alignment of StatusResponse.Code with gRPC API

We’ve verified that StatusResponse.Code and its mappings in scanoss_api_mapper.go consistently use int32 across the codebase. No .proto files were found here, so please double-check that your external gRPC API definitions also use int32 (rather than, for example, uint32 or an enum). If they differ, update this struct to match the spec.

frontend/src/components/ui/table.tsx (7)

5-17: LGTM! Well-implemented Table component with proper overflow handling.

The Table component correctly uses forwardRef and wraps the table in a scrollable container for responsive design.


19-25: LGTM! TableHeader follows semantic HTML structure.

The component properly uses thead element with appropriate border styling for table headers.


27-37: LGTM! TableBody component is correctly implemented.

The component uses proper tbody semantics and handles last row border styling appropriately.


54-67: LGTM! TableRow includes good interactive states.

The component properly implements hover states and selected state styling using data attributes.


69-82: LGTM! TableHead component handles accessibility features.

The component includes proper checkbox positioning and role attribute handling for accessibility.


84-97: LGTM! TableCell maintains consistent styling with TableHead.

The component mirrors the checkbox and role handling from TableHead while using appropriate cell padding.


111-120: LGTM! Clean exports for the table component library.

All table components are properly exported for external consumption.

backend/service/component_service_impl.go (4)

43-43: LGTM! Proper dependency injection pattern.

Adding the scanossApiService field follows good dependency injection practices.


55-55: LGTM! Proper field initialization.

The new dependency is correctly assigned in the constructor.


287-295: LGTM! Clean delegation pattern with appropriate logging.

The SearchComponents method properly delegates to the injected service with good debug logging. The log fields provide useful context without exposing sensitive information.


50-50: Constructor signature change fully propagated

All calls to NewComponentServiceImpl include the new scanossApiService argument (e.g., in main.go), and no other instantiation sites were found. No further updates required.

backend/mappers/scanoss_api_mapper.go (4)

31-34: LGTM! Well-defined mapper interface.

The interface clearly defines the mapping operations needed for gRPC communication.


45-54: LGTM! Straightforward request mapping.

The mapping from internal entity to gRPC request is direct and correct.


57-66: LGTM! Proper slice mapping with type conversion.

The component slice mapping correctly handles the conversion between gRPC and internal types.


76-81: LGTM! Proper status mapping with nil check.

The status mapping correctly handles the optional status field and properly converts the gRPC status to internal format.

frontend/src/components/OnlineComponentSearchDialog.tsx (7)

40-42: LGTM! Well-defined constants for configuration.

The debounce timing and status code constants improve code readability and maintainability.


104-122: LGTM! Robust query function with proper error handling.

The React Query implementation correctly handles empty searches, API calls, and error scenarios.


131-141: LGTM! Good error handling with user feedback.

The error handling properly displays toast notifications and logs errors for debugging.


184-189: LGTM! Clear loading state with context.

The loading indicator includes the current search term, providing good user feedback.


191-199: LGTM! Comprehensive error state messaging.

The error state provides clear messaging and actionable guidance for users.


211-218: LGTM! Well-structured results display with scrollable area.

The results section properly handles the display of search results with appropriate scrolling and count information.


116-118: Confirm status code mapping
I couldn’t find a matching “success” code definition in the backend for the frontend’s

const STATUS_CODE_OK = 1

Please verify that your HTTP/JSON handlers (or your .proto-defined StatusCode enum) actually return status.code = 1 on success. If the backend uses a different value (e.g. 0), either update STATUS_CODE_OK or align the backend’s constant accordingly.

• Frontend: frontend/src/components/OnlineComponentSearchDialog.tsx:41
• Look in your Go code for the JSON response wrapper (e.g. a Status struct under backend/… with json:"status" and its code constants)
• If you’re using gRPC, inspect all *.proto files for the StatusCode enum values

frontend/src/components/ReplaceComponentDialog.tsx (7)

26-26: LGTM!

The Search icon import is correctly added alongside other lucide-react icons.


41-41: LGTM!

The OnlineComponentSearchDialog component import follows the existing import pattern.


74-76: LGTM!

The new state variables are properly initialized and follow React conventions.


129-144: LGTM!

The handler correctly manages component selection, prevents duplicates, and updates the form consistently with the existing patterns.


194-194: LGTM!

Converting CommandInput to a controlled component enables proper tracking of the search value.


209-222: LGTM!

The online search option is well-implemented with proper conditional rendering and state management.


310-316: LGTM!

The OnlineComponentSearchDialog integration follows the established pattern and properly manages dialog state.

backend/service/scanoss_api_service_grpc_impl.go (5)

24-41: LGTM!

Package declaration and imports are well-organized following Go conventions.


43-50: LGTM!

The struct is well-designed with all necessary fields for a gRPC client service.


52-68: Verify error handling strategy for gRPC client initialization.

The constructor logs but doesn't return the error when gRPC client initialization fails. This allows service creation to succeed even without a working gRPC connection. Ensure this graceful degradation approach is intentional and that downstream error handling in SearchComponents is sufficient.


70-108: LGTM!

The SearchComponents method is well-implemented with proper validation, error handling, and timeout management.


140-154: LGTM!

The helper methods for authentication and connection cleanup are properly implemented.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
backend/service/scanoss_api_service_grpc_impl.go (3)

26-41: Organize imports by group for better readability.

Group imports following Go conventions: standard library, external packages, then internal packages.

 import (
 	"context"
 	"crypto/tls"
 	"fmt"
 	"time"
 
 	"github.com/rs/zerolog/log"
 	"github.com/scanoss/papi/api/componentsv2"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/metadata"
+
 	"github.com/scanoss/scanoss.cc/backend/entities"
 	"github.com/scanoss/scanoss.cc/backend/mappers"
 	"github.com/scanoss/scanoss.cc/internal/config"
 	"github.com/scanoss/scanoss.cc/internal/utils"
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/credentials"
-	"google.golang.org/grpc/metadata"
 )

58-58: Consider making the timeout configurable.

The timeout is hardcoded to 30 seconds. Consider making it configurable through environment variables or configuration.

+const defaultTimeout = 30 * time.Second
+
 func NewScanossApiServiceGrpcImpl(
 	mapper mappers.ScanossApiMapper,
 ) ScanossApiService {
 	cfg := config.GetInstance()
+	timeout := defaultTimeout
+	if configuredTimeout := cfg.GetGrpcTimeout(); configuredTimeout > 0 {
+		timeout = configuredTimeout
+	}
 	service := &ScanossApiServiceGrpcImpl{
 		mapper:   mapper,
-		timeout:  30 * time.Second,
+		timeout:  timeout,
 		endpoint: utils.RemoveProtocolFromURL(cfg.GetApiUrl()),
 		apiKey:   cfg.GetApiToken(),
 	}

125-125: Consider adding gRPC connection options for production use.

The gRPC client creation could benefit from additional options for better reliability and observability.

-	conn, err := grpc.NewClient(s.endpoint, grpc.WithTransportCredentials(credentials.NewTLS(config)))
+	conn, err := grpc.NewClient(
+		s.endpoint,
+		grpc.WithTransportCredentials(credentials.NewTLS(config)),
+		grpc.WithDefaultCallOptions(
+			grpc.MaxCallRecvMsgSize(10*1024*1024), // 10MB max message size
+		),
+	)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c018d1 and 869524d.

📒 Files selected for processing (3)
  • backend/service/scanoss_api_service_grpc_impl.go (1 hunks)
  • frontend/src/components/OnlineComponentSearchDialog.tsx (1 hunks)
  • internal/utils/url_utils.go (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/components/OnlineComponentSearchDialog.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/utils/url_utils.go
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/service/scanoss_api_service_grpc_impl.go (7)
backend/mappers/scanoss_api_mapper.go (1)
  • ScanossApiMapper (31-34)
backend/service/scanoss_api_service.go (1)
  • ScanossApiService (28-30)
internal/config/config.go (3)
  • GetInstance (111-116)
  • SCANOSS_PREMIUM_API_URL (55-55)
  • Config (58-68)
internal/utils/url_utils.go (1)
  • RemoveProtocolFromURL (7-19)
backend/entities/component_search.go (2)
  • ComponentSearchRequest (27-34)
  • ComponentSearchResponse (37-40)
internal/utils/custom_validations.go (1)
  • GetValidator (37-39)
backend/entities/component.go (1)
  • Component (104-158)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🔭 Outside diff range comments (2)
frontend/src/components/ui/popover.tsx (1)

16-27: Replace PopoverPrimitive.PopoverPortal with PopoverPrimitive.Portal

In @radix-ui/react-popover v1.1.14, the Popover portal component is exported as Portal. Continuing to use PopoverPrimitive.PopoverPortal will be undefined at runtime.

Please update in frontend/src/components/ui/popover.tsx (around lines 16–27):

-  <PopoverPrimitive.PopoverPortal>
+  <PopoverPrimitive.Portal>
     <PopoverPrimitive.Content
       ref={ref}
       align={align}
       sideOffset={sideOffset}
       className={cn(
         'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
         className
       )}
       {...props}
     />
-  </PopoverPrimitive.PopoverPortal>
+  </PopoverPrimitive.Portal>
frontend/src/components/SelectLicenseList.tsx (1)

48-52: Fix stale callbacks and reduce shadowing.

useCallback hooks have empty deps and capture onSelect from props; this can stale if onSelect changes. Also, value parameter shadows state value, which is confusing.

-  const handleSelect = useCallback((value: string) => {
-    setValue(value);
-    onSelect(value);
-    setPopoverOpen(false);
-  }, []);
+  const handleSelect = useCallback((licenseId: string) => {
+    setValue(licenseId);
+    onSelect(licenseId);
+    setPopoverOpen(false);
+  }, [onSelect]);

-  const handleClearSelection = useCallback(() => {
-    setValue(undefined);
-    onSelect(undefined);
-  }, []);
+  const handleClearSelection = useCallback(() => {
+    setValue(undefined);
+    onSelect(undefined);
+  }, [onSelect]);

Also applies to: 54-57

🧹 Nitpick comments (1)
frontend/src/components/SelectLicenseList.tsx (1)

69-71: Add accessible label to the clear button.

Small a11y nit: provide an aria-label so the icon-only button is announced properly.

-          <Button size="icon" variant="ghost" onClick={handleClearSelection}>
+          <Button size="icon" variant="ghost" onClick={handleClearSelection} aria-label="Clear selected license">
             <X className="h-4 w-4" />
           </Button>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d94aca and 9b8f334.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • frontend/package.json (1 hunks)
  • frontend/package.json.md5 (1 hunks)
  • frontend/src/components/SelectLicenseList.tsx (1 hunks)
  • frontend/src/components/ui/dialog.tsx (0 hunks)
  • frontend/src/components/ui/popover.tsx (2 hunks)
💤 Files with no reviewable changes (1)
  • frontend/src/components/ui/dialog.tsx
✅ Files skipped from review due to trivial changes (2)
  • frontend/package.json
  • frontend/package.json.md5
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/src/components/SelectLicenseList.tsx (1)
frontend/src/components/ui/popover.tsx (1)
  • Popover (31-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
frontend/src/components/ShortcutBadge.tsx (1)

1-3: Use semantic and add a11y hints; consider a slightly larger font

Span works, but is the semantic element for keyboard shortcuts. Also, 8px text is very small; 10–11px is typically the lower bound for readability.

Apply this minimal change:

-export default function ShortcutBadge({ shortcut }: { shortcut: string }) {
-  return <span className="ml-2 rounded-sm bg-card p-1 text-[8px] leading-none">{shortcut}</span>;
-}
+export default function ShortcutBadge({ shortcut }: { shortcut: string }) {
+  return (
+    <kbd
+      title={`Shortcut: ${shortcut}`}
+      aria-label={`Shortcut: ${shortcut}`}
+      className="ml-2 rounded-sm bg-card p-1 text-[10px] leading-none font-mono"
+    >
+      {shortcut}
+    </kbd>
+  );
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f24044 and 12ae09d.

📒 Files selected for processing (2)
  • frontend/src/components/ReplaceComponentDialog.tsx (9 hunks)
  • frontend/src/components/ShortcutBadge.tsx (1 hunks)
🔇 Additional comments (3)
frontend/src/components/ReplaceComponentDialog.tsx (3)

204-204: Controlled CommandInput looks good

Binding value/onValueChange to searchValue is correct and unlocks the inline “Search online” action. No issues.


146-150: Good UX: opening online search closes the popover

The flow to open the online search dialog and close the popover is sound. This prevents layered overlays.


317-323: Online dialog wiring looks correct

Props passed (onOpenChange, searchTerm, onComponentSelect) align with the expected flow. Rendering is gated by onlineSearchDialogOpen. LGTM.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
main.go (1)

92-96: LGTM; minor nit on error wrapping

Wiring the new Scanoss API service and injecting it into ComponentServiceImpl is correct. Consider %w instead of %v to preserve error wrapping.

- return fmt.Errorf("error initializing scanoss api service: %v", err)
+ return fmt.Errorf("error initializing scanoss api service: %w", err)

Also, for future enhancements, consider propagating a context into outbound API calls for cancellation/timeout alignment with app lifecycle.

backend/service/scanoss_api_service_http_impl.go (4)

92-102: Safer URL construction

If endpoint already includes a trailing slash, simple formatting can yield a double slash. Prefer url.JoinPath, which handles slashes correctly.

- apiURL := fmt.Sprintf("%s/api/v2/components/search", s.endpoint)
+ apiURL, err := url.JoinPath(s.endpoint, "/api/v2/components/search")
+ if err != nil {
+     return entities.ComponentSearchResponse{}, fmt.Errorf("failed to construct API URL: %w", err)
+ }

Add the import:

import "net/url"

110-130: Avoid reading full body into memory; stream-decode JSON

For large responses, prefer streaming decode over io.ReadAll. This also reduces memory spikes.

- body, err := io.ReadAll(resp.Body)
- if err != nil {
-     log.Error().Err(err).Msg("Error reading API response")
-     return entities.ComponentSearchResponse{}, fmt.Errorf("failed to read response: %w", err)
- }
- 
- if resp.StatusCode != http.StatusOK {
-     log.Error().
-         Int("statusCode", resp.StatusCode).
-         Str("body", string(body)).
-         Msg("API returned non-200 status")
-     return entities.ComponentSearchResponse{}, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
- }
- 
- var apiResponse entities.ComponentSearchResponse
- 
- if err := json.Unmarshal(body, &apiResponse); err != nil {
-     log.Error().Err(err).Str("body", string(body)).Msg("Error parsing API response")
-     return entities.ComponentSearchResponse{}, fmt.Errorf("failed to parse response: %w", err)
- }
+ if resp.StatusCode != http.StatusOK {
+     // Read a bounded portion of the body for logging on error
+     limited := io.LimitedReader{R: resp.Body, N: 4096}
+     errBody, _ := io.ReadAll(&limited)
+     log.Error().
+         Int("statusCode", resp.StatusCode).
+         Str("body", string(errBody)).
+         Msg("API returned non-200 status")
+     return entities.ComponentSearchResponse{}, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(errBody))
+ }
+
+ var apiResponse entities.ComponentSearchResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil {
+     log.Error().Err(err).Msg("Error parsing API response")
+     return entities.ComponentSearchResponse{}, fmt.Errorf("failed to parse response: %w", err)
+ }

131-146: Redundant slice copy; simplify and ensure empty slice vs nil

You copy fields 1:1 into a new slice, then rebuild the response. Instead, return the decoded response and normalize a nil slice to an empty slice to keep [] in JSON (better for TS consumers).

- components := make([]entities.SearchedComponent, len(apiResponse.Components))
- for i, comp := range apiResponse.Components {
-     components[i] = entities.SearchedComponent{
-         Component: comp.Component,
-         Purl:      comp.Purl,
-         URL:       comp.URL,
-     }
- }
-
- response := entities.ComponentSearchResponse{
-     Components: components,
-     Status: entities.StatusResponse{
-         Status:  apiResponse.Status.Status,
-         Message: apiResponse.Status.Message,
-     },
- }
+ if apiResponse.Components == nil {
+     apiResponse.Components = []entities.SearchedComponent{}
+ }
+ response := apiResponse

69-73: Validation log level

Invalid input from the UI isn’t necessarily an error condition; consider logging at warn/debug and returning a typed validation error so the UI can present field-level messages.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12ae09d and eef6e88.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (6)
  • backend/entities/component_search.go (1 hunks)
  • backend/service/scanoss_api_service_http_impl.go (1 hunks)
  • frontend/src/components/OnlineComponentSearchDialog.tsx (1 hunks)
  • frontend/wailsjs/go/models.ts (3 hunks)
  • go.mod (3 hunks)
  • main.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/entities/component_search.go
  • frontend/src/components/OnlineComponentSearchDialog.tsx
🧰 Additional context used
🧬 Code Graph Analysis (3)
frontend/wailsjs/go/models.ts (1)
backend/entities/component_search.go (4)
  • ComponentSearchRequest (27-34)
  • StatusResponse (50-53)
  • SearchedComponent (43-47)
  • ComponentSearchResponse (37-40)
backend/service/scanoss_api_service_http_impl.go (5)
backend/service/scanoss_api_service.go (1)
  • ScanossApiService (28-30)
internal/config/config.go (2)
  • GetInstance (111-116)
  • SCANOSS_PREMIUM_API_URL (55-55)
backend/entities/component_search.go (4)
  • ComponentSearchRequest (27-34)
  • ComponentSearchResponse (37-40)
  • SearchedComponent (43-47)
  • StatusResponse (50-53)
internal/utils/custom_validations.go (1)
  • GetValidator (37-39)
internal/fetch/fetch.go (2)
  • Options (32-36)
  • Fetch (39-59)
main.go (2)
backend/service/scanoss_api_service_http_impl.go (1)
  • NewScanossApiServiceHttpImpl (47-65)
backend/service/component_service_impl.go (1)
  • NewComponentServiceImpl (50-65)
🔇 Additional comments (6)
go.mod (1)

30-31: Indirect deps look fine

Bumping github.com/google/uuid to v1.6.0 and adding github.com/google/go-cmp v0.7.0 as indirects is okay. Make sure go mod tidy -compat=1.23 has been run so go.sum is consistent across environments.

frontend/wailsjs/go/models.ts (5)

182-203: TS model matches backend request schema

Fields align with backend entities.ComponentSearchRequest. This looks correct and consistent with Wails’ generated models.


204-217: StatusResponse is consistent

Optional fields match backend omitempty semantics.


218-233: SearchedComponent aligns with backend

Naming and required fields are consistent with Go entity.


234-265: ComponentSearchResponse constructor is correct; consider empty-array default (handled in backend)

TS constructor is fine. Since backend ensures components is an empty slice (not null) on response, the UI won’t see undefined here. No change needed in generated models.


689-730: TreeNode model addition looks good

Shape and recursive convertValues utility are consistent with the rest of the generated models.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (1)
backend/service/license_service_impl.go (1)

26-31: Add defensive checks and basic request validation in GetLicensesByPurl

If scanossApiService is ever nil (e.g., misconfigured wiring), this will panic. Also, validate request.Purl early to avoid unnecessary calls.

Apply this diff:

 import (
 	"sort"
 
 	"github.com/scanoss/scanoss.cc/backend/entities"
 	"github.com/scanoss/scanoss.cc/backend/repository"
+	"fmt"
+	"strings"
 )
@@
 func (s *LicenseServiceImpl) GetLicensesByPurl(request entities.ComponentRequest) (entities.GetLicensesByPurlResponse, error) {
-	return s.scanossApiService.GetLicensesByPurl(request)
+	if s.scanossApiService == nil {
+		return entities.GetLicensesByPurlResponse{}, fmt.Errorf("scanossApiService is not configured")
+	}
+	if strings.TrimSpace(request.Purl) == "" {
+		return entities.GetLicensesByPurlResponse{}, fmt.Errorf("purl is required")
+	}
+	return s.scanossApiService.GetLicensesByPurl(request)
 }

Also applies to: 58-60

🧹 Nitpick comments (8)
backend/entities/common.go (2)

3-6: Exported types lack GoDoc comments

Add brief comments for exported types to satisfy Go conventions and improve readability.

Apply this diff:

 type ComponentRequest struct {
+	// Purl is the package URL of the component to query (required).
 	Purl        string `json:"purl" validate:"required,min=1"`
+	// Requirement is an optional version constraint (e.g., semver range).
 	Requirement string `json:"requirement,omitempty"`
 }

8-11: Consider making StatusResponse optional in responses

If some APIs don’t always populate status, consider omitempty on the field that embeds StatusResponse (e.g., Status StatusResponse 'json:"status,omitempty"') to avoid empty objects in JSON.

backend/service/license_service.go (1)

30-30: Consider context-aware API

Since downstream ScanossApiService manages context (via SetContext(ctx)), consider adding context.Context to this interface method to propagate cancellations/deadlines.

Example:

GetLicensesByPurl(ctx context.Context, request entities.ComponentRequest) (entities.GetLicensesByPurlResponse, error)

This would be a broader change; treat as a follow-up enhancement.

backend/service/license_service_impl.go (2)

45-56: Sorting by LicenseId is fine; consider stable sort by Name if UX prefers

Not a blocker. If UI expects alphabetical names, sorting by Name may be more intuitive.


58-60: Unit tests missing for new method

Please add unit tests for GetLicensesByPurl covering:

  • Valid request path (delegation to scanossApiService)
  • Empty PURL validation (if adopted)
  • Error propagation from scanossApiService

I can scaffold a test with a mock ScanossApiService if you want.

backend/entities/license.go (2)

37-41: Go naming: prefer ID over Id

For consistency with Go conventions, consider ID (e.g., ID string) instead of Id. The file already uses LicenseId elsewhere, so align project-wide or leave as-is for consistency.


50-53: Consider omitempty on response fields

If status or component may be absent, add omitempty to avoid emitting empty objects in JSON.

Apply this diff:

 type GetLicensesByPurlResponse struct {
-	Status    StatusResponse       `json:"status"`
-	Component ComponentLicenseInfo `json:"component"`
+	Status    StatusResponse       `json:"status,omitempty"`
+	Component ComponentLicenseInfo `json:"component,omitempty"`
 }
backend/service/scanoss_api_service_http_impl.go (1)

172-209: Fix inconsistent error message in GetLicensesByPurl.

Line 192 contains an incorrect error message referring to "component search API" when it should refer to "license API".

-		log.Error().Err(err).Msg("Error calling SCANOSS component search API")
+		log.Error().Err(err).Msg("Error calling SCANOSS license API")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eef6e88 and 3cfeca5.

📒 Files selected for processing (13)
  • backend/entities/common.go (1 hunks)
  • backend/entities/component_search.go (1 hunks)
  • backend/entities/license.go (1 hunks)
  • backend/service/component_service_impl.go (2 hunks)
  • backend/service/license_service.go (1 hunks)
  • backend/service/license_service_impl.go (2 hunks)
  • backend/service/scanoss_api_service.go (1 hunks)
  • backend/service/scanoss_api_service_http_impl.go (1 hunks)
  • frontend/src/components/ReplaceComponentDialog.tsx (10 hunks)
  • frontend/wailsjs/go/models.ts (5 hunks)
  • frontend/wailsjs/go/service/LicenseServiceImpl.d.ts (1 hunks)
  • frontend/wailsjs/go/service/LicenseServiceImpl.js (1 hunks)
  • main.go (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • frontend/wailsjs/go/service/LicenseServiceImpl.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/service/component_service_impl.go
  • backend/service/scanoss_api_service.go
  • frontend/src/components/ReplaceComponentDialog.tsx
🧰 Additional context used
🧬 Code Graph Analysis (8)
backend/service/license_service.go (4)
frontend/wailsjs/go/service/LicenseServiceImpl.js (1)
  • GetLicensesByPurl (9-11)
backend/entities/common.go (1)
  • ComponentRequest (3-6)
frontend/wailsjs/go/models.ts (2)
  • ComponentRequest (234-247)
  • GetLicensesByPurlResponse (378-409)
backend/entities/license.go (1)
  • GetLicensesByPurlResponse (50-53)
frontend/wailsjs/go/service/LicenseServiceImpl.d.ts (4)
frontend/wailsjs/go/service/LicenseServiceImpl.js (1)
  • GetLicensesByPurl (9-11)
backend/entities/common.go (1)
  • ComponentRequest (3-6)
frontend/wailsjs/go/models.ts (2)
  • ComponentRequest (234-247)
  • GetLicensesByPurlResponse (378-409)
backend/entities/license.go (1)
  • GetLicensesByPurlResponse (50-53)
backend/entities/license.go (3)
frontend/wailsjs/go/models.ts (4)
  • License (444-459)
  • LicenseInfo (182-195)
  • StatusResponse (270-283)
  • ComponentLicenseInfo (196-233)
backend/entities/common.go (1)
  • StatusResponse (8-11)
backend/entities/component.go (1)
  • Component (104-158)
backend/service/license_service_impl.go (6)
backend/repository/license_repository.go (1)
  • LicenseRepository (28-30)
backend/service/scanoss_api_service.go (1)
  • ScanossApiService (32-36)
backend/service/license_service.go (1)
  • LicenseService (28-31)
backend/entities/common.go (1)
  • ComponentRequest (3-6)
frontend/wailsjs/go/models.ts (2)
  • ComponentRequest (234-247)
  • GetLicensesByPurlResponse (378-409)
backend/entities/license.go (1)
  • GetLicensesByPurlResponse (50-53)
backend/entities/component_search.go (3)
backend/entities/component.go (1)
  • Component (104-158)
frontend/wailsjs/go/models.ts (2)
  • SearchedComponent (284-299)
  • StatusResponse (270-283)
backend/entities/common.go (1)
  • StatusResponse (8-11)
main.go (4)
backend/service/scanoss_api_service_http_impl.go (1)
  • NewScanossApiServiceHttpImpl (55-74)
backend/service/component_service_impl.go (1)
  • NewComponentServiceImpl (50-65)
backend/service/scanoss_settings_service_impl.go (1)
  • NewScanossSettingsServiceImpl (35-39)
backend/service/license_service_impl.go (1)
  • NewLicenseServiceImpl (38-43)
frontend/wailsjs/go/models.ts (3)
backend/entities/license.go (3)
  • LicenseInfo (37-40)
  • ComponentLicenseInfo (42-48)
  • GetLicensesByPurlResponse (50-53)
backend/entities/common.go (2)
  • ComponentRequest (3-6)
  • StatusResponse (8-11)
backend/entities/component_search.go (3)
  • ComponentSearchRequest (27-34)
  • SearchedComponent (37-41)
  • ComponentSearchResponse (44-47)
backend/service/scanoss_api_service_http_impl.go (7)
backend/service/scanoss_api_service.go (1)
  • ScanossApiService (32-36)
internal/config/config.go (2)
  • GetInstance (111-116)
  • SCANOSS_PREMIUM_API_URL (55-55)
backend/entities/component_search.go (2)
  • ComponentSearchRequest (27-34)
  • ComponentSearchResponse (44-47)
frontend/wailsjs/go/models.ts (4)
  • ComponentSearchRequest (248-269)
  • ComponentSearchResponse (300-331)
  • ComponentRequest (234-247)
  • GetLicensesByPurlResponse (378-409)
internal/utils/custom_validations.go (1)
  • GetValidator (37-39)
backend/entities/common.go (1)
  • ComponentRequest (3-6)
backend/entities/license.go (1)
  • GetLicensesByPurlResponse (50-53)
🔇 Additional comments (26)
frontend/wailsjs/go/service/LicenseServiceImpl.d.ts (1)

7-7: LGTM: New declaration aligns with models and JS wrapper

The signature GetLicensesByPurl(arg1: entities.ComponentRequest): Promise<entities.GetLicensesByPurlResponse> matches frontend models and the JS bridge wrapper.

backend/service/license_service_impl.go (1)

38-42: Constructor signature change: verify all call sites updated

NewLicenseServiceImpl now requires scanossApiService. Ensure all initializers (e.g., main wiring, tests) are updated accordingly to avoid nil injection.

I can generate a repo-wide check to locate calls to NewLicenseServiceImpl and verify argument counts if useful.

backend/entities/component_search.go (4)

1-23: LGTM - Standard MIT license header.

The license header follows proper MIT license format and is consistent with the project's licensing approach.


26-34: LGTM - Well-structured request entity with proper validation.

The ComponentSearchRequest struct is well-designed with appropriate JSON tags and validation annotations. The required Search field with minimum length validation ensures API calls have meaningful search terms.


36-41: LGTM - Clean and focused response component entity.

The SearchedComponent struct provides the essential fields needed for component search results with clear, descriptive field names.


43-47: Confirm StatusResponse Reference and JSON Structure

  • StatusResponse is defined in backend/entities/common.go (lines 8–11) with fields
    Status  string `json:"status,omitempty"`
    Message string `json:"message,omitempty"`
  • No other API response struct in the codebase embeds StatusResponse.
  • ComponentSearchResponse correctly imports and uses it under json:"status".
  • Please verify that the JSON keys (components and status) match the actual API response format, and consider adding omitempty to the outer status tag if empty statuses should be omitted.
main.go (3)

92-96: LGTM - Proper service initialization with error handling.

The scanossApiService initialization follows the established pattern with appropriate error handling and clear error messaging.


96-96: LGTM - Correct service dependency injection.

The updated service constructors properly inject the new scanossApiService dependency into both componentService and licenseService.

Also applies to: 101-101


116-116: LGTM - Context propagation to the new service.

The context setup for scanossApiService follows the established pattern and ensures proper context propagation for HTTP operations.

frontend/wailsjs/go/models.ts (8)

182-195: LGTM - Properly generated TypeScript binding for LicenseInfo.

The LicenseInfo class correctly mirrors the backend entity with appropriate field mapping and follows the established pattern for generated TypeScript bindings.


196-233: LGTM - Comprehensive TypeScript binding with nested object handling.

The ComponentLicenseInfo class properly handles nested LicenseInfo object conversion using the convertValues helper method, maintaining type safety.


234-247: LGTM - Clean request entity binding.

The ComponentRequest class correctly represents the backend entity with appropriate field mapping.


248-269: LGTM - Well-structured search request binding.

The ComponentSearchRequest class properly maps all the backend fields including optional parameters with correct TypeScript typing.


270-283: LGTM - Status response binding maintains consistency.

The StatusResponse class follows the established pattern and correctly maps the backend entity fields.


284-299: LGTM - Component search result binding.

The SearchedComponent class correctly represents the search result entity with all required fields properly mapped.


300-331: LGTM - Complex response binding with proper nested object handling.

The ComponentSearchResponse class correctly handles the nested array of SearchedComponent objects and the StatusResponse using the convertValues helper method.


378-409: LGTM - License response binding with nested object conversion.

The GetLicensesByPurlResponse class properly handles nested object conversion for both StatusResponse and ComponentLicenseInfo using the convertValues helper.

backend/service/scanoss_api_service_http_impl.go (9)

1-23: LGTM - Standard MIT license header.

The license header follows proper MIT license format and is consistent with the project's licensing approach.


41-46: LGTM - Clean interface abstraction and type definitions.

The HTTPClient interface provides good testability and the QueryParams type alias improves code readability.


47-54: LGTM - Well-structured service implementation.

The ScanossApiServiceHttpImpl struct includes all necessary fields for HTTP operations with proper timeout management.


55-74: LGTM - Robust service constructor with configuration.

The constructor properly reads configuration, provides sensible defaults, and initializes the HTTP client with appropriate timeout settings.


76-78: LGTM - Context management implementation.

The SetContext method properly stores the context for use in HTTP requests.


80-96: LGTM - Robust URL building with proper encoding.

The buildURL method correctly handles URL construction and query parameter encoding, with proper error handling.


98-118: LGTM - HTTP request implementation with proper context and headers.

The GetWithParams method correctly uses the stored context, sets appropriate headers, and handles errors properly. This addresses the previous concern about unused client/timeout fields.


121-169: LGTM - Comprehensive component search implementation.

The SearchComponents method includes proper input validation, detailed logging, API key verification, parameter building, error handling, and response decoding. The implementation is thorough and follows good practices.


172-209: LGTM - Well-implemented license fetching method.

Aside from the minor error message issue, the GetLicensesByPurl method follows the same robust pattern as SearchComponents with proper validation, logging, and error handling.

Comment on lines +3 to +6
type ComponentRequest struct {
Purl string `json:"purl" validate:"required,min=1"`
Requirement string `json:"requirement,omitempty"`
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Validation tags are inert unless explicitly enforced

You’re using validate:"required,min=1" but there’s no enforcement shown here. Ensure the handling layer (service/controller) runs a validator against ComponentRequest to prevent empty purls from reaching downstream services.

If you’re using go-playground/validator, wire it where requests enter the system. For example, in your service method:

// pseudo-code in the service handling layer
if strings.TrimSpace(req.Purl) == "" {
    return ..., fmt.Errorf("purl is required")
}

Do you want me to propose a concrete validation snippet in the call sites?

Also applies to: 8-11


🏁 Script executed:

#!/bin/bash
# Search for Go Playground Validator imports and usage in the repository
rg 'go-playground/validator' -n
rg 'validator.New' -n
rg 'validate.Struct' -n

Length of output: 656


🏁 Script executed:

#!/bin/bash
# Display main.go around validator setup and handler registration
sed -n '1,200p' main.go
echo "----"
sed -n '200,400p' main.go

Length of output: 4916


🏁 Script executed:

#!/bin/bash
# Inspect internal/utils package for validator setup and custom validations
echo "Contents of internal/utils:"
ls internal/utils
echo ""
# Locate SetValidator definition
rg "func SetValidator" -n internal/utils
echo ""
# Show custom_validations.go to see ValidatePurl and SetValidator
sed -n '1,200p' internal/utils/custom_validations.go

Length of output: 2006


🏁 Script executed:

#!/bin/bash
# Locate the ComponentServiceImpl definition and its methods to identify where to insert validation
rg -n "type .*ComponentServiceImpl" -n
rg -A3 "NewComponentServiceImpl" -n
rg -R "ComponentServiceImpl" -n service

Length of output: 1559


Enforce validation on ComponentRequest inputs

The validate:"required,min=1" tag on Purl in backend/entities/common.go only takes effect if you explicitly invoke the validator. Add a call to your global validator at each entry point handling ComponentRequest to reject empty or malformed PURLs before they reach downstream logic:

• backend/service/component_service_impl.go

  • In CreateComponent (and similarly in any other method that accepts entities.ComponentRequest), invoke:
    func (s *ComponentServiceImpl) CreateComponent(ctx context.Context, req entities.ComponentRequest) (entities.Component, error) {
    +   if err := utils.GetValidator().Struct(req); err != nil {
    +       return entities.Component{}, fmt.Errorf("invalid component request: %w", err)
    +   }
        entity := s.mapper.ToEntity(req)
        // …
    }
  • Ensure you have:
    import (
        // …
        "github.com/scanoss/scanoss.cc/internal/utils"
    )

Apply the same pattern wherever ComponentRequest is consumed (e.g., update or batch handlers) so that your required,min=1 and custom valid-purl rules are actually enforced.

🤖 Prompt for AI Agents
In backend/entities/common.go around lines 3 to 6, the struct tag
validate:"required,min=1" on Purl won't run automatically; update each entry
point that accepts entities.ComponentRequest (start with
backend/service/component_service_impl.go in CreateComponent, and any
update/batch handlers) to call the global validator before further processing:
import the validator utility (github.com/scanoss/scanoss.cc/internal/utils) and
invoke the validation helper on the received ComponentRequest, and if validation
returns an error immediately return/propagate a bad-request validation error so
empty or malformed PURLs are rejected early.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🔭 Outside diff range comments (3)
frontend/src/components/SelectLicenseList.tsx (3)

61-66: Stale closure: add onSelect to useCallback deps and avoid param shadowing

handleSelect captures onSelect from the initial render; if the parent changes onSelect, this handler won’t update. Also, the param name value shadows the state variable.

-const handleSelect = useCallback((value: string) => {
-  setValue(value);
-  onSelect(value);
-  setPopoverOpen(false);
-}, []);
+const handleSelect = useCallback((licenseId: string) => {
+  setValue(licenseId);
+  onSelect(licenseId);
+  setPopoverOpen(false);
+}, [onSelect]);

67-70: Clear handler should close the popover and update deps

When clearing, also close the popover. Add onSelect to deps to avoid stale references.

-const handleClearSelection = useCallback(() => {
-  setValue(undefined);
-  onSelect(undefined);
-}, []);
+const handleClearSelection = useCallback(() => {
+  setValue(undefined);
+  onSelect(undefined);
+  setPopoverOpen(false);
+}, [onSelect]);

77-79: Selected label can render empty when chosen from Matched list

If the selected license exists only in matchedLicenses and not in licenses, licenses?.find(...)? .licenseId returns undefined and the button renders nothing. Use the selected value directly or resolve from both lists.

Simple and robust:

-            {value ? licenses?.find((license) => license.licenseId === value)?.licenseId : 'Select license'}
+            {value ?? 'Select license'}

If you prefer to resolve the full object, search both sources with a fallback:

const selected = value
  ? (matchedLicenses?.find(l => l.licenseId === value) ?? licenses?.find(l => l.licenseId === value))
  : undefined;

And render:

{selected ? selected.licenseId : 'Select license'}
♻️ Duplicate comments (5)
frontend/src/components/SelectLicenseList.tsx (1)

73-73: Modal popover blocks outside interactions; Clear button becomes unclickable

Using modal prevents pointer events outside the PopoverContent. Since the Clear button is outside the content, it won’t be clickable while the popover is open. Either remove modal, move the Clear button into the content, or ensure clearing also closes the popover.

Minimal fix (remove modal):

-<Popover open={popoverOpen} onOpenChange={setPopoverOpen} modal>
+<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
frontend/src/components/ReplaceComponentDialog.tsx (4)

116-129: Replace inline type with shared entities.SearchedComponent type

The inline type { component: string; purl: string; url: string } in onOnlineComponentSelected duplicates the structure defined in OnlineComponentSearchDialog. Use the shared entities.SearchedComponent type for consistency and type safety.

-const onOnlineComponentSelected = (component: { component: string; purl: string; url: string }) => {
+const onOnlineComponentSelected = (component: entities.SearchedComponent) => {

Also consider clearing searchValue after selection to avoid stale text in the input:

   setOnlineSearchDialogOpen(false);
+  setSearchValue('');

173-180: Critical: Conflicting keyboard shortcut bindings cause unintended behavior

You have two useKeyboardShortcut hooks bound to the same KEYBOARD_SHORTCUTS.confirm.keys, which will both fire when the popover is open, potentially causing the form to submit while trying to search online.

Consolidate into a single handler that branches based on popoverOpen:

-  const ref = useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => submitButtonRef.current?.click(), {
-    enableOnFormTags: true,
-  });
-
-  useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => handleSearchOnline(), {
-    enableOnFormTags: true,
-    enabled: popoverOpen,
-  });
+  const ref = useKeyboardShortcut(
+    KEYBOARD_SHORTCUTS.confirm.keys,
+    () => {
+      if (popoverOpen) {
+        handleSearchOnline();
+      } else {
+        submitButtonRef.current?.click();
+      }
+    },
+    { enableOnFormTags: true }
+  );

225-235: Use semantic button element for accessibility

The clickable <div> for "Search online" is not keyboard accessible. Replace it with a semantic <button> element to ensure proper keyboard navigation and screen reader support.

-                              {searchValue ? (
-                                <CommandItem asChild>
-                                  <div onClick={handleSearchOnline} className="flex items-center justify-between gap-2">
-                                    <div className="flex items-center gap-2">
-                                      <Search className="h-3 w-3" />
-                                      Search &ldquo;{searchValue}&rdquo; online
-                                    </div>
-                                    <ShortcutBadge shortcut="⌘ + Enter" />
-                                  </div>
-                                </CommandItem>
-                              ) : null}
+                              {searchValue ? (
+                                <CommandItem asChild>
+                                  <button
+                                    type="button"
+                                    onClick={handleSearchOnline}
+                                    className="flex w-full items-center justify-between gap-2 text-left"
+                                    aria-label={`Search "${searchValue}" online`}
+                                  >
+                                    <div className="flex items-center gap-2">
+                                      <Search className="h-3 w-3" />
+                                      Search &ldquo;{searchValue}&rdquo; online
+                                    </div>
+                                    <ShortcutBadge shortcut="⌘ + Enter" />
+                                  </button>
+                                </CommandItem>
+                              ) : null}

232-232: Use platform-aware shortcut display helper instead of hardcoding

The hardcoded "⌘ + Enter" is Mac-specific. Windows/Linux users expect "Ctrl + Enter". Import and use the existing getShortcutDisplay helper to automatically handle platform differences.

Add the import at the top of the file:

+import { getShortcutDisplay } from '@/utils/keyboardShortcuts';

Then update both occurrences:

-                                    <ShortcutBadge shortcut="⌘ + Enter" />
+                                    <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys)} />
-                  Confirm <ShortcutBadge shortcut="⌘ + Enter" />
+                  Confirm <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys)} />

Also applies to: 313-313

🧹 Nitpick comments (6)
frontend/src/components/SelectLicenseList.tsx (5)

41-51: Avoid shadowing and improve clarity by renaming prop

The prop value on LicenseItem is easy to confuse with the parent state value. Rename it to something explicit like selectedId.

Apply:

-function LicenseItem({ license, value }: { license: entities.License; value: string }) {
+function LicenseItem({ license, selectedId }: { license: entities.License; selectedId: string }) {
   return (
     <>
-      <Check className={cn('mr-2 h-4 w-4', license.licenseId === value ? 'opacity-100' : 'opacity-0')} />
+      <Check className={cn('mr-2 h-4 w-4', license.licenseId === selectedId ? 'opacity-100' : 'opacity-0')} />
       <div>
         <p>{license.licenseId}</p>
         <p className="text-xs text-muted-foreground">{license.name}</p>
       </div>
     </>
   );
 }

Note: If you adopt the direct type import (previous comment), replace entities.License accordingly.


53-53: Consider de-duplication between “Matched” and “Catalogued”

If a license appears in both lists, it will render twice. Filtering out matched IDs from the catalog improves UX.

Use this helper inside the component (outside the return) to compute a filtered list:

const matchedIds = new Set((matchedLicenses ?? []).map(l => l.licenseId));
const filteredLicenses = (licenses ?? []).filter(l => !matchedIds.has(l.licenseId));

Then render filteredLicenses in the Catalogued group (see suggested diff on Lines 102-106).


87-87: Constrain popover height to prevent viewport overflow

Without a scroll container, long lists can overflow. Add max height and scrolling.

-<PopoverContent className="min-w-[420px] p-0">
+<PopoverContent className="min-w-[420px] max-h-80 overflow-auto p-0">

95-97: Update LicenseItem prop name after rename

If you rename the prop in LicenseItem, update the usages accordingly.

-                  <LicenseItem license={license} value={value || ''} />
+                  <LicenseItem license={license} selectedId={value || ''} />

102-106: Use de-duplicated catalog list and update LicenseItem prop name

Render the filtered list (see helper in earlier comment), and update the prop if you renamed it.

-              {licenses?.map((license) => (
+              {filteredLicenses?.map((license) => (
                 <CommandItem value={license.licenseId} key={license.licenseId} onSelect={handleSelect}>
-                  <LicenseItem license={license} value={value || ''} />
+                  <LicenseItem license={license} selectedId={value || ''} />
                 </CommandItem>
               ))}

Optionally add a loading state (non-blocking):

const { data: licenses, isLoading } = useQuery({ queryKey: ['licenses'], queryFn: GetAllLicenses, staleTime: 24 * 60 * 60 * 1000 });

And show a hint:

<CommandEmpty>{isLoading ? 'Loading licenses…' : 'No licenses found.'}</CommandEmpty>
frontend/src/components/ReplaceComponentDialog.tsx (1)

131-150: Consider handling edge cases in license matching

The getMatchedLicenses function handles errors well, but consider these improvements:

  1. The hardcoded SPDX URL template on line 140 assumes all licenses have SPDX identifiers. Some licenses might not be in SPDX.
  2. No loading state is shown while fetching licenses.

Consider making the reference URL more robust:

         const matchedLicenses: entities.License[] = component.licenses.map((license) => ({
           name: license.full_name,
           licenseId: license.id,
-          reference: `https://spdx.org/licenses/${license.id}.html`, // This is a workaround for the fact that the reference is not available in the response
+          reference: license.reference || `https://spdx.org/licenses/${license.id}.html`,
         }));

For loading state, you could add a state variable and show a spinner/message while fetching.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cfeca5 and be54b88.

📒 Files selected for processing (5)
  • backend/entities/license.go (1 hunks)
  • frontend/src/components/OnlineComponentSearchDialog.tsx (1 hunks)
  • frontend/src/components/ReplaceComponentDialog.tsx (10 hunks)
  • frontend/src/components/SelectLicenseList.tsx (3 hunks)
  • frontend/wailsjs/go/models.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/entities/license.go
  • frontend/src/components/OnlineComponentSearchDialog.tsx
  • frontend/wailsjs/go/models.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
frontend/src/components/ReplaceComponentDialog.tsx (7)
frontend/wailsjs/go/models.ts (2)
  • DeclaredComponent (332-345)
  • License (444-459)
frontend/wailsjs/go/service/LicenseServiceImpl.js (1)
  • GetLicensesByPurl (9-11)
frontend/src/hooks/useKeyboardShortcut.tsx (1)
  • useKeyboardShortcut (31-52)
frontend/src/lib/shortcuts.ts (1)
  • KEYBOARD_SHORTCUTS (32-164)
frontend/src/components/ShortcutBadge.tsx (1)
  • ShortcutBadge (1-3)
frontend/src/components/NewComponentDialog.tsx (1)
  • NewComponentDialog (46-107)
frontend/src/components/OnlineComponentSearchDialog.tsx (1)
  • OnlineComponentSearchDialog (92-223)
frontend/src/components/SelectLicenseList.tsx (2)
backend/entities/license.go (1)
  • License (31-35)
frontend/wailsjs/go/models.ts (1)
  • License (444-459)
🔇 Additional comments (4)
frontend/src/components/SelectLicenseList.tsx (3)

37-39: Prop extension looks good

Optional matchedLicenses is a sensible addition; keeps the component flexible for pre-filtered results.


92-100: Conditional rendering for Matched list looks good

Only rendering the “Matched” group when there are results is clean and avoids empty headings.


30-30: No changes needed: entities namespace correctly exported in Wails models

Verified that frontend/wailsjs/go/models.ts defines

export namespace entities {  export class License {  } }

so import { entities } from '../../wailsjs/go/models' and usage of entities.License are valid.

frontend/src/components/ReplaceComponentDialog.tsx (1)

327-330: Good async error handling pattern

The async handling in onComponentSelect properly awaits the license fetch after component selection, ensuring the matched licenses are loaded. The error handling is delegated to the getMatchedLicenses function.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (3)
frontend/src/components/ReplaceComponentDialog.tsx (3)

176-184: Fix duplicate keyboard shortcut binding conflict.

There are two bindings for the same KEYBOARD_SHORTCUTS.confirm.keys which will both fire when popoverOpen is true, causing unintended behavior.

Remove the duplicate binding and consolidate into a single handler:

- const ref = useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => submitButtonRef.current?.click(), {
-   enableOnFormTags: true,
- });
-
- useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => handleSearchOnline(), {
-   enableOnFormTags: true,
-   enabled: popoverOpen,
- });
+ const ref = useKeyboardShortcut(
+   KEYBOARD_SHORTCUTS.confirm.keys,
+   () => {
+     if (popoverOpen) {
+       handleSearchOnline();
+     } else {
+       submitButtonRef.current?.click();
+     }
+   },
+   { enableOnFormTags: true }
+ );

330-333: Consider clearing the search value after online component selection.

After selecting a component from the online search, the searchValue state retains the old query which might be confusing when reopening the popover.

  onComponentSelect={async (c) => {
    onComponentSelected(c);
    await getMatchedLicenses(c.purl);
+   setSearchValue('');
  }}

235-235: Use platform-aware shortcut display helper.

The hardcoded "⌘ + Enter" won't display correctly on Windows/Linux where users expect "Ctrl + Enter".

Import and use the existing shortcut display helper:

+ import { getShortcutDisplay } from '@/utils/keyboardShortcuts';

  // Line 235
- <ShortcutBadge shortcut="⌘ + Enter" />
+ <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys)} />

  // Line 316
- Confirm <ShortcutBadge shortcut="⌘ + Enter" />
+ Confirm <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys)} />
#!/bin/bash
# Verify the existence of getShortcutDisplay helper
fd -t f "keyboardShortcuts" --exec grep -l "getShortcutDisplay"

# Check if there's already a utility for platform-aware shortcut display
rg "getShortcutDisplay|formatShortcut|displayShortcut" -A 3

Also applies to: 316-316

🧹 Nitpick comments (9)
frontend/src/components/Sidebar.tsx (1)

92-103: Consider consolidating navigation shortcuts configuration.

The keyboard navigation blocking is correctly implemented. However, the Enter key shortcut (line 100-103) could benefit from being added to the KEYBOARD_SHORTCUTS constant for consistency with other shortcuts.

Consider adding the Enter key shortcut to the centralized KEYBOARD_SHORTCUTS configuration:

// In frontend/src/lib/shortcuts.ts
export const KEYBOARD_SHORTCUTS: Record<entities.Action, KeyboardShortcut> = {
  // ... existing shortcuts ...
  
  // Navigation
  [entities.Action.MoveUp]: { /* ... */ },
  [entities.Action.MoveDown]: { /* ... */ },
+ [entities.Action.FocusResults]: {
+   name: 'Focus Results',
+   description: 'Move focus to results list',
+   keys: 'enter',
+ },

Then use it in Sidebar:

- useKeyboardShortcut('enter', moveFocusToResults, {
+ useKeyboardShortcut(KEYBOARD_SHORTCUTS.focusResults.keys, moveFocusToResults, {
frontend/src/components/ReplaceComponentDialog.tsx (2)

119-132: Consider handling the duplicate PURL case more gracefully.

The current implementation shows a toast when a duplicate PURL exists but doesn't prevent the dialog from closing. This might confuse users.

Consider keeping the dialog open when a duplicate is detected:

  const onComponentSelected = (component: entities.DeclaredComponent) => {
    const alreadyExists = declaredComponents.some((c) => c.purl === component.purl);
    if (alreadyExists) {
-     return toast({
+     toast({
        title: 'Warning',
        description: `A component with the same PURL already exists. Please select it from the list or enter a different PURL.`,
      });
+     // Don't close the new component dialog on duplicate
+     return;
    }
    setDeclaredComponents((prevState) => [...prevState, component]);
    form.setValue('purl', component.purl);
    form.setValue('name', component.name);
    resetLicense();
    setNewComponentDialogOpen(false);
  };

134-153: Add null check for component.licenses and improve error message.

The function handles the case where component or licenses might be missing, but could provide more specific error information.

  const getMatchedLicenses = async (purl: string) => {
    try {
      const { component } = await GetLicensesByPurl({ purl });

-     if (!component || !component.licenses) return;
+     if (!component || !component.licenses) {
+       setMatchedLicenses([]);
+       return;
+     }

      const matchedLicenses: entities.License[] = component.licenses.map((license) => ({
        name: license.full_name,
        licenseId: license.id,
        reference: `https://spdx.org/licenses/${license.id}.html`, // This is a workaround for the fact that the reference is not available in the response
      }));
      setMatchedLicenses(matchedLicenses);
    } catch (error) {
+     console.error('Failed to fetch licenses:', error);
      toast({
        title: 'Error',
-       description: 'Error fetching matched licenses',
+       description: `Failed to fetch licenses for ${purl}`,
        variant: 'destructive',
      });
    }
  };
frontend/src/components/ui/keyboard-table.tsx (1)

59-59: Use Number.MAX_SAFE_INTEGER instead of magic number.

Using a hardcoded large number is less clear than using the appropriate constant.

-       move(99999);
+       move(Number.MAX_SAFE_INTEGER);
frontend/src/components/OnlineComponentSearchDialog.tsx (5)

131-142: Fix effect dependencies and avoid stale closures in toast

Compute the message inside the effect and include toast in deps to satisfy exhaustive-deps and avoid stale references.

Apply:

-  useEffect(() => {
-    if (error) {
-      console.error('Error searching components:', error);
-      toast({
-        title: 'Search Error',
-        description: errorMessage + '. Please try again.',
-        variant: 'destructive',
-      });
-    }
-  }, [error]);
+  useEffect(() => {
+    if (error) {
+      const message = error instanceof Error ? error.message : 'Failed to search for components online';
+      console.error('Error searching components:', error);
+      toast({
+        title: 'Search Error',
+        description: message + '. Please try again.',
+        variant: 'destructive',
+      });
+    }
+  }, [error, toast]);

145-146: Verify dialog control strategy (open is hardcoded true)

Dialog is always open while mounted. Ensure the parent unmounts this component on onOpenChange(false) (Escape/overlay click), otherwise the dialog won’t close. If you want controlled behavior, expose an open prop and bind it to Dialog.

Example (requires API changes elsewhere):

-interface OnlineComponentSearchDialogProps {
-  onOpenChange: (open: boolean) => void;
-  searchTerm: string;
-  onComponentSelect: (component: entities.DeclaredComponent) => void;
-}
+interface OnlineComponentSearchDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  searchTerm: string;
+  onComponentSelect: (component: entities.DeclaredComponent) => void;
+}
@@
-export default function OnlineComponentSearchDialog({ onOpenChange, searchTerm, onComponentSelect }: OnlineComponentSearchDialogProps) {
+export default function OnlineComponentSearchDialog({ open, onOpenChange, searchTerm, onComponentSelect }: OnlineComponentSearchDialogProps) {
@@
-    <Dialog open onOpenChange={onOpenChange}>
+    <Dialog open={open} onOpenChange={onOpenChange}>

186-191: Show the actually queried term in the loading message

Using currentSearch may mismatch the in-flight query (which uses debouncedSearchTerm).

Apply:

-              <span className="ml-2 text-sm text-muted-foreground">Searching for &ldquo;{currentSearch}&rdquo;...</span>
+              <span className="ml-2 text-sm text-muted-foreground">Searching for &ldquo;{debouncedSearchTerm}&rdquo;...</span>

43-82: Centralize/validate catalogPackages against backend

The package list is hardcoded and may drift from what the backend/Scanoss supports. Consider:

  • Sourcing the list from the backend (endpoint or bindings).
  • Or centralizing it in a shared constants module used across components.

If you prefer a quick win, extract to a shared constants file:

// src/constants/catalogPackages.ts
export const CATALOG_PACKAGES = [/* current list */] as const;
export type CatalogPackage = typeof CATALOG_PACKAGES[number];

Then import here and type selectedPackage as CatalogPackage to prevent invalid values at compile time.


100-115: Consider returning previous data during refetch to reduce UI flicker

Optional: keep prior search results visible while a new query is in flight to avoid empty states during fast typing.

If you’re on TanStack Query v4:

-  } = useQuery<entities.SearchedComponent[], Error>({
+  } = useQuery<entities.SearchedComponent[], Error>({
@@
-    retry: false,
+    retry: false,
+    keepPreviousData: true,
   });

If on v5, use placeholderData: (previous) => previous instead.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be54b88 and 8b95d89.

📒 Files selected for processing (6)
  • frontend/src/components/OnlineComponentSearchDialog.tsx (1 hunks)
  • frontend/src/components/ReplaceComponentDialog.tsx (10 hunks)
  • frontend/src/components/Sidebar.tsx (3 hunks)
  • frontend/src/components/ui/keyboard-table.tsx (1 hunks)
  • frontend/src/contexts/DialogStateContext.tsx (1 hunks)
  • frontend/src/main.tsx (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
frontend/src/main.tsx (3)
frontend/src/contexts/DialogStateContext.tsx (1)
  • DialogStateProvider (39-75)
frontend/src/providers/DialogProvider.tsx (1)
  • DialogProvider (46-122)
frontend/src/components/ui/toaster.tsx (1)
  • Toaster (11-33)
frontend/src/components/Sidebar.tsx (3)
frontend/src/contexts/DialogStateContext.tsx (1)
  • useDialogState (77-83)
frontend/src/hooks/useKeyboardShortcut.tsx (1)
  • useKeyboardShortcut (31-52)
frontend/src/lib/shortcuts.ts (1)
  • KEYBOARD_SHORTCUTS (32-164)
frontend/src/components/ui/keyboard-table.tsx (1)
frontend/src/components/ui/table.tsx (6)
  • Table (112-112)
  • TableHeader (113-113)
  • TableRow (117-117)
  • TableHead (116-116)
  • TableBody (114-114)
  • TableCell (118-118)
frontend/src/components/ReplaceComponentDialog.tsx (8)
frontend/wailsjs/go/models.ts (2)
  • DeclaredComponent (332-345)
  • License (444-459)
frontend/src/contexts/DialogStateContext.tsx (1)
  • useDialogRegistration (85-94)
frontend/wailsjs/go/service/LicenseServiceImpl.js (1)
  • GetLicensesByPurl (9-11)
frontend/src/lib/shortcuts.ts (1)
  • KEYBOARD_SHORTCUTS (32-164)
frontend/src/components/ui/command.tsx (2)
  • CommandInput (146-146)
  • CommandItem (150-150)
frontend/src/components/ShortcutBadge.tsx (1)
  • ShortcutBadge (1-3)
frontend/src/components/NewComponentDialog.tsx (1)
  • NewComponentDialog (46-107)
frontend/src/components/OnlineComponentSearchDialog.tsx (1)
  • OnlineComponentSearchDialog (92-233)
frontend/src/components/OnlineComponentSearchDialog.tsx (11)
frontend/wailsjs/go/models.ts (3)
  • DeclaredComponent (332-345)
  • ComponentSearchRequest (248-269)
  • SearchedComponent (284-299)
frontend/src/components/ui/use-toast.ts (2)
  • useToast (192-192)
  • toast (192-192)
frontend/src/contexts/DialogStateContext.tsx (1)
  • useDialogRegistration (85-94)
backend/entities/component_search.go (2)
  • ComponentSearchRequest (27-34)
  • SearchedComponent (37-41)
frontend/wailsjs/go/service/ComponentServiceImpl.js (1)
  • SearchComponents (37-39)
frontend/src/components/ui/dialog.tsx (4)
  • Dialog (77-77)
  • DialogContent (77-77)
  • DialogHeader (77-77)
  • DialogTitle (77-77)
frontend/src/components/ui/label.tsx (1)
  • Label (24-24)
frontend/src/components/ui/input.tsx (1)
  • Input (25-25)
frontend/src/components/ui/select.tsx (6)
  • Select (152-152)
  • SelectTrigger (155-155)
  • SelectValue (154-154)
  • SelectContent (156-156)
  • SelectGroup (153-153)
  • SelectItem (158-158)
frontend/src/components/ui/scroll-area.tsx (1)
  • ScrollArea (46-46)
frontend/src/components/ui/keyboard-table.tsx (1)
  • KeyboardTable (21-124)
🔇 Additional comments (7)
frontend/src/contexts/DialogStateContext.tsx (1)

1-94: LGTM! Well-implemented dialog state management context.

The implementation correctly manages dialog state with keyboard navigation blocking. The use of Map for tracking multiple dialogs and the cleanup via useEffect in the registration hook ensures proper lifecycle management.

frontend/src/main.tsx (1)

57-62: LGTM! Correct provider nesting order.

The DialogStateProvider is correctly placed as the outermost provider wrapping DialogProvider, which allows DialogProvider and its child dialogs to use the dialog state context for keyboard navigation blocking.

frontend/src/components/Sidebar.tsx (1)

31-31: LGTM! Good integration with dialog state context.

The component correctly imports and uses the dialog state context to control keyboard navigation.

Also applies to: 50-50

frontend/src/components/ReplaceComponentDialog.tsx (2)

83-83: LGTM! Proper dialog registration for keyboard navigation blocking.

The dialog correctly registers itself with keyboard navigation blocking enabled.


228-238: LGTM! Good UX improvement with inline search action.

The inline search functionality provides a smooth user experience by allowing direct online search from the input field.

frontend/src/components/ui/keyboard-table.tsx (1)

21-28: LGTM! Well-structured generic table component with keyboard navigation.

The component provides excellent keyboard navigation support with proper TypeScript generics and flexible column rendering.

frontend/src/components/OnlineComponentSearchDialog.tsx (1)

122-129: Mapping SearchedComponent to DeclaredComponent looks good

The structural mapping (component -> name, purl passthrough) is correct and aligns with models.ts definitions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🔭 Outside diff range comments (9)
.github/workflows/test.yml (1)

36-41: Broken env var usage when passing APPARGS to wails build

Shell syntax is wrong: APPARGS must be set in the environment before the command, not after it. As written, APPARGS becomes a CLI argument to wails, which will be ignored or cause errors.

Fix:

-          wails build APPARGS="--key test"
+          APPARGS="--key test" wails build

Alternatively:

export APPARGS="--key test"
wails build
.github/workflows/release.yml (7)

168-176: APP_VERSION is undefined in this step (commit message becomes blank version)

The variable is computed in the previous step only. Each GitHub Actions step runs in a new process; APP_VERSION won’t persist here.

Apply this diff to recompute APP_VERSION before using it:

       cd homebrew-dist
       git config user.name "SCANOSS Bot"
       git config user.email "[email protected]"
+      APP_VERSION=$(git tag --sort=-version:refname | head -n 1)
       git add Casks/scanoss-code-compare.rb
       git commit -m "Update scanoss-code-compare to ${APP_VERSION}"
       git push

25-27: Ensure tags are available for git tag lookups (AppVersion ldflags, Homebrew bump)

checkout@v4 defaults to shallow clones without tags; git tag may return nothing on runners. Fetch tags explicitly.

Apply this diff:

       - name: Checkout
         uses: actions/checkout@v4
         with:
           submodules: recursive
+          fetch-depth: 0
+          fetch-tags: true

Repeat for Linux and Windows jobs (see corresponding comments).


189-192: Linux job: fetch tags for version embedding

Same rationale as macOS job: wails build reads AppVersion from git tags.

       - name: Checkout
         uses: actions/checkout@v4
         with:
           submodules: recursive
+          fetch-depth: 0
+          fetch-tags: true

255-258: Windows job: fetch tags for version embedding

Ensure tags are present before calling git tag in the build step.

       - name: Checkout
         uses: actions/checkout@v4
         with:
           submodules: recursive
+          fetch-depth: 0
+          fetch-tags: true

208-211: Pin Wails version on Linux for reproducible builds (avoid @latest drift)

Using @latest can break unexpectedly, especially after a Go toolchain bump. Align with macOS’ pinned version or centralize in env.

-      - name: Install Wails
-        run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
+      - name: Install Wails
+        run: go install github.com/wailsapp/wails/v2/cmd/[email protected]
         shell: bash

If macOS version changes, keep all three jobs in sync.


275-277: Pin Wails version on Windows for reproducible builds

Mirror the Linux/macOS recommendation.

-      - name: Install Wails
-        run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
+      - name: Install Wails
+        run: go install github.com/wailsapp/wails/v2/cmd/[email protected]
         shell: bash

317-317: Pin CodeSignTool action to a stable tag
Based on the repository’s tags, the latest stable release is v1.3.1. Pinning to this exact tag ensures reproducibility and supply-chain integrity.

• File: .github/workflows/release.yml (line 317)

-        uses: sslcom/esigner-codesign@develop
+        uses: sslcom/[email protected]
frontend/src/components/ReplaceComponentDialog.tsx (1)

220-230: Use semantic buttons instead of clickable divs inside CommandItem for a11y.

Clickable divs are not focusable and hurt keyboard/screen reader accessibility. Since CommandItem supports asChild, render buttons.

Apply this diff:

-                              <CommandItem asChild>
-                                <div
-                                  onClick={() => {
-                                    setNewComponentDialogOpen(true);
-                                    setPopoverOpen(false);
-                                  }}
-                                >
-                                  <Plus className="mr-2 h-4 w-4" />
-                                  Add new component
-                                </div>
-                              </CommandItem>
+                              <CommandItem asChild>
+                                <button
+                                  type="button"
+                                  onClick={() => {
+                                    setNewComponentDialogOpen(true);
+                                    setPopoverOpen(false);
+                                  }}
+                                  className="flex w-full items-center gap-2 text-left"
+                                  aria-label="Add new component"
+                                >
+                                  <Plus className="mr-2 h-4 w-4" />
+                                  Add new component
+                                </button>
+                              </CommandItem>
-                              {searchValue ? (
-                                <CommandItem asChild>
-                                  <div onClick={handleSearchOnline} className="flex items-center justify-between gap-2">
-                                    <div className="flex items-center gap-2">
-                                      <Search className="h-3 w-3" />
-                                      Search &ldquo;{searchValue}&rdquo; online
-                                    </div>
-                                    <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys, modifierKey.label)[0]} />
-                                  </div>
-                                </CommandItem>
-                              ) : null}
+                              {searchValue ? (
+                                <CommandItem asChild>
+                                  <button
+                                    type="button"
+                                    onClick={handleSearchOnline}
+                                    className="flex w-full items-center justify-between gap-2"
+                                    aria-label={`Search "${searchValue}" online`}
+                                  >
+                                    <div className="flex items-center gap-2">
+                                      <Search className="h-3 w-3" />
+                                      Search &ldquo;{searchValue}&rdquo; online
+                                    </div>
+                                    <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys, modifierKey.label)[0]} />
+                                  </button>
+                                </CommandItem>
+                              ) : null}

Also applies to: 231-241

♻️ Duplicate comments (2)
frontend/src/components/ReplaceComponentDialog.tsx (2)

179-187: Fix duplicate confirm shortcut binding (both handlers fire when popover is open).

You bind KEYBOARD_SHORTCUTS.confirm twice; the submit handler is always enabled, and the second handler is enabled when popoverOpen is true. Result: pressing the shortcut while the popover is open can both submit and open online search.

Unify into a single binding that branches on popoverOpen and remove the second hook.

Apply this diff:

-  const ref = useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => submitButtonRef.current?.click(), {
-    enableOnFormTags: true,
-  });
-
-  useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => handleSearchOnline(), {
-    enableOnFormTags: true,
-    enabled: popoverOpen,
-  });
+  const ref = useKeyboardShortcut(
+    KEYBOARD_SHORTCUTS.confirm.keys,
+    () => {
+      if (popoverOpen) handleSearchOnline();
+      else submitButtonRef.current?.click();
+    },
+    { enableOnFormTags: true }
+  );

122-135: Clear inline search input after selection and refresh license matches for existing components.

The search input retains stale text after selecting a component. Also, license matches are fetched only for online selections; selecting an existing component can show stale matches. Clear the input and refresh matches on all selection paths.

Apply this diff:

   const onComponentSelected = (component: entities.DeclaredComponent) => {
     const alreadyExists = declaredComponents.some((c) => c.purl === component.purl);
     if (alreadyExists) {
       return toast({
         title: 'Warning',
         description: `A component with the same PURL already exists. Please select it from the list or enter a different PURL.`,
       });
     }
     setDeclaredComponents((prevState) => [...prevState, component]);
     form.setValue('purl', component.purl);
     form.setValue('name', component.name);
     resetLicense();
     setNewComponentDialogOpen(false);
+    setSearchValue('');
   };
                                   onSelect={() => {
                                     form.setValue('purl', component.purl);
                                     form.setValue('name', component.name);
                                     resetLicense();
                                     setPopoverOpen(false);
+                                    setSearchValue('');
+                                    // Refresh matched licenses for existing components too
+                                    void getMatchedLicenses(component.purl);
                                   }}
           onComponentSelect={async (c) => {
             onComponentSelected(c);
             await getMatchedLicenses(c.purl);
+            setSearchValue('');
           }}

Also applies to: 249-254, 333-336

🧹 Nitpick comments (3)
.github/workflows/test.yml (2)

14-14: GOLANG_VERSION env is unused; either wire it into setup-go or remove it

You’re using actions/setup-go with go-version-file, so this env var doesn’t affect the toolchain. Keeping it risks drift/confusion.

Apply one of the following:

Option A — remove the unused env:

-  GOLANG_VERSION: "1.23"

Option B — use it explicitly (then drop go-version-file):

# Outside this range: in both setup-go steps, replace:
#   with:
#     go-version-file: go.mod
# with:
with:
  go-version: ${{ env.GOLANG_VERSION }}

Please verify go.mod also targets 1.23 to avoid mismatches.


98-102: LGTM; consider aligning action versions in this job for consistency

Integration tests look fine, SCANOSS_API_KEY is correctly sourced. Minor consistency nit: elsewhere you use actions/checkout@v4 and actions/setup-go@v5; here you’re on @V3 and @v4 respectively. Consider upgrading for uniformity and latest fixes.

Suggested updates outside this range:

- uses: actions/checkout@v3
+ uses: actions/checkout@v4

- uses: actions/setup-go@v4
+ uses: actions/setup-go@v5

- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
.github/workflows/release.yml (1)

134-135: Use xcrun for stapler validate for consistency

You already use xcrun stapler staple; do the same for validate to avoid PATH issues on macOS runners.

-          stapler validate "$DMG_PATH"
+          xcrun stapler validate "$DMG_PATH"
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b95d89 and 2cad7ee.

📒 Files selected for processing (4)
  • .github/workflows/release.yml (1 hunks)
  • .github/workflows/test.yml (3 hunks)
  • frontend/src/components/OnlineComponentSearchDialog.tsx (1 hunks)
  • frontend/src/components/ReplaceComponentDialog.tsx (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/OnlineComponentSearchDialog.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
frontend/src/components/ReplaceComponentDialog.tsx (13)
frontend/wailsjs/go/models.ts (2)
  • DeclaredComponent (332-345)
  • License (444-459)
frontend/src/hooks/useEnvironment.tsx (1)
  • useEnvironment (37-66)
frontend/src/contexts/DialogStateContext.tsx (1)
  • useDialogRegistration (85-94)
frontend/wailsjs/go/service/LicenseServiceImpl.js (1)
  • GetLicensesByPurl (9-11)
frontend/src/lib/shortcuts.ts (1)
  • getShortcutDisplay (166-180)
frontend/src/components/ui/popover.tsx (1)
  • Popover (31-31)
frontend/src/components/ui/command.tsx (2)
  • CommandInput (146-146)
  • CommandItem (150-150)
frontend/src/components/ShortcutBadge.tsx (1)
  • ShortcutBadge (1-3)
frontend/src/components/SelectLicenseList.tsx (1)
  • SelectLicenseList (53-113)
frontend/src/components/ui/dialog.tsx (3)
  • DialogFooter (77-77)
  • DialogContent (77-77)
  • Dialog (77-77)
frontend/src/components/ui/form.tsx (1)
  • Form (169-169)
frontend/src/components/NewComponentDialog.tsx (1)
  • NewComponentDialog (46-107)
frontend/src/components/OnlineComponentSearchDialog.tsx (1)
  • OnlineComponentSearchDialog (91-234)
🔇 Additional comments (5)
.github/workflows/release.yml (1)

15-15: Go toolchain bump to 1.23: validate cross-job compatibility

Actions/setup-go@v5 supports 1.23, but please ensure:

  • go.mod declares go 1.23 (or toolchain go1.23).
  • Wails version(s) used build cleanly on 1.23 (see related suggestions below).
frontend/src/components/ReplaceComponentDialog.tsx (4)

33-34: Good: platform-aware shortcut labels via centralized helper.

Importing getShortcutDisplay and using modifierKey from useEnvironment to render the correct confirm shortcut is clean and consistent with the rest of the app.

Also applies to: 84-85, 238-239, 319-319


205-205: Good: Popover uses modal for proper focus management.

Enabling modal improves keyboard and focus trapping inside the combobox popover.


122-122: Type alignment with search dialog looks correct.

Using entities.DeclaredComponent for onComponentSelected matches OnlineComponentSearchDialog’s onComponentSelect contract.


283-284: LGTM: matchedLicenses plumbed into SelectLicenseList.

Prop wiring aligns with the new “Matched” section in the license picker.

@matiasdaloia matiasdaloia force-pushed the feature/mdaloia/sp-2882-add-component-search branch from 2cad7ee to 7901638 Compare September 24, 2025 13:50
@github-actions
Copy link

SCANOSS SCAN Completed 🚀

  • Detected components: 8
  • Undeclared components: 3
  • Declared components: 5
  • Detected files: 179
  • Detected files undeclared: 3
  • Detected files declared: 176
  • Licenses detected: 5
  • Licenses detected with copyleft: 2
  • Policies: ❌ 1 fail (1 total)

View more details on SCANOSS Action Summary

@github-actions
Copy link

SCANOSS SCAN Completed 🚀

  • Detected components: 8
  • Undeclared components: 3
  • Declared components: 5
  • Detected files: 179
  • Detected files undeclared: 3
  • Detected files declared: 176
  • Licenses detected: 5
  • Licenses detected with copyleft: 2
  • Policies: ❌ 1 fail (1 total)

View more details on SCANOSS Action Summary

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (23)
internal/utils/url_utils.go (4)

12-18: Handle scheme case-insensitively (RFC 3986).

URL schemes are case-insensitive; “HTTP://example.com” won’t be stripped currently.

Apply this diff:

-	if after, ok := strings.CutPrefix(url, "http://"); ok {
-		return after
-	}
-	if after, ok := strings.CutPrefix(url, "https://"); ok {
-		return after
-	}
+	lu := strings.ToLower(url)
+	if strings.HasPrefix(lu, "http://") {
+		return url[len("http://"):]
+	}
+	if strings.HasPrefix(lu, "https://") {
+		return url[len("https://"):]
+	}

5-7: Name vs. behavior mismatch (optional).

Function name suggests removing any URL scheme; implementation/docs only handle HTTP/HTTPS. Either:

  • keep name and generalize to strip any scheme ending with "://", or
  • keep behavior and rename (e.g., RemoveHTTPSchemeFromURL) for precision.

Given the PR scope, consider deferring to a follow-up.


8-11: Minor: early-return is fine but not required.

CutPrefix/HasPrefix guards already cover empty input. Keeping the early return is okay for clarity; can be removed to reduce branching if preferred.


5-19: Add table-driven tests for edge cases.

Recommend tests covering: empty string, http, https, uppercase scheme variants, no scheme, and path/query fragments.

Example (utils/url_utils_test.go):

package utils

import "testing"

func TestRemoveProtocolFromURL(t *testing.T) {
  cases := []struct{
    in, out string
  }{
    {"", ""},
    {"http://example.com", "example.com"},
    {"https://example.com/path?x=1#frag", "example.com/path?x=1#frag"},
    {"HTTP://EXAMPLE.COM", "EXAMPLE.COM"},
    {"Https://Mixed.Case/Path", "Mixed.Case/Path"},
    {"ftp://example.com", "ftp://example.com"},
    {"example.com", "example.com"},
  }
  for _, c := range cases {
    if got := RemoveProtocolFromURL(c.in); got != c.out {
      t.Fatalf("input %q => got %q, want %q", c.in, got, c.out)
    }
  }
}

Happy to commit these in this PR if you want.

backend/service/scanoss_api_service_http_impl.go (4)

127-133: Optional: include package/offset in debug logs

Helps troubleshoot query shape.

 	log.Debug().
 		Str("search", request.Search).
 		Str("vendor", request.Vendor).
 		Str("component", request.Component).
+		Str("package", request.Package).
+		Int32("offset", request.Offset).
 		Int32("limit", request.Limit).
 		Msg("Searching components via SCANOSS API")

171-172: Fix doc comment to match method name

Minor naming mismatch.

-// GetLicenseByPurl fetches licenses for a given purl/component combination using the SCANOSS HTTP API
+// GetLicensesByPurl fetches licenses for a given purl using the SCANOSS HTTP API

190-194: Fix log message: wrong API name in GetLicensesByPurl error path

Message says “component search API”.

-	log.Error().Err(err).Msg("Error calling SCANOSS component search API")
+	log.Error().Err(err).Msg("Error calling SCANOSS licenses API")

80-96: Optional: robust URL joining

Avoid double slashes by using url.JoinPath if using Go >=1.20; otherwise trim slashes.

Example:

-	u, err := url.Parse(s.baseURL + endpoint)
+	uStr, err := url.JoinPath(s.baseURL, endpoint)
+	if err != nil {
+		return "", err
+	}
+	u, err := url.Parse(uStr)
backend/service/license_service_impl.go (1)

58-60: Defensive: guard against nil scanossApiService

Prevent runtime panic if dependency wiring fails.

 func (s *LicenseServiceImpl) GetLicensesByPurl(request entities.ComponentRequest) (entities.GetLicensesByPurlResponse, error) {
-	return s.scanossApiService.GetLicensesByPurl(request)
+	if s.scanossApiService == nil {
+		return entities.GetLicensesByPurlResponse{}, fmt.Errorf("scanossApiService not configured")
+	}
+	return s.scanossApiService.GetLicensesByPurl(request)
 }

Note: add fmt import.

backend/service/scanoss_api_service.go (1)

32-36: Design: prefer per-call context over stored state

Consider passing context.Context as a parameter to each method instead of SetContext to avoid shared mutable state and ease cancellation per call.

frontend/src/components/ui/keyboard-table.tsx (5)

29-33: Clamp focusedIndex on init and when rows change

Prevents invalid aria references and out-of-range indices when data updates.

   const containerRef = useRef<HTMLDivElement>(null);
-  const [focusedIndex, setFocusedIndex] = useState<number>(initialFocusedIndex);
+  const [focusedIndex, setFocusedIndex] = useState<number>(() =>
+    rows.length ? Math.max(0, Math.min(initialFocusedIndex, rows.length - 1)) : -1
+  );
+
+  useEffect(() => {
+    setFocusedIndex((prev) => {
+      const last = rows.length - 1;
+      if (last < 0) return -1;
+      const next = prev < 0 ? 0 : prev;
+      return Math.max(0, Math.min(next, last));
+    });
+  }, [rows.length]);

32-37: Use data length instead of querying the DOM

Simpler and avoids layout thrash from DOM queries.

   const move = (next: number) => {
-    const count = containerRef.current?.querySelectorAll<HTMLElement>('[data-row-index]').length ?? 0;
+    const count = rows.length;
     if (!count) return;
     const last = count - 1;
     setFocusedIndex(Math.max(0, Math.min(last, next)));
   };

87-87: Avoid mixing ARIA grid roles with native table semantics

The children are native table elements (tr/td). Using role="grid" + aria-activedescendant with table semantics is inconsistent. Prefer native table semantics here.

-    <div ref={containerRef} tabIndex={0} role="grid" aria-activedescendant={`row-${focusedIndex}`} onKeyDown={onKeyDown}>
+    <div ref={containerRef} tabIndex={0} onKeyDown={onKeyDown}>

If you want full grid semantics instead, cells/headers should use grid roles (gridcell/row/columnheader) across the Table primitives.


103-111: Expose selection state to assistive tech

Mark the focused row as selected for better SR feedback.

                 data-row-index={i}
+                aria-selected={isFocused}

99-101: Row styling nits: ring/rounded on and className override

  • rounded/ring utilities often don’t render on tr; consider moving focus styles to cells (e.g., apply to first/last cell) or use outline on the row wrapper.
  • Consider merging default and custom row class names to preserve focus styling.
-            const customClassName = rowClassName ? rowClassName(row, i, isFocused) : defaultClassName;
+            const customClassName = clsx(defaultClassName, rowClassName?.(row, i, isFocused));
frontend/src/components/SelectLicenseList.tsx (4)

61-66: Fix stale onSelect reference in callback deps

Include onSelect in the dependency array to avoid stale closures.

-  const handleSelect = useCallback((value: string) => {
+  const handleSelect = useCallback((value: string) => {
     setValue(value);
     onSelect(value);
     setPopoverOpen(false);
-  }, []);
+  }, [onSelect]);

67-70: Fix stale onSelect reference in callback deps

Same here: add onSelect to dependencies.

-  const handleClearSelection = useCallback(() => {
+  const handleClearSelection = useCallback(() => {
     setValue(undefined);
     onSelect(undefined);
-  }, []);
+  }, [onSelect]);

82-84: Add accessible name to Clear icon button

Improve a11y with an aria-label.

-          <Button size="icon" variant="ghost" onClick={handleClearSelection}>
+          <Button size="icon" variant="ghost" onClick={handleClearSelection} aria-label="Clear selection">
             <X className="h-4 w-4" />
           </Button>

101-106: De-duplicate matched items from the catalogued list

Avoid showing the same license twice if it appears in matchedLicenses.

-              {licenses?.map((license) => (
+              {licenses
+                ?.filter((license) => !matchedLicenses?.some((m) => m.licenseId === license.licenseId))
+                .map((license) => (
                 <CommandItem value={license.licenseId} key={license.licenseId} onSelect={handleSelect}>
                   <LicenseItem license={license} value={value || ''} />
                 </CommandItem>
-              ))}
+              ))}
frontend/src/components/ReplaceComponentDialog.tsx (4)

221-229: Use a semantic button for “Add new component” (a11y)

Clickable divs aren’t accessible. Use a button.

-                              <CommandItem asChild>
-                                <div
-                                  onClick={() => {
-                                    setNewComponentDialogOpen(true);
-                                    setPopoverOpen(false);
-                                  }}
-                                >
-                                  <Plus className="mr-2 h-4 w-4" />
-                                  Add new component
-                                </div>
-                              </CommandItem>
+                              <CommandItem asChild>
+                                <button
+                                  type="button"
+                                  className="flex w-full items-center"
+                                  onClick={() => {
+                                    setNewComponentDialogOpen(true);
+                                    setPopoverOpen(false);
+                                  }}
+                                >
+                                  <Plus className="mr-2 h-4 w-4" />
+                                  Add new component
+                                </button>
+                              </CommandItem>

122-135: Clear matchedLicenses when selecting a declared component

Avoid carrying matches from a previous selection.

     form.setValue('name', component.name);
     resetLicense();
+    setMatchedLicenses([]);
     setNewComponentDialogOpen(false);

249-254: Also clear matches when picking from the list

Ensure matchedLicenses doesn’t reflect a prior selection.

                                       form.setValue('name', component.name);
                                       resetLicense();
+                                      setMatchedLicenses([]);
                                       setPopoverOpen(false);

334-336: Optional: clear the inline search input after online selection

Prevents stale text when the popover is reopened.

             onComponentSelect={async (c) => {
               onComponentSelected(c);
               await getMatchedLicenses(c.purl);
+              setSearchValue('');
             }}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cad7ee and 7901638.

⛔ Files ignored due to path filters (2)
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (34)
  • .github/workflows/release.yml (1 hunks)
  • .github/workflows/test.yml (3 hunks)
  • backend/entities/common.go (1 hunks)
  • backend/entities/component_search.go (1 hunks)
  • backend/entities/license.go (1 hunks)
  • backend/service/component_service.go (2 hunks)
  • backend/service/component_service_impl.go (2 hunks)
  • backend/service/component_service_test.go (0 hunks)
  • backend/service/license_service.go (1 hunks)
  • backend/service/license_service_impl.go (2 hunks)
  • backend/service/scanoss_api_service.go (1 hunks)
  • backend/service/scanoss_api_service_http_impl.go (1 hunks)
  • frontend/package.json (1 hunks)
  • frontend/package.json.md5 (1 hunks)
  • frontend/src/components/OnlineComponentSearchDialog.tsx (1 hunks)
  • frontend/src/components/ReplaceComponentDialog.tsx (10 hunks)
  • frontend/src/components/SelectLicenseList.tsx (3 hunks)
  • frontend/src/components/ShortcutBadge.tsx (1 hunks)
  • frontend/src/components/Sidebar.tsx (3 hunks)
  • frontend/src/components/ui/dialog.tsx (0 hunks)
  • frontend/src/components/ui/keyboard-table.tsx (1 hunks)
  • frontend/src/components/ui/popover.tsx (2 hunks)
  • frontend/src/components/ui/select.tsx (1 hunks)
  • frontend/src/components/ui/table.tsx (1 hunks)
  • frontend/src/contexts/DialogStateContext.tsx (1 hunks)
  • frontend/src/main.tsx (2 hunks)
  • frontend/wailsjs/go/models.ts (5 hunks)
  • frontend/wailsjs/go/service/ComponentServiceImpl.d.ts (1 hunks)
  • frontend/wailsjs/go/service/ComponentServiceImpl.js (1 hunks)
  • frontend/wailsjs/go/service/LicenseServiceImpl.d.ts (1 hunks)
  • frontend/wailsjs/go/service/LicenseServiceImpl.js (1 hunks)
  • go.mod (2 hunks)
  • internal/utils/url_utils.go (1 hunks)
  • main.go (2 hunks)
💤 Files with no reviewable changes (2)
  • backend/service/component_service_test.go
  • frontend/src/components/ui/dialog.tsx
✅ Files skipped from review due to trivial changes (1)
  • frontend/package.json.md5
🚧 Files skipped from review as they are similar to previous changes (23)
  • backend/service/license_service.go
  • backend/entities/common.go
  • frontend/src/components/OnlineComponentSearchDialog.tsx
  • frontend/wailsjs/go/service/ComponentServiceImpl.js
  • backend/entities/license.go
  • frontend/src/components/Sidebar.tsx
  • frontend/src/components/ui/table.tsx
  • frontend/src/main.tsx
  • .github/workflows/test.yml
  • backend/entities/component_search.go
  • frontend/wailsjs/go/service/LicenseServiceImpl.d.ts
  • backend/service/component_service_impl.go
  • frontend/src/components/ShortcutBadge.tsx
  • frontend/wailsjs/go/models.ts
  • frontend/src/contexts/DialogStateContext.tsx
  • frontend/src/components/ui/popover.tsx
  • frontend/wailsjs/go/service/LicenseServiceImpl.js
  • go.mod
  • main.go
  • frontend/package.json
  • frontend/src/components/ui/select.tsx
  • .github/workflows/release.yml
  • frontend/wailsjs/go/service/ComponentServiceImpl.d.ts
🧰 Additional context used
🧬 Code graph analysis (7)
backend/service/scanoss_api_service.go (3)
backend/entities/component_search.go (2)
  • ComponentSearchRequest (27-34)
  • ComponentSearchResponse (44-47)
backend/entities/common.go (1)
  • ComponentRequest (3-6)
backend/entities/license.go (1)
  • GetLicensesByPurlResponse (50-53)
backend/service/component_service.go (3)
frontend/wailsjs/go/service/ComponentServiceImpl.js (1)
  • SearchComponents (37-39)
frontend/wailsjs/go/models.ts (2)
  • ComponentSearchRequest (248-269)
  • ComponentSearchResponse (300-331)
backend/entities/component_search.go (2)
  • ComponentSearchRequest (27-34)
  • ComponentSearchResponse (44-47)
frontend/src/components/SelectLicenseList.tsx (2)
backend/entities/license.go (1)
  • License (31-35)
frontend/wailsjs/go/models.ts (1)
  • License (444-459)
backend/service/license_service_impl.go (6)
backend/service/scanoss_api_service.go (1)
  • ScanossApiService (32-36)
backend/service/license_service.go (1)
  • LicenseService (28-31)
frontend/wailsjs/go/service/LicenseServiceImpl.js (1)
  • GetLicensesByPurl (9-11)
frontend/wailsjs/go/models.ts (2)
  • ComponentRequest (234-247)
  • GetLicensesByPurlResponse (378-409)
backend/entities/common.go (1)
  • ComponentRequest (3-6)
backend/entities/license.go (1)
  • GetLicensesByPurlResponse (50-53)
frontend/src/components/ReplaceComponentDialog.tsx (9)
frontend/wailsjs/go/models.ts (2)
  • DeclaredComponent (332-345)
  • License (444-459)
frontend/src/hooks/useEnvironment.tsx (1)
  • useEnvironment (37-66)
frontend/src/contexts/DialogStateContext.tsx (1)
  • useDialogRegistration (85-94)
frontend/wailsjs/go/service/LicenseServiceImpl.js (1)
  • GetLicensesByPurl (9-11)
frontend/src/hooks/useKeyboardShortcut.tsx (1)
  • useKeyboardShortcut (31-52)
frontend/src/lib/shortcuts.ts (1)
  • getShortcutDisplay (166-180)
frontend/src/components/ShortcutBadge.tsx (1)
  • ShortcutBadge (1-3)
frontend/src/components/SelectLicenseList.tsx (1)
  • SelectLicenseList (53-113)
frontend/src/components/OnlineComponentSearchDialog.tsx (1)
  • OnlineComponentSearchDialog (91-234)
backend/service/scanoss_api_service_http_impl.go (6)
backend/service/scanoss_api_service.go (1)
  • ScanossApiService (32-36)
internal/config/config.go (2)
  • GetInstance (111-116)
  • SCANOSS_PREMIUM_API_URL (55-55)
backend/entities/component_search.go (2)
  • ComponentSearchRequest (27-34)
  • ComponentSearchResponse (44-47)
internal/utils/custom_validations.go (1)
  • GetValidator (37-39)
backend/entities/common.go (1)
  • ComponentRequest (3-6)
backend/entities/license.go (1)
  • GetLicensesByPurlResponse (50-53)
frontend/src/components/ui/keyboard-table.tsx (1)
frontend/src/components/ui/table.tsx (6)
  • Table (112-112)
  • TableHeader (113-113)
  • TableRow (117-117)
  • TableHead (116-116)
  • TableBody (114-114)
  • TableCell (118-118)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (10)
internal/utils/url_utils.go (1)

5-19: Approved — strings.CutPrefix is safe (repo targets Go >= 1.23)
go.mod declares "go 1.23.0" and "toolchain go1.23.2", so strings.CutPrefix (added in Go 1.20) is supported; no change required.

backend/service/license_service_impl.go (1)

38-42: New constructor parameter already propagated — no action required

Call site main.go:101 passes scanossApiService; no other usages of NewLicenseServiceImpl were found.

backend/service/scanoss_api_service_http_impl.go (1)

55-74: Verified — SetContext is invoked by callers (nil-context risk mitigated).
main.go:114–116: scanService.SetContext(ctx), resultService.SetContext(ctx), scanossApiService.SetContext(ctx)

backend/service/component_service.go (1)

28-37: Interface change: confirm all implementers updated to include SearchComponents

Adding SearchComponents to ComponentService is a breaking change — ensure every concrete implementer (e.g., ComponentServiceImpl) adds this method or provide a compatibility adapter. Repository search returned no matches for SearchComponents; manually verify implementations and update tests/call sites.

frontend/src/components/ui/keyboard-table.tsx (1)

106-106: Remove redundant role="row" on TableRow

-                role="row"
frontend/src/components/SelectLicenseList.tsx (1)

73-85: Modal popover blocks the Clear button interaction

This was flagged earlier; using modal blocks outside clicks, making Clear unusable while open. Remove modal or move Clear inside PopoverContent.

-    <Popover open={popoverOpen} onOpenChange={setPopoverOpen} modal>
+    <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
frontend/src/components/ReplaceComponentDialog.tsx (4)

179-186: Conflicting confirm shortcut: double-bound handlers fire together

Both hooks bind the same keys; when popoverOpen is true, submit and online search can trigger simultaneously. Use a single binding that branches.

-  const ref = useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => submitButtonRef.current?.click(), {
-    enableOnFormTags: true,
-  });
-
-  useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => handleSearchOnline(), {
-    enableOnFormTags: true,
-    enabled: popoverOpen,
-  });
+  const ref = useKeyboardShortcut(
+    KEYBOARD_SHORTCUTS.confirm.keys,
+    () => {
+      if (popoverOpen) handleSearchOnline();
+      else submitButtonRef.current?.click();
+    },
+    { enableOnFormTags: true },
+    [popoverOpen]
+  );

231-241: Use a semantic button for “Search online” (a11y)

Replace the clickable div with a button and full-width hit area.

-                              {searchValue ? (
-                                <CommandItem asChild>
-                                  <div onClick={handleSearchOnline} className="flex items-center justify-between gap-2">
-                                    <div className="flex items-center gap-2">
-                                      <Search className="h-3 w-3" />
-                                      Search &ldquo;{searchValue}&rdquo; online
-                                    </div>
-                                    <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys, modifierKey.label)[0]} />
-                                  </div>
-                                </CommandItem>
-                              ) : null}
+                              {searchValue ? (
+                                <CommandItem asChild>
+                                  <button
+                                    type="button"
+                                    onClick={handleSearchOnline}
+                                    className="flex w-full items-center justify-between gap-2"
+                                    aria-label={`Search "${searchValue}" online`}
+                                  >
+                                    <div className="flex items-center gap-2">
+                                      <Search className="h-3 w-3" />
+                                      Search &ldquo;{searchValue}&rdquo; online
+                                    </div>
+                                    <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys, modifierKey.label)[0]} />
+                                  </button>
+                                </CommandItem>
+                              ) : null}

137-156: Clear matchedLicenses to prevent stale results (start/empty/error)

Reset before fetch, when no results, and on error to avoid showing old matches.

-  const getMatchedLicenses = async (purl: string) => {
-    try {
-      const { component } = await GetLicensesByPurl({ purl });
-
-      if (!component || !component.licenses) return;
-
-      const matchedLicenses: entities.License[] = component.licenses.map((license) => ({
-        name: license.full_name,
-        licenseId: license.id,
-        reference: `https://spdx.org/licenses/${license.id}.html`, // This is a workaround for the fact that the reference is not available in the response
-      }));
-      setMatchedLicenses(matchedLicenses);
-    } catch (error) {
-      toast({
-        title: 'Error',
-        description: 'Error fetching matched licenses',
-        variant: 'destructive',
-      });
-    }
-  };
+  const getMatchedLicenses = async (purl: string) => {
+    // Reset before fetching to avoid showing stale results
+    setMatchedLicenses([]);
+    try {
+      const { component } = await GetLicensesByPurl({ purl });
+      if (!component || !component.licenses || component.licenses.length === 0) {
+        setMatchedLicenses([]);
+        return;
+      }
+      const licenses: entities.License[] = component.licenses.map((license) => ({
+        name: license.full_name,
+        licenseId: license.id,
+        reference: `https://spdx.org/licenses/${license.id}.html`,
+      }));
+      setMatchedLicenses(licenses);
+    } catch (error) {
+      setMatchedLicenses([]);
+      toast({
+        title: 'Error',
+        description: 'Error fetching matched licenses',
+        variant: 'destructive',
+      });
+    }
+  };

205-215: Verify intent for Popover modal inside Dialog

With modal, Cancel/Confirm and other outside controls are non-interactive while open. Confirm this UX; if not intended, drop modal.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/ReplaceComponentDialog.tsx (1)

122-135: Clear matchedLicenses when switching components to avoid stale suggestions

If a user selects a declared/new component, previous online “Matched” licenses persist. Clear them on selection.

   const onComponentSelected = (component: entities.DeclaredComponent) => {
@@
     form.setValue('name', component.name);
     resetLicense();
+    setMatchedLicenses([]); // avoid stale matched list when switching sources
     setNewComponentDialogOpen(false);
   };
                                   onSelect={() => {
                                     form.setValue('purl', component.purl);
                                     form.setValue('name', component.name);
                                     resetLicense();
+                                    setMatchedLicenses([]);
                                     setPopoverOpen(false);
                                   }}

Also applies to: 251-260

🧹 Nitpick comments (3)
frontend/src/components/ReplaceComponentDialog.tsx (2)

225-235: Use a semantic button for “Add new component”

For a11y/semantics, avoid clickable divs.

-                              <CommandItem asChild>
-                                <div
-                                  onClick={() => {
-                                    setNewComponentDialogOpen(true);
-                                    setPopoverOpen(false);
-                                  }}
-                                >
-                                  <Plus className="mr-2 h-4 w-4" />
-                                  Add new component
-                                </div>
-                              </CommandItem>
+                              <CommandItem asChild>
+                                <button
+                                  type="button"
+                                  onClick={() => {
+                                    setNewComponentDialogOpen(true);
+                                    setPopoverOpen(false);
+                                  }}
+                                  className="flex w-full items-center"
+                                >
+                                  <Plus className="mr-2 h-4 w-4" />
+                                  Add new component
+                                </button>
+                              </CommandItem>

98-104: Optional: auto-fetch matched licenses when purl changes

Keeps “Matched” section in sync for any source of selection (declared/new/online).

Add after selectedPurl is defined:

useEffect(() => {
  if (!selectedPurl) {
    setMatchedLicenses([]);
    return;
  }
  getMatchedLicenses(selectedPurl);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedPurl]);
backend/service/license_service_impl.go (1)

41-45: Fail‑fast nil check in NewLicenseServiceImpl

Verified: main.go (line 101) constructs scanossApiService via NewScanossApiServiceHttpImpl() and passes it to NewLicenseServiceImpl; no explicit nil injections found. Still add a defensive nil-check in NewLicenseServiceImpl to surface wiring errors early — e.g. if repo == nil || scanossApiService == nil { panic("NewLicenseServiceImpl: nil dependency") }.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7901638 and a7ae238.

📒 Files selected for processing (3)
  • .github/workflows/release.yml (1 hunks)
  • backend/service/license_service_impl.go (2 hunks)
  • frontend/src/components/ReplaceComponentDialog.tsx (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/release.yml
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/components/ReplaceComponentDialog.tsx (8)
frontend/wailsjs/go/models.ts (2)
  • DeclaredComponent (332-345)
  • License (444-459)
frontend/src/hooks/useEnvironment.tsx (1)
  • useEnvironment (37-66)
frontend/src/contexts/DialogStateContext.tsx (1)
  • useDialogRegistration (85-94)
frontend/wailsjs/go/service/LicenseServiceImpl.js (1)
  • GetLicensesByPurl (9-11)
frontend/src/lib/shortcuts.ts (2)
  • KEYBOARD_SHORTCUTS (32-164)
  • getShortcutDisplay (166-180)
frontend/src/components/ShortcutBadge.tsx (1)
  • ShortcutBadge (1-3)
frontend/src/components/SelectLicenseList.tsx (1)
  • SelectLicenseList (53-113)
frontend/src/components/OnlineComponentSearchDialog.tsx (1)
  • OnlineComponentSearchDialog (91-234)
backend/service/license_service_impl.go (6)
backend/repository/license_repository.go (1)
  • LicenseRepository (28-30)
backend/service/scanoss_api_service.go (1)
  • ScanossApiService (32-36)
backend/service/license_service.go (1)
  • LicenseService (28-31)
backend/entities/common.go (1)
  • ComponentRequest (3-6)
backend/entities/license.go (1)
  • GetLicensesByPurlResponse (50-53)
internal/utils/custom_validations.go (1)
  • GetValidator (37-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: integration-tests
  • GitHub Check: unit_tests
🔇 Additional comments (5)
frontend/src/components/ReplaceComponentDialog.tsx (4)

184-192: Fix conflicting confirm shortcut: consolidate into a single binding

Both handlers fire when the popover is open, causing submit + open-search to trigger together. Use one binding that branches on popoverOpen and remove the second hook.

-  const ref = useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => submitButtonRef.current?.click(), {
-    enableOnFormTags: true,
-  });
-
-  useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => handleSearchOnline(), {
-    enableOnFormTags: true,
-    enabled: popoverOpen,
-  });
+  const ref = useKeyboardShortcut(
+    KEYBOARD_SHORTCUTS.confirm.keys,
+    () => {
+      if (popoverOpen) handleSearchOnline();
+      else submitButtonRef.current?.click();
+    },
+    { enableOnFormTags: true }
+  );

236-246: Use a semantic button for the “Search online” action

Clickable div is not accessible; use a button to ensure focus/keyboard semantics.

-                              {searchValue ? (
-                                <CommandItem asChild>
-                                  <div onClick={handleSearchOnline} className="flex items-center justify-between gap-2">
-                                    <div className="flex items-center gap-2">
-                                      <Search className="h-3 w-3" />
-                                      Search &ldquo;{searchValue}&rdquo; online
-                                    </div>
-                                    <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys, modifierKey.label)[0]} />
-                                  </div>
-                                </CommandItem>
-                              ) : null}
+                              {searchValue ? (
+                                <CommandItem asChild>
+                                  <button
+                                    type="button"
+                                    onClick={handleSearchOnline}
+                                    className="flex w-full items-center justify-between gap-2"
+                                    aria-label={`Search "${searchValue}" online`}
+                                  >
+                                    <div className="flex items-center gap-2">
+                                      <Search className="h-3 w-3" />
+                                      Search &ldquo;{searchValue}&rdquo; online
+                                    </div>
+                                    <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys, modifierKey.label)[0]} />
+                                  </button>
+                                </CommandItem>
+                              ) : null}

335-341: Clear local search input after picking from online search

Prevents stale inline search text when returning to the popover.

           onComponentSelect={async (c) => {
             onComponentSelected(c);
             await getMatchedLicenses(c.purl);
+            setSearchValue('');
           }}

323-325: Prevent double submit: remove onClick from submit button

Form has onSubmit; keeping onClick risks submitting twice.

-                <Button ref={submitButtonRef} type="submit" onClick={form.handleSubmit(onSubmit)}>
+                <Button ref={submitButtonRef} type="submit">
                   Confirm <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys, modifierKey.label)[0]} />
                 </Button>
backend/service/license_service_impl.go (1)

37-39: DI addition looks good

Adding scanossApiService to the impl is consistent with the new interface and keeps concerns separated.

@github-actions
Copy link

github-actions bot commented Oct 1, 2025

SCANOSS SCAN Completed 🚀

  • Detected components: 13
  • Undeclared components: 0
  • Declared components: 13
  • Detected files: 179
  • Detected files undeclared: 0
  • Detected files declared: 179
  • Licenses detected: 10
  • Licenses detected with copyleft: 3
  • Policies: ✅ 1 pass (1 total)

View more details on SCANOSS Action Summary

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (5)
frontend/src/components/ui/keyboard-table.tsx (1)

98-106: Remove redundant role="row" on <TableRow>.

The <tr> element has an implicit ARIA role of "row," and the TableRow component spreads all props onto <tr>. Explicitly setting role="row" is unnecessary and may lead to duplicate role declarations, which can confuse assistive technologies.

Apply this diff to remove the redundant role:

   <TableRow
     key={getRowId ? getRowId(row, i) : i}
     id={`row-${i}`}
-    role="row"
     data-row-index={i}
     onClick={() => onRowSelect(row)}
     className={customClassName}
     onMouseEnter={() => setFocusedIndex(i)}
   >

Based on previous review feedback.

backend/service/scanoss_api_service_http_impl.go (1)

98-127: The timeout field is unused; per-request timeout not enforced.

Lines 109-110 use context.WithCancel, which doesn't enforce the s.timeout duration. The only timeout in effect is the one configured on the http.Client itself (line 68). This means the timeout field (line 52, line 70) is effectively unused, and per-request timeout control is not available.

Apply this diff to enforce the per-request timeout:

 	ctx := s.ctx
 	if ctx == nil {
 		ctx = context.Background()
 	}
 
-	ctx, cancel := context.WithCancel(ctx)
+	ctx, cancel := context.WithTimeout(ctx, s.timeout)
 	defer cancel()

If the timeout field remains unused, consider removing it from the struct (line 52) and the constructor (line 70) to reduce confusion.

frontend/src/components/ReplaceComponentDialog.tsx (3)

236-246: Clickable div still present—a11y issue not resolved

The past review comment recommended changing the clickable <div> to a semantic <button> for accessibility, marked as addressed. However, line 238 still uses a <div onClick={...}>, which is not keyboard-accessible or properly announced to screen readers.

Apply the suggested fix:

-                              {searchValue ? (
-                                <CommandItem asChild>
-                                  <div onClick={handleSearchOnline} className="flex items-center justify-between gap-2">
-                                    <div className="flex items-center gap-2">
-                                      <Search className="h-3 w-3" />
-                                      Search &ldquo;{searchValue}&rdquo; online
-                                    </div>
-                                    <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys, modifierKey.label)[0]} />
-                                  </div>
-                                </CommandItem>
-                              ) : null}
+                              {searchValue ? (
+                                <CommandItem asChild>
+                                  <button
+                                    type="button"
+                                    onClick={handleSearchOnline}
+                                    className="flex w-full items-center justify-between gap-2"
+                                  >
+                                    <div className="flex items-center gap-2">
+                                      <Search className="h-3 w-3" />
+                                      Search &ldquo;{searchValue}&rdquo; online
+                                    </div>
+                                    <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys, modifierKey.label)[0]} />
+                                  </button>
+                                </CommandItem>
+                              ) : null}

323-325: Double submit risk: onClick handler still present on button

The past review comment recommended removing onClick={form.handleSubmit(onSubmit)} from the Confirm button to prevent double submission (form's onSubmit at line 197 already handles it). The code still has the onClick handler.

Apply the fix:

-                <Button ref={submitButtonRef} onClick={form.handleSubmit(onSubmit)}>
+                <Button ref={submitButtonRef} type="submit">
                   Confirm <ShortcutBadge shortcut={getShortcutDisplay(KEYBOARD_SHORTCUTS.confirm.keys, modifierKey.label)[0]} />
                 </Button>

188-191: Critical: duplicate keyboard shortcut binding still present

Despite being marked as addressed, the duplicate KEYBOARD_SHORTCUTS.confirm binding remains. When the popover is open, both hooks fire—the first (lines 184–186) clicks submit, the second (lines 188–191) opens online search—causing conflicting actions and an unintended submit.

Remove this second hook and update the first hook to branch on popoverOpen:

-  const ref = useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => submitButtonRef.current?.click(), {
+  const ref = useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => {
+    if (popoverOpen) {
+      handleSearchOnline();
+    } else {
+      submitButtonRef.current?.click();
+    }
+  }, {
     enableOnFormTags: true,
   });

-  useKeyboardShortcut(KEYBOARD_SHORTCUTS.confirm.keys, () => handleSearchOnline(), {
-    enableOnFormTags: true,
-    enabled: popoverOpen,
-  });
🧹 Nitpick comments (5)
frontend/src/components/ui/keyboard-table.tsx (2)

25-30: Prefer rows.length over DOM query for row count.

Querying the DOM to determine the number of rows on every move call is less reliable and slightly less efficient than using rows.length directly. If the DOM hasn't re-rendered yet, count may be stale, leading to incorrect clamping.

Apply this diff to use rows.length:

   const move = (next: number) => {
-    const count = containerRef.current?.querySelectorAll<HTMLElement>('[data-row-index]').length ?? 0;
+    const count = rows.length;
     if (!count) return;
     const last = count - 1;
     setFocusedIndex(Math.max(0, Math.min(last, next)));
   };

49-52: Replace magic number 99999 with a more explicit constant.

Using 99999 as a stand-in for "end of list" works but is not self-documenting. Consider using rows.length or Number.MAX_SAFE_INTEGER for clarity.

Apply this diff for clarity:

       case 'End':
         e.preventDefault();
         e.stopPropagation();
-        move(99999);
+        move(Number.MAX_SAFE_INTEGER);
         break;

Alternatively, if you refactor move to accept rows.length, you could call move(rows.length - 1) directly for the End key.

backend/service/scanoss_api_service_http_impl.go (2)

55-74: Constructor always returns nil error; consider simplifying the signature.

The constructor never returns an error, so the error return value is unnecessary. Consider changing the signature to NewScanossApiServiceHttpImpl() ScanossApiService.

Apply this diff:

-func NewScanossApiServiceHttpImpl() (ScanossApiService, error) {
+func NewScanossApiServiceHttpImpl() ScanossApiService {
 	cfg := config.GetInstance()
 	baseURL := cfg.GetApiUrl()
 	apiKey := cfg.GetApiToken()
 
 	if baseURL == "" {
 		baseURL = config.SCANOSS_PREMIUM_API_URL
 	}
 
 	service := &ScanossApiServiceHttpImpl{
 		apiKey:  apiKey,
 		baseURL: baseURL,
 		client: &http.Client{
 			Timeout: 30 * time.Second,
 		},
 		timeout: 30 * time.Second,
 	}
 
-	return service, nil
+	return service
 }

197-235: Always sending requirement parameter, even when empty.

Line 213 adds requirement to query parameters unconditionally. If request.Requirement is empty, this sends requirement= in the query string. For consistency with SearchComponents (which only adds non-empty optional parameters), consider adding requirement only when non-empty.

Apply this diff:

 	params := QueryParams{
-		"purl":        request.Purl,
-		"requirement": request.Requirement,
+		"purl": request.Purl,
 	}
+
+	if request.Requirement != "" {
+		params["requirement"] = request.Requirement
+	}
frontend/src/components/ReplaceComponentDialog.tsx (1)

334-343: Consider clearing search value after online component selection

After selecting a component from the online search, searchValue isn't cleared. This means the next time the popover opens, the stale search term remains visible. While this preserves context, it may confuse users.

Clear the search input after closing the online search dialog:

         <OnlineComponentSearchDialog
           onOpenChange={() => setOnlineSearchDialogOpen(false)}
           searchTerm={searchValue}
           onComponentSelect={async (c) => {
             onComponentSelected(c);
             await getMatchedLicenses(c.purl);
+            setSearchValue('');
           }}
         />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7ae238 and 3e2de76.

📒 Files selected for processing (4)
  • backend/service/scanoss_api_service_http_impl.go (1 hunks)
  • frontend/src/components/ReplaceComponentDialog.tsx (10 hunks)
  • frontend/src/components/ui/keyboard-table.tsx (1 hunks)
  • scanoss.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
backend/service/scanoss_api_service_http_impl.go (6)
backend/service/scanoss_api_service.go (1)
  • ScanossApiService (32-36)
internal/config/config.go (2)
  • GetInstance (111-116)
  • SCANOSS_PREMIUM_API_URL (55-55)
backend/entities/component_search.go (2)
  • ComponentSearchRequest (27-34)
  • ComponentSearchResponse (44-47)
internal/utils/custom_validations.go (1)
  • GetValidator (37-39)
backend/entities/common.go (1)
  • ComponentRequest (3-6)
backend/entities/license.go (1)
  • GetLicensesByPurlResponse (50-53)
frontend/src/components/ReplaceComponentDialog.tsx (9)
frontend/wailsjs/go/models.ts (2)
  • DeclaredComponent (332-345)
  • License (444-459)
frontend/src/hooks/useEnvironment.tsx (1)
  • useEnvironment (37-66)
frontend/src/contexts/DialogStateContext.tsx (1)
  • useDialogRegistration (85-94)
frontend/wailsjs/go/service/LicenseServiceImpl.js (1)
  • GetLicensesByPurl (9-11)
frontend/src/hooks/useKeyboardShortcut.tsx (1)
  • useKeyboardShortcut (31-52)
frontend/src/lib/shortcuts.ts (1)
  • getShortcutDisplay (166-180)
frontend/src/components/ShortcutBadge.tsx (1)
  • ShortcutBadge (1-3)
frontend/src/components/SelectLicenseList.tsx (1)
  • SelectLicenseList (53-113)
frontend/src/components/OnlineComponentSearchDialog.tsx (1)
  • OnlineComponentSearchDialog (91-234)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (14)
frontend/src/components/ui/keyboard-table.tsx (2)

64-71: Enter key guard correctly added.

The guard at line 67 correctly prevents onRowSelect from being called with an invalid index, addressing the previous review comment. This ensures robustness when the table is empty or focusedIndex is out of bounds.


81-118: Render logic is well-structured.

The component correctly uses role="grid" and aria-activedescendant for keyboard navigation, applies focus styling with clsx, and safely handles missing cell data with fallback logic. The interaction handlers (onClick, onMouseEnter) provide a good user experience.

scanoss.json (1)

139-152: LGTM!

These replacement entries are properly structured with all required fields.

backend/service/scanoss_api_service_http_impl.go (5)

41-45: LGTM!

The HTTPClient interface enables testability via dependency injection, and the QueryParams type alias improves readability for query parameter handling.


47-53: Good structure for managing API client state.

The struct properly separates concerns with context, credentials, HTTP client interface, and timeout configuration.


76-78: LGTM!

The SetContext method follows the standard pattern for injecting a parent context into the service.


80-96: LGTM!

The buildURL helper correctly uses the url package to construct URLs with properly encoded query parameters.


129-195: LGTM!

The SearchComponents method correctly:

  • Validates the request with the validator
  • Builds query parameters conditionally (only non-empty/non-zero values)
  • Handles errors with proper logging
  • Checks for 200 status and decodes the response
frontend/src/components/ReplaceComponentDialog.tsx (6)

78-86: LGTM!

State initialization and dialog registration are properly implemented. The dialog correctly registers as blocking keyboard navigation.


122-135: LGTM!

The component selection handler properly prevents duplicates, updates form state, and resets dependent fields.


137-161: LGTM!

The license fetching logic properly clears stale state before fetch, on empty results, and on errors. The workaround for missing reference URLs is clearly documented.


163-166: LGTM!

Handler correctly toggles dialog states for the online search flow.


210-221: LGTM!

Popover modal behavior and controlled CommandInput are correctly implemented.


288-288: LGTM!

Passing matchedLicenses to SelectLicenseList correctly integrates the fetched license suggestions into the UI.

Comment on lines +21 to +23
export function KeyboardTable<T>({ columns, rows, onRowSelect, getRowId, rowClassName, initialFocusedIndex = 0 }: KeyboardTableProps<T>) {
const containerRef = useRef<HTMLDivElement>(null);
const [focusedIndex, setFocusedIndex] = useState<number>(initialFocusedIndex);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clamp initialFocusedIndex on mount to avoid invalid aria-activedescendant.

If initialFocusedIndex exceeds the number of rows, focusedIndex will point to a non-existent row until the first keyboard or hover interaction. This can cause aria-activedescendant to reference an invalid ID and may confuse assistive technologies.

Consider initializing focusedIndex with a clamped value:

-  const [focusedIndex, setFocusedIndex] = useState<number>(initialFocusedIndex);
+  const [focusedIndex, setFocusedIndex] = useState<number>(() =>
+    Math.max(0, Math.min(initialFocusedIndex, rows.length - 1))
+  );

Alternatively, use a useEffect to clamp focusedIndex whenever rows.length changes, ensuring the focused index remains valid if rows are updated dynamically.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function KeyboardTable<T>({ columns, rows, onRowSelect, getRowId, rowClassName, initialFocusedIndex = 0 }: KeyboardTableProps<T>) {
const containerRef = useRef<HTMLDivElement>(null);
const [focusedIndex, setFocusedIndex] = useState<number>(initialFocusedIndex);
export function KeyboardTable<T>({
columns,
rows,
onRowSelect,
getRowId,
rowClassName,
initialFocusedIndex = 0,
}: KeyboardTableProps<T>) {
const containerRef = useRef<HTMLDivElement>(null);
const [focusedIndex, setFocusedIndex] = useState<number>(() =>
Math.max(0, Math.min(initialFocusedIndex, rows.length - 1))
);
// ...
}
🤖 Prompt for AI Agents
In frontend/src/components/ui/keyboard-table.tsx around lines 21-23, the
focusedIndex is initialized directly from initialFocusedIndex which can exceed
rows.length and produce an invalid aria-activedescendant; initialize
focusedIndex with a clamped value (e.g., clamp initialFocusedIndex between 0 and
rows.length - 1) and also add a useEffect that watches rows.length (and
initialFocusedIndex if desired) to re-clamp and setFocusedIndex whenever the
number of rows changes so the focused index always points to an existing row.

@github-actions
Copy link

github-actions bot commented Oct 1, 2025

SCANOSS SCAN Completed 🚀

  • Detected components: 13
  • Undeclared components: 0
  • Declared components: 13
  • Detected files: 179
  • Detected files undeclared: 0
  • Detected files declared: 179
  • Licenses detected: 10
  • Licenses detected with copyleft: 3
  • Policies: ✅ 1 pass (1 total)

View more details on SCANOSS Action Summary

@github-actions
Copy link

github-actions bot commented Oct 1, 2025

SCANOSS SCAN Completed 🚀

  • Detected components: 3
  • Undeclared components: 0
  • Declared components: 3
  • Detected files: 168
  • Detected files undeclared: 0
  • Detected files declared: 168
  • Licenses detected: 2
  • Licenses detected with copyleft: 1
  • Policies: ✅ 1 pass (1 total)

View more details on SCANOSS Action Summary

@github-actions
Copy link

github-actions bot commented Oct 1, 2025

SCANOSS SCAN Completed 🚀

  • Detected components: 3
  • Undeclared components: 0
  • Declared components: 3
  • Detected files: 168
  • Detected files undeclared: 0
  • Detected files declared: 168
  • Licenses detected: 2
  • Licenses detected with copyleft: 1
  • Policies: ✅ 1 pass (1 total)

View more details on SCANOSS Action Summary

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
.github/workflows/test.yml (1)

14-14: Remove unused GOLANG_VERSION env from workflow

In .github/workflows/test.yml (line 14), the GOLANG_VERSION env is ignored by actions/setup-go (it reads go.mod via go-version-file). Remove this line or switch setup-go to use go-version: ${{ env.GOLANG_VERSION }}.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e5ef30c and 1d3df20.

📒 Files selected for processing (1)
  • .github/workflows/test.yml (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

mkdir build
cp -r assets build/assets
wails build
wails build APPARGS="--key test"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

❓ Verification inconclusive

APPARGS is positioned incorrectly; build likely fails.

Environment variables must precede the command. Current syntax passes APPARGS=... as a CLI arg to wails.

Apply:

-          wails build APPARGS="--key test"
+          APPARGS="--key test" wails build

Also confirm APPARGS is intended for build here; it’s typically used for wails dev.


Move APPARGS before the command
Replace the line so the environment variable is set correctly:

-          wails build APPARGS="--key test"
+          APPARGS="--key test" wails build

Also confirm that APPARGS should be used with wails build here (it’s typically passed to wails dev).

🤖 Prompt for AI Agents
In .github/workflows/test.yml around line 40, the environment variable is
currently placed after the command; move APPARGS before the command so it is
exported for the process (i.e., set APPARGS="--key test" wails build), and
verify whether APPARGS is intended for a build step — if it was meant for
development, change the command to use wails dev instead of wails build.

- name: Run integration tests
env:
SCANOSS_API_KEY: ${{ secrets.SCANOSS_API_KEY }}
SCANOSS_API_KEY: ${{ secrets.SC_API_KEY }}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Secret mapping OK; guard integration tests when secret is absent.

Forked PRs don’t receive repo secrets; this step will fail. Add a conditional so CI skips gracefully.

Apply:

       - name: Run integration tests
+        if: ${{ secrets.SC_API_KEY != '' }}
         env:
           SCANOSS_API_KEY: ${{ secrets.SC_API_KEY }}
         run: go test -v ./... -tags=integration

Check the code expects SCANOSS_API_KEY (not SC_API_KEY):


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Searching for env usage:"
rg -nP 'SCANOSS_API_KEY|SC_API_KEY' -C2

Length of output: 508


Guard integration tests when SC_API_KEY secret is absent.
Forked PRs don't receive repo secrets; skip the integration test step when the secret is empty. Verified: README expects SCANOSS_API_KEY and Makefile maps SC_API_KEY -> SCANOSS_API_KEY; workflow currently sets SCANOSS_API_KEY: ${{ secrets.SC_API_KEY }}. Apply:

       - name: Run integration tests
+        if: ${{ secrets.SC_API_KEY != '' }}
         env:
           SCANOSS_API_KEY: ${{ secrets.SC_API_KEY }}
         run: go test -v ./... -tags=integration
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SCANOSS_API_KEY: ${{ secrets.SC_API_KEY }}
- name: Run integration tests
if: ${{ secrets.SC_API_KEY != '' }}
env:
SCANOSS_API_KEY: ${{ secrets.SC_API_KEY }}
run: go test -v ./... -tags=integration
🤖 Prompt for AI Agents
.github/workflows/test.yml around line 100: the workflow unconditionally sets
SCANOSS_API_KEY from secrets which causes forked PRs (no secret) to fail; guard
the integration test step or job by adding a conditional so it only runs when
the secret is present, for example add an if: ${{ secrets.SC_API_KEY != '' }} on
the integration test step or job (or set SCANOSS_API_KEY: ${{ secrets.SC_API_KEY
}} only within that conditional) so the step is skipped for forks without the
secret.

@matiasdaloia matiasdaloia merged commit 446260e into main Oct 1, 2025
6 checks passed
@matiasdaloia matiasdaloia deleted the feature/mdaloia/sp-2882-add-component-search branch October 1, 2025 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants