Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions workspaces/boost/.changeset/extensible-browse-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@red-hat-developer-hub/backstage-plugin-boost': minor
---

Add extensible browse filters via NFS AiCatalogFilterBlueprint. Deployers can disable built-in filters and third-party plugins can add new filters to the AI Catalog sidebar via app-config.

**Breaking:** The page extension ID changed from `page:boost` to `page:boost/ai-catalog`. Update any `app.extensions` references in `app-config.yaml` accordingly.
2 changes: 2 additions & 0 deletions workspaces/boost/app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ app:
redirects:
- from: /
to: /ai-catalog
# Example: disable a built-in filter
# - ai-catalog-filter:boost/owner: false

organization:
name: Red Hat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ Boost has a backend with 30+ API routes and 9 plugin packages but no frontend. T

- Chat UI, admin panels, or agent gallery (future domains)
- Custom entity detail pages (use existing catalog pages)
- RHDH dynamic plugin packaging (Scalprum/export-dynamic deferred)
- Custom search collator for global search (rely on default catalog indexing)

## Decisions
Expand Down Expand Up @@ -52,6 +51,55 @@ BUI (`@backstage/ui`) is the component library for all new UI. MUI v5 as fallbac

Permission checks for `ai-catalog.asset.read.usage-docs` default to allow when the permission is not yet registered (RHDHPLAN-1508 not built). Content is shown, and enforcement activates automatically when RBAC lands.

### Decision 8: Extensible browse filters via data-driven FilterDefinition

The browse page filter sidebar becomes NFS-extensible using a **data-driven** approach. Filters are plain objects (`FilterDefinition`), not per-filter React components. The `FilterSidebar` renders a generic `<Select>` for each registered filter — the same pattern already used for all 4 current filters.

**Architecture:**

- A `FilterDefinition` interface defines each filter: `urlParam`, `label`, `getOptions(entities)`, `matchEntity(entity, values)`, `priority`
- `AiCatalogFilterBlueprint` wraps a `FilterDefinition` as an NFS extension (kind: `ai-catalog-filter`) with a single custom `createExtensionDataRef`
- Built-in filters are plain objects in `src/filters/builtinFilters.ts`, registered as Blueprint extensions in `plugin.tsx`
- The `aiCatalogPage` PageBlueprint uses `makeWithOverrides` to declare a `filters` input, resolves `FilterDefinition[]`, sorts by priority, and passes to the page component
- `FilterSidebar` maps over the array and renders `<Select>` for each — no per-filter component files
- `useUrlFilters` reads/writes URL params dynamically from the definition array
- `applyEntityFilters` loops over active definitions calling `matchEntity` in AND logic

**Deployer customization (app-config.yaml):**

```yaml
app:
extensions:
# Disable a built-in filter
- ai-catalog-filter:boost/owner: false
# Custom filter from a third-party module (just enable it)
- ai-catalog-filter:my-plugin/team-filter: {}
```

**Third-party filter contribution:**

```typescript
createFrontendModule({
pluginId: 'boost',
extensions: [
AiCatalogFilterBlueprint.make({
name: 'team-filter',
params: {
urlParam: 'team',
label: 'Team',
getOptions: entities =>
[...new Set(entities.map(e => e.spec?.team).filter(Boolean))]
.sort()
.map(t => ({ id: t, label: t })),
matchEntity: (entity, values) =>
values.some(v => v === entity.spec?.team),
priority: 200,
},
}),
],
});
```

## Entity Model

Backstage v1.51.0 introduced `AiResource` kind and `API` with `spec.type: mcp-server` via `@backstage/plugin-catalog-backend-module-ai-model`. Boost uses upstream kinds where available:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Dynamic Plugin Export

> **Status: Draft** — Pre-implementation specification. Subject to change during implementation.

Package the AI Catalog frontend plugin for RHDH dynamic plugin deployment and register it in the overlays repo for OCI image builds. Boost is NFS-only (`createFrontendPlugin` as default export) — no Scalprum, no `./alpha`, no legacy entry point.

## Requirements

### Requirement: Plugin Export Configuration

The plugin is configured for `rhdh-cli plugin export`.

#### Scenario: export-dynamic script exists

- **GIVEN** the `plugins/boost/package.json`
- **WHEN** a developer runs `yarn export-dynamic`
- **THEN** `rhdh-cli plugin export` runs and produces output in `dist-dynamic/`

#### Scenario: dist-dynamic included in package files

- **GIVEN** the plugin's `package.json` `files` array
- **THEN** it includes `dist-dynamic/*.*` and `dist-dynamic/dist/**`
- **AND** the published package contains the dynamic plugin bundle

### Requirement: Default Extension Configuration

A default `app-config.dynamic.yaml` provides sensible extension defaults for deployers.

#### Scenario: Page route configured

- **GIVEN** the `app-config.dynamic.yaml` in `plugins/boost/`
- **THEN** it configures `page:boost/ai-catalog` with the `/ai-catalog` path

#### Scenario: Entity cards configured

- **GIVEN** the `app-config.dynamic.yaml`
- **THEN** it lists `entity-card:boost/summary`, `entity-card:boost/adoption`, and `entity-card:boost/version-list` under `app.extensions`
- **AND** each card has a default entity filter matching AI asset kinds

#### Scenario: Entity tab configured

- **GIVEN** the `app-config.dynamic.yaml`
- **THEN** it lists `entity-content:boost/usage` under `app.extensions`
- **AND** the tab has a default title and group assignment

### Requirement: Overlay Registration

The plugin is registered in the overlays repo for automated OCI image builds.

#### Scenario: Plugin added to rhdh-plugin-export-overlays

- **GIVEN** the `redhat-developer/rhdh-plugin-export-overlays` repository
- **WHEN** a PR adds the boost frontend plugin entry
- **THEN** the CI pipeline builds an OCI image for the plugin
- **AND** the image is published to the configured registry

#### Scenario: Image reference updated in workspace

- **GIVEN** the OCI image is published
- **WHEN** the workspace `dynamic-plugins-image-reference.yaml` is updated
- **THEN** it contains the OCI image reference for the boost frontend plugin
- **AND** the reference follows the same format as existing backend plugin entries

### Requirement: Plugin Loads in RHDH

The OCI-packaged plugin loads correctly in an RHDH deployment.

#### Scenario: Plugin loads with Module Federation

- **GIVEN** an RHDH deployment with `ENABLE_STANDARD_MODULE_FEDERATION=true`
- **AND** the boost frontend dynamic plugin is installed via `dynamic-plugins.yaml` with `enabled: true`
- **WHEN** a user navigates to the RHDH instance
- **THEN** the "AI Catalog" nav item appears in the sidebar
- **AND** navigating to `/ai-catalog` renders the browse page

#### Scenario: Entity page extensions mount

- **GIVEN** the boost frontend dynamic plugin is installed
- **WHEN** a user navigates to a catalog entity page for an AI asset
- **THEN** the AI Asset Summary Card, Download/Adopt Card, and Version List Card render on the overview
- **AND** the Usage tab appears in the entity page tabs

#### Scenario: Extensions absent on non-AI entities

- **GIVEN** the boost frontend dynamic plugin is installed
- **WHEN** a user navigates to a catalog entity page for a non-AI entity (e.g., a regular Component)
- **THEN** no boost entity cards or tabs are rendered

### Requirement: Adopter Overrides

Deployers can customize the plugin via `app.extensions` in `app-config.yaml`.

#### Scenario: Disable an entity card

- **GIVEN** the deployer sets `entity-card:boost/adoption: false` in `app.extensions`
- **WHEN** a user views an AI asset entity page
- **THEN** the Download/Adopt Card is not rendered
- **AND** other boost cards still render

#### Scenario: Rename a tab

- **GIVEN** the deployer sets `entity-content:boost/usage` with `config.title: "How to Use"`
- **WHEN** a user views an AI asset entity page
- **THEN** the tab label reads "How to Use" instead of the default

#### Scenario: Change entity filter on a card

- **GIVEN** the deployer sets `entity-card:boost/summary` with `config.filter` restricting to `kind: component`
- **WHEN** a user views an AiResource entity page
- **THEN** the summary card is not rendered (filter excludes AiResource)
- **WHEN** a user views a Component/ai-agent entity page
- **THEN** the summary card renders

#### Scenario: Disable the page

- **GIVEN** the deployer sets `page:boost/ai-catalog: false` in `app.extensions`
- **WHEN** a user views the RHDH sidebar
- **THEN** the "AI Catalog" nav item is not present
- **AND** navigating to `/ai-catalog` shows a 404 or redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# E2E Tests

> **Status: Draft** — Pre-implementation specification. Subject to change during implementation.

Playwright end-to-end tests for the AI Catalog frontend plugin. Boost is NFS-only (no legacy app), so no `APP_MODE` dual testing. Tests use translation keys instead of hardcoded strings and include axe-core accessibility audits.

## Requirements

### Requirement: Playwright Infrastructure

The workspace has a working Playwright setup following rhdh-plugins conventions.

#### Scenario: Playwright config exists

- **GIVEN** the boost workspace
- **WHEN** a developer runs `yarn test:e2e`
- **THEN** Playwright starts the dev app via `yarn start`, waits for readiness, and runs tests from `e2e-tests/`

#### Scenario: Multi-locale test projects

- **GIVEN** the Playwright config defines projects for at least `en` and one non-English locale (e.g., `ja`)
- **WHEN** the full e2e suite runs
- **THEN** tests execute against both locales
- **AND** per-locale `app-config-e2e-*.yaml` overrides configure separate ports so projects can run in parallel

#### Scenario: CI integration

- **GIVEN** CI runs `yarn playwright test` in the boost workspace
- **WHEN** the test suite completes
- **THEN** test reports are generated in `e2e-test-report/`
- **AND** test artifacts (screenshots, traces on failure) are stored in `e2e-test-results/`

### Requirement: Browse Page Tests

The AI Catalog browse page is tested end-to-end.

#### Scenario: Card grid renders with fixture data

- **WHEN** the e2e test navigates to `/ai-catalog`
- **THEN** AI asset cards are visible on the page
- **AND** cards display translated text (using translation keys, not hardcoded English)

#### Scenario: Search filters cards

- **GIVEN** the browse page has loaded with AI assets
- **WHEN** the test types a keyword in the search bar
- **THEN** the visible cards are filtered to match the keyword
- **AND** the URL updates with `?q=<keyword>`

#### Scenario: Sidebar filters narrow results

- **GIVEN** the browse page has loaded
- **WHEN** the test selects a category filter (e.g., "skill")
- **THEN** only cards matching that category are shown
- **AND** the URL updates with the filter param

#### Scenario: Multiple filters combine as AND

- **GIVEN** the browse page has loaded
- **WHEN** the test selects category "skill" AND a specific tag
- **THEN** only cards matching both criteria are shown

#### Scenario: Clear filters resets the view

- **GIVEN** filters are active and the card grid is narrowed
- **WHEN** the test clicks the clear-filters action
- **THEN** all filter URL params are removed
- **AND** the full card grid is restored

#### Scenario: Card click navigates to entity detail

- **GIVEN** AI asset cards are visible
- **WHEN** the test clicks on a card
- **THEN** the browser navigates to the catalog entity detail page for that asset

### Requirement: State and Error Handling Tests

Edge cases are covered by e2e tests.

#### Scenario: Empty state when no assets match

- **GIVEN** the browse page has loaded
- **WHEN** the test applies filters that match no assets
- **THEN** the empty state message is displayed (verified via translation key)
- **AND** the clear-filters action is visible

#### Scenario: Pagination controls work

- **GIVEN** the catalog has more assets than one page
- **WHEN** the test clicks the next-page control
- **THEN** the card grid updates to show the next page of results
- **AND** the URL updates with the page param

#### Scenario: Sort control changes order

- **GIVEN** the browse page has loaded
- **WHEN** the test changes the sort to "last updated"
- **THEN** the card order updates accordingly

### Requirement: Accessibility Audits

Every tested page passes automated accessibility checks.

#### Scenario: Browse page passes axe audit

- **WHEN** the browse page has loaded
- **THEN** an axe-core scan with WCAG 2.1 AA rules reports zero violations
- **AND** violations (if any) are attached to the test report for debugging

#### Scenario: Filtered state passes axe audit

- **GIVEN** filters are active on the browse page
- **WHEN** an axe-core scan runs
- **THEN** zero WCAG 2.1 AA violations are reported (focus management, aria-labels intact)

### Requirement: Translation Key Usage

Tests do not hardcode English strings.

#### Scenario: UI text verified via translation keys

- **GIVEN** the test needs to verify a UI label
- **WHEN** it locates the element
- **THEN** it uses the translated string from the plugin's translation module for the current locale
- **AND** the same test code works for `en`, `de`, `es`, `fr`, `it`, `ja` without modification

#### Scenario: Translation helper utility exists

- **GIVEN** a `getTranslations(locale)` utility exists in `e2e-tests/utils/`
- **WHEN** a test calls it
- **THEN** it returns the message map for that locale loaded from the plugin's translation modules
Loading
Loading