diff --git a/.ci/pipelines/lib/disconnected/plugins.sh b/.ci/pipelines/lib/disconnected/plugins.sh index 7bda833c62..59a265e241 100644 --- a/.ci/pipelines/lib/disconnected/plugins.sh +++ b/.ci/pipelines/lib/disconnected/plugins.sh @@ -111,8 +111,7 @@ disconnected::write_digest_plugin_list() { | grep -E '@sha256:[0-9a-f]+' >> "${list_file}" || true if [[ -f "${data_dir}/dynamic-plugins.default.yaml" ]]; then - grep -oE 'oci://[^[:space:]]+@sha256:[0-9a-f]+[^[:space:]]*' \ - "${data_dir}/dynamic-plugins.default.yaml" >> "${list_file}" || true + grep -oE 'oci://[^[:space:]]+@sha256:[0-9a-f]+[^[:space:]]*' "${data_dir}/dynamic-plugins.default.yaml" >> "${list_file}" || true # notsecret fi # Deduplicate; strip !package suffix for skopeo (mirror-plugins accepts either). diff --git a/.ci/pipelines/resources/config_map/app-config-rhdh-rbac.yaml b/.ci/pipelines/resources/config_map/app-config-rhdh-rbac.yaml index 91caaaf81a..91138778f6 100644 --- a/.ci/pipelines/resources/config_map/app-config-rhdh-rbac.yaml +++ b/.ci/pipelines/resources/config_map/app-config-rhdh-rbac.yaml @@ -59,7 +59,7 @@ proxy: # Other Proxies # customize developer hub instance "/developer-hub": - target: ${DH_TARGET_URL} + target: http://${DH_TARGET_URL} changeOrigin: true # Change to "false" in case of using self hosted cluster with a self-signed certificate secure: false diff --git a/.ci/pipelines/resources/config_map/app-config-rhdh.yaml b/.ci/pipelines/resources/config_map/app-config-rhdh.yaml index b0b552205b..88acfe3307 100644 --- a/.ci/pipelines/resources/config_map/app-config-rhdh.yaml +++ b/.ci/pipelines/resources/config_map/app-config-rhdh.yaml @@ -114,6 +114,12 @@ proxy: # endpoints: {} endpoints: # Other Proxies + # customize developer hub instance + "/developer-hub": + target: http://${DH_TARGET_URL} + changeOrigin: true + # Change to "false" in case of using self hosted cluster with a self-signed certificate + secure: false "/quay/api": target: https://quay.io/ headers: diff --git a/.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml b/.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml index 9ff61e01d7..9ac8cd4b98 100644 --- a/.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml +++ b/.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml @@ -1,208 +1,231 @@ +# RHDH serves the Backstage new frontend system (packages/app) by default, which reads +# `app.extensions` below. See docs/dynamic-plugins/migrating-config-to-new-frontend-system.md +# for the full OFS -> NFS config mapping and known RHDH-specific gaps. +app: + extensions: + # Home page (red-hat-developer-hub.backstage-plugin-dynamic-home-page NFS export). + # `page:home` is disabled by default upstream (@backstage/plugin-home) and must be + # explicitly enabled — see the plugin's README ("New Frontend System" section) — + # otherwise "/" 404s even though the plugin's widget/layout extensions load fine via + # Module Federation. NFS home-page cards are a fixed set of built-in widgets + # (home-page-widget:home/*) auto-enabled by the plugin — arbitrary cards + # (Placeholder/Markdown/JokeCard/Headline) that the old OFS mountPoints used to add + # have no NFS equivalent, so home-page-customization.spec.ts only asserts the + # widgets that still exist: Quick Access, Featured Docs, Recently Visited, Top + # Visited. Search and Starred Catalog Entities are also still-existing widgets; + # they're asserted by guest-signin-happy-path.spec.ts instead. + - page:home: + config: + path: / + - api:home/visits: true + - app-root-element:home/visit-listener: true + # Global header: wrapper + translations attach to the core `app` plugin and must be + # explicitly enabled. The plugin's own NFS extensions (gh-component:global-header/*, + # gh-menu-item:global-header/*) are enabled by default — no pluginConfig needed. + - app-root-wrapper:app/global-header + - translation:app/global-header-translations + # NFS AppLanguageApi reads availableLanguages here — not from i18n.locales in + # app-config-rhdh.yaml. Without this, UserSettingsLanguageToggle hides itself + # (languages.length <= 1). + - api:app/app-language: + config: + availableLanguages: [en, de, es, fr, it, ja] + defaultLanguage: en + # Backstage 1.54 added home-page-widget:search/search-bar on @backstage/plugin-search. + # The RHDH homepage plugin already provides home-page-widget:home/search-bar; leaving + # both enabled duplicates the Search input and fails guest-signin-happy-path.spec.ts. + - home-page-widget:search/search-bar: false + # customizable: false so `priority` actually governs ordering (with customizable: + # true, the layout extension keeps the plugin-registration order and ignores + # priority entirely). Keys must match each widget's `params.name` (not its + # blueprint `name`) — see + # rhdh-plugins/workspaces/homepage/plugins/homepage/src/alpha/extensions/homePageCards.tsx. + # Breakpoints mirror app-config.local.yaml so the default NFS widgets render in a + # stable layout for home-page-customization.spec.ts and guest-signin-happy-path.spec.ts. + - home-page-layout:home/dynamic-homepage-layout: + config: + customizable: false + widgetLayout: + "Red Hat Developer Hub - Explore templates": + priority: 300 + breakpoints: + xl: + w: 12 + h: 5 + lg: + w: 12 + h: 5 + md: + w: 12 + h: 5 + sm: + w: 12 + h: 5 + xs: + w: 12 + h: 7.5 + xxs: + w: 12 + h: 13.5 + "Red Hat Developer Hub - Software Catalog": + priority: 200 + breakpoints: + xl: + w: 12 + h: 7 + lg: + w: 12 + h: 7 + md: + w: 12 + h: 8 + sm: + w: 12 + h: 9 + xs: + w: 12 + h: 11 + xxs: + w: 12 + h: 15 + "Red Hat Developer Hub - Onboarding": + priority: 100 + breakpoints: + xl: + w: 12 + h: 6 + lg: + w: 12 + h: 6 + md: + w: 12 + h: 7 + sm: + w: 12 + h: 8 + xs: + w: 12 + h: 9 + xxs: + w: 12 + h: 14 + "Quick Access Card": + priority: 90 + breakpoints: + xl: + w: 6 + h: 8 + x: 6 + lg: + w: 6 + h: 8 + x: 6 + md: + w: 6 + h: 8 + x: 6 + sm: + w: 12 + h: 8 + x: 6 + xs: + w: 12 + h: 8 + x: 6 + xxs: + w: 12 + h: 8 + x: 6 + "Featured docs": + priority: 80 + breakpoints: + xl: + w: 6 + h: 4 + lg: + w: 6 + h: 4 + md: + w: 6 + h: 4 + sm: + w: 12 + h: 4 + xs: + w: 12 + h: 4 + xxs: + w: 12 + h: 4 + "Catalog starred": + priority: 70 + breakpoints: + xl: + w: 6 + h: 4 + lg: + w: 6 + h: 4 + md: + w: 6 + h: 4 + sm: + w: 12 + h: 4 + xs: + w: 12 + h: 4 + xxs: + w: 12 + h: 4 + "Recently visited": + priority: 60 + breakpoints: + xl: + w: 6 + h: 4 + lg: + w: 6 + h: 4 + md: + w: 6 + h: 4 + sm: + w: 12 + h: 4 + xs: + w: 12 + h: 4 + xxs: + w: 12 + h: 4 + "Top visited": + priority: 50 + breakpoints: + xl: + w: 6 + h: 4 + lg: + w: 6 + h: 4 + md: + w: 6 + h: 4 + sm: + w: 12 + h: 4 + xs: + w: 12 + h: 4 + xxs: + w: 12 + h: 4 + # Nested sidebar menu groups (e.g. "References" > APIs/Learning Paths, "Favorites" > + # Docs) have no NFS equivalent: the app's sidebar + # (packages/app/src/modules/nav/Sidebar.tsx) is a fixed, code-defined + # NavContentBlueprint (Search / Menu / Settings groups, with Catalog and Create + # pinned and everything else flat, sorted by title) — it does not read menuItems + # config at all. See sidebar.spec.ts for the flat-nav assertions this drives + # instead. dynamicPlugins: rootDirectory: dynamic-plugins-root - frontend: - red-hat-developer-hub.backstage-plugin-homepage: - mountPoints: - - mountPoint: application/listener - importName: VisitListener - - mountPoint: home.page/cards - importName: SearchBar - config: - layouts: - xl: { w: 10, h: 1, x: 1 } - lg: { w: 10, h: 1, x: 1 } - md: { w: 10, h: 1, x: 1 } - sm: { w: 10, h: 1, x: 1 } - xs: { w: 12, h: 1 } - xxs: { w: 12, h: 1 } - - mountPoint: home.page/cards - importName: QuickAccessCard - config: - layouts: - xl: { w: 7, h: 8 } - lg: { w: 7, h: 8 } - md: { w: 7, h: 8 } - sm: { w: 12, h: 8 } - xs: { w: 12, h: 8 } - xxs: { w: 12, h: 8 } - - mountPoint: home.page/cards - importName: CatalogStarredEntitiesCard - config: - layouts: - xl: { w: 5, h: 4, x: 7 } - lg: { w: 5, h: 4, x: 7 } - md: { w: 5, h: 4, x: 7 } - sm: { w: 12, h: 4 } - xs: { w: 12, h: 4 } - xxs: { w: 12, h: 4 } - - - mountPoint: home.page/cards - importName: Headline - config: - layouts: - xl: { w: 12, h: 1 } - lg: { w: 12, h: 1 } - md: { w: 12, h: 1 } - sm: { w: 12, h: 1 } - xs: { w: 12, h: 1 } - xxs: { w: 12, h: 1 } - props: - title: Placeholder tests - align: center - - - mountPoint: home.page/cards - importName: Placeholder - config: - layouts: - xl: { x: 1, y: 0, w: 10, h: 1 } - lg: { x: 1, y: 0, w: 10, h: 1 } - md: { x: 1, y: 0, w: 10, h: 1 } - sm: { x: 0, y: 0, w: 12, h: 1 } - xs: { x: 0, y: 0, w: 12, h: 1 } - xxs: { x: 0, y: 0, w: 12, h: 1 } - props: - showBorder: true - debugContent: Home page customization test 1 - - mountPoint: home.page/cards - importName: Placeholder - config: - layouts: - xl: { x: 0, y: 0, w: 7, h: 4 } - lg: { x: 0, y: 0, w: 7, h: 4 } - md: { x: 0, y: 0, w: 7, h: 4 } - sm: { x: 0, y: 0, w: 12, h: 4 } - xs: { x: 0, y: 0, w: 12, h: 4 } - xxs: { x: 0, y: 0, w: 12, h: 4 } - props: - showBorder: true - debugContent: Home page customization test 2 - - mountPoint: home.page/cards - importName: Placeholder - config: - layouts: - xl: { x: 7, y: 0, w: 5, h: 4 } - lg: { x: 7, y: 0, w: 5, h: 4 } - md: { x: 7, y: 0, w: 5, h: 4 } - sm: { x: 0, y: 0, w: 12, h: 4 } - xs: { x: 0, y: 0, w: 12, h: 4 } - xxs: { x: 0, y: 0, w: 12, h: 4 } - props: - showBorder: true - debugContent: Home page customization test 3 - - mountPoint: home.page/cards - importName: Headline - config: - layouts: - xl: { w: 12, h: 1 } - lg: { w: 12, h: 1 } - md: { w: 12, h: 1 } - sm: { w: 12, h: 1 } - xs: { w: 12, h: 1 } - xxs: { w: 12, h: 1 } - props: - title: Markdown tests - align: center - - - mountPoint: home.page/cards - importName: MarkdownCard - config: - layouts: - xl: { w: 6, h: 4 } - lg: { w: 6, h: 4 } - md: { w: 6, h: 4 } - sm: { w: 6, h: 4 } - xs: { w: 6, h: 4 } - xxs: { w: 6, h: 4 } - props: - title: Company links - content: | - ### RHDH - - * [Website](https://developers.redhat.com/rhdh/overview) - * [Documentation](https://docs.redhat.com/en/documentation/red_hat_developer_hub/) - * [GitHub Showcase](https://github.com/redhat-developer/rhdh) - * [GitHub Plugins](https://github.com/janus-idp/backstage-plugins) - - mountPoint: home.page/cards - importName: Markdown - config: - layouts: - xl: { w: 6, h: 4, x: 6 } - lg: { w: 6, h: 4, x: 6 } - md: { w: 6, h: 4, x: 6 } - sm: { w: 6, h: 4, x: 6 } - xs: { w: 6, h: 4, x: 6 } - xxs: { w: 6, h: 4, x: 6 } - props: - title: Important company links - content: | - ### RHDH - - * [Website](https://developers.redhat.com/rhdh/overview) - * [Documentation](https://docs.redhat.com/en/documentation/red_hat_developer_hub/) - * [GitHub Showcase](https://github.com/redhat-developer/rhdh) - * [GitHub Plugins](https://github.com/janus-idp/backstage-plugins) - - mountPoint: home.page/cards - importName: FeaturedDocsCard - - mountPoint: home.page/cards - importName: JokeCard - - mountPoint: home.page/cards - importName: RecentlyVisitedCard - config: - layouts: - xl: { w: 6, h: 4, x: 6 } - lg: { w: 6, h: 4, x: 6 } - md: { w: 6, h: 4, x: 6 } - sm: { w: 6, h: 4, x: 6 } - xs: { w: 6, h: 4, x: 6 } - xxs: { w: 6, h: 4, x: 6 } - - mountPoint: home.page/cards - importName: TopVisitedCard - config: - layouts: - xl: { w: 6, h: 4 } - lg: { w: 6, h: 4 } - md: { w: 6, h: 4 } - sm: { w: 6, h: 4 } - xs: { w: 6, h: 4 } - xxs: { w: 6, h: 4 } - - default.main-menu-items: - menuItems: - default.list: - title: References - icon: bookmarks - default.apis: - parent: default.list - default.learning-path: - parent: default.list - default.test_enabled: - title: Test enabled - enabled: true - default.test_nested_enabled: - parent: default.test_enabled - title: Test nested enabled - enabled: true - default.test_nested_disabled: - parent: default.test_enabled - title: Test nested disabled - enabled: false - default.test_disabled: - title: Test disabled - enabled: false - backstage.plugin-techdocs: - menuItems: - favorites: - title: Favorites - icon: star - priority: 10 - docs: - parent: favorites - priority: 1 - test_i_enabled: - title: Test_i enabled - priority: 20 - enabled: true - test_i_disabled: - title: Test_i disabled - priority: 20 - enabled: false diff --git a/.ci/pipelines/resources/rhdh-operator/rhdh-start-rbac.yaml b/.ci/pipelines/resources/rhdh-operator/rhdh-start-rbac.yaml index e83e7e07d7..b1631de4c8 100644 --- a/.ci/pipelines/resources/rhdh-operator/rhdh-start-rbac.yaml +++ b/.ci/pipelines/resources/rhdh-operator/rhdh-start-rbac.yaml @@ -28,6 +28,7 @@ spec: appConfig: configMaps: - name: app-config-rhdh + - name: dynamic-plugins-config mountPath: /opt/app-root/src dynamicPluginsConfigMapName: dynamic-plugins extraEnvs: diff --git a/.ci/pipelines/resources/rhdh-operator/rhdh-start-rbac_K8s.yaml b/.ci/pipelines/resources/rhdh-operator/rhdh-start-rbac_K8s.yaml index 4bbfbaa506..b9a52a88cb 100644 --- a/.ci/pipelines/resources/rhdh-operator/rhdh-start-rbac_K8s.yaml +++ b/.ci/pipelines/resources/rhdh-operator/rhdh-start-rbac_K8s.yaml @@ -33,6 +33,7 @@ spec: appConfig: configMaps: - name: app-config-rhdh + - name: dynamic-plugins-config mountPath: /opt/app-root/src dynamicPluginsConfigMapName: dynamic-plugins extraEnvs: diff --git a/.ci/pipelines/value_files/values_showcase-rbac.yaml b/.ci/pipelines/value_files/values_showcase-rbac.yaml index edbacc71f8..9a2ba9d51c 100644 --- a/.ci/pipelines/value_files/values_showcase-rbac.yaml +++ b/.ci/pipelines/value_files/values_showcase-rbac.yaml @@ -213,6 +213,8 @@ upstream: extraAppConfig: - configMapRef: app-config-rhdh filename: app-config-rhdh.yaml + - configMapRef: dynamic-plugins-config + filename: dynamic-plugins-config.yaml extraEnvVarsSecrets: - rhdh-secrets - postgres-cred diff --git a/.ci/pipelines/value_files/values_showcase.yaml b/.ci/pipelines/value_files/values_showcase.yaml index 72bd503729..f7f2859620 100644 --- a/.ci/pipelines/value_files/values_showcase.yaml +++ b/.ci/pipelines/value_files/values_showcase.yaml @@ -16,27 +16,6 @@ global: enabled: true - package: ref://backstage-plugin-catalog-backend-module-github enabled: true - - package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-application-provider-test:bs_1.45.3__0.6.0 - pluginConfig: - dynamicPlugins: - frontend: - red-hat-developer-hub.backstage-plugin-application-provider-test: - dynamicRoutes: - - path: /application-provider-test-page - importName: TestPage - mountPoints: - - mountPoint: application/provider - importName: TestProviderOne - - mountPoint: application/provider - importName: TestProviderTwo - - package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-application-listener-test:bs_1.45.3__0.6.0 - pluginConfig: - dynamicPlugins: - frontend: - red-hat-developer-hub.backstage-plugin-application-listener-test: - mountPoints: - - mountPoint: application/listener - importName: LocationListener - package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/roadiehq-backstage-plugin-github-pull-requests:bs_1.49.4__3.7.0" enabled: true - package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-plugin-auth-backend-module-oidc-provider:bs_1.52.0__0.4.17" diff --git a/.github/actions/resolve-catalog-index-image/action.yaml b/.github/actions/resolve-catalog-index-image/action.yaml new file mode 100644 index 0000000000..49d5f292af --- /dev/null +++ b/.github/actions/resolve-catalog-index-image/action.yaml @@ -0,0 +1,35 @@ +name: "Resolve catalog index image" +description: "Resolves quay.io/rhdh/plugin-catalog-index from branch (next or release-*) or an override input" + +inputs: + branch: + description: "Branch to derive the index tag from (release-* -> version tag, else next)" + required: true + catalog_index_image: + description: "Optional full image override (validated before use)" + required: false + default: "" + +outputs: + image: + description: "Resolved catalog index image reference" + value: ${{ steps.resolve.outputs.image }} + +runs: + using: "composite" + steps: + - name: Resolve the catalog index image + id: resolve + shell: bash + env: + BRANCH: ${{ inputs.branch }} + OVERRIDE: ${{ inputs.catalog_index_image }} + run: | + image="$(./e2e-tests/local-harness/resolve-catalog-index-image.sh "$BRANCH" "$OVERRIDE")" + echo "image=${image}" >> "$GITHUB_OUTPUT" + # The tag is a moving target; record the digest it resolved to so a + # later run can be traced back to the exact index it validated. + digest=$(skopeo inspect --no-creds --override-os linux --override-arch amd64 \ + --format '{{.Digest}}' "docker://${image}" 2> /dev/null || echo "unresolved") + echo "Catalog index: ${image} (${digest})" + echo "Catalog index: \`${image}\` @ \`${digest}\`" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/e2e-cluster-free.yaml b/.github/workflows/e2e-cluster-free.yaml index 6aa67e877d..dded7789e1 100644 --- a/.github/workflows/e2e-cluster-free.yaml +++ b/.github/workflows/e2e-cluster-free.yaml @@ -1,8 +1,8 @@ name: E2E Cluster-free # Runs the cluster-free local E2E harness (RHIDP-13501 / RHIDP-15075): boots the -# backend and the legacy app dev server in-process and drives Playwright against -# them, with dynamic plugins installed from the public OCI registry (ghcr) via +# backend and the NFS app (packages/app) dev server in-process and drives Playwright +# against them, with dynamic plugins installed from the public OCI registry (quay.io) via # the install-dynamic-plugins CLI (skopeo). No OpenShift/Kubernetes cluster or # image. See docs/e2e-tests/local-e2e-harness.md. @@ -15,6 +15,8 @@ on: - "packages/app/package.json" - ".github/workflows/e2e-cluster-free.yaml" - ".github/actions/setup-cluster-free-harness/**" + - ".github/actions/resolve-catalog-index-image/**" + - "e2e-tests/local-harness/resolve-catalog-index-image.sh" push: branches: - 'main' @@ -26,6 +28,8 @@ on: - "packages/app/package.json" - ".github/workflows/e2e-cluster-free.yaml" - ".github/actions/setup-cluster-free-harness/**" + - ".github/actions/resolve-catalog-index-image/**" + - "e2e-tests/local-harness/resolve-catalog-index-image.sh" # The catalog index is built outside this repo and changes on its own, so the # plugin sanity job needs a trigger that is not tied to a commit here. GitHub # only runs `schedule` from the default branch, so this covers the `next` @@ -76,21 +80,30 @@ jobs: - name: Set up the cluster-free harness uses: ./.github/actions/setup-cluster-free-harness + - name: Resolve the catalog index image + id: index + uses: ./.github/actions/resolve-catalog-index-image + with: + branch: ${{ github.event.pull_request.base.ref || github.ref_name }} + catalog_index_image: ${{ inputs.catalog_index_image }} + - name: Populate dynamic-plugins-root (OCI) - # install-dynamic-plugins pulls the harness plugin set from the public OCI - # registry (ghcr) via skopeo. + # populate.sh resolves {{inherit}} against the catalog index DPDY, then + # installs the harness plugin set from quay.io. + env: + CATALOG_INDEX_IMAGE: ${{ steps.index.outputs.image }} run: ./e2e-tests/local-harness/populate.sh - - name: Run cluster-free E2E (legacy app) + - name: Run cluster-free E2E (NFS app) working-directory: ./e2e-tests - run: yarn e2e:legacy-local + run: yarn e2e:local - name: Upload Playwright report if: ${{ !cancelled() }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: playwright-report-legacy-local - path: e2e-tests/playwright-report-legacy-local + name: playwright-report-local + path: e2e-tests/playwright-report-local retention-days: 7 plugin-sanity: @@ -116,31 +129,10 @@ jobs: - name: Resolve the catalog index image id: index - env: - # `next` tracks main; release branches carry their own version tag. - BRANCH: ${{ github.event.pull_request.base.ref || github.ref_name }} - OVERRIDE: ${{ inputs.catalog_index_image }} - run: | - if [[ -n "${OVERRIDE}" ]]; then - # Shape-check the dispatch input: it reaches GITHUB_OUTPUT, and a - # multi-line value there can forge additional step outputs. - if [[ ! "${OVERRIDE}" =~ ^[A-Za-z0-9._/-]+(:[A-Za-z0-9._-]+)?(@sha256:[a-f0-9]{64})?$ ]]; then - echo "invalid catalog_index_image: ${OVERRIDE}" >&2 - exit 1 - fi - image="${OVERRIDE}" - elif [[ "${BRANCH}" == release-* ]]; then - image="quay.io/rhdh/plugin-catalog-index:${BRANCH#release-}" - else - image="quay.io/rhdh/plugin-catalog-index:next" - fi - echo "image=${image}" >> "$GITHUB_OUTPUT" - # The tag is a moving target; record the digest it resolved to so a - # later run can be traced back to the exact index it validated. - digest=$(skopeo inspect --no-creds --override-os linux --override-arch amd64 \ - --format '{{.Digest}}' "docker://${image}" 2> /dev/null || echo "unresolved") - echo "Catalog index: ${image} (${digest})" - echo "Catalog index: \`${image}\` @ \`${digest}\`" >> "$GITHUB_STEP_SUMMARY" + uses: ./.github/actions/resolve-catalog-index-image + with: + branch: ${{ github.event.pull_request.base.ref || github.ref_name }} + catalog_index_image: ${{ inputs.catalog_index_image }} - name: Populate dynamic-plugins-root from the catalog index env: diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index c23c69e06d..b3ebea9ea2 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -126,7 +126,7 @@ jobs: - name: Regenerate dockerfiles if: ${{ steps.check-image.outputs.is_skipped != 'true' }} run: | - yarn run build:dockerfile; if [[ $(git diff --name-only | grep Dockerfile || true) != "" ]]; then \ + yarn run build:dockerfile; if [[ $(git diff --name-only | grep -E 'Dockerfile|Containerfile' || true) != "" ]]; then \ echo "ERROR: Workspace is dirty! Must run 'yarn build:dockerfile' and commit changes!"; exit 1; \ fi diff --git a/.gitignore b/.gitignore index d006ecde54..92c6bf62bb 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,8 @@ dynamic-plugins-root/* !dynamic-plugins-root/.gitkeep # install-dynamic-plugins config copied to the repo root by e2e-tests/local-harness/populate.sh /dynamic-plugins.yaml +# catalog-index DPDY extracted by e2e-tests/local-harness/populate.sh when CATALOG_INDEX_IMAGE is set +e2e-tests/local-harness/.generated/ #dev caches .webpack-cache diff --git a/app-config.local-e2e.yaml b/app-config.local-e2e.yaml index e011e6fe4e..8e37e7ede8 100644 --- a/app-config.local-e2e.yaml +++ b/app-config.local-e2e.yaml @@ -1,9 +1,9 @@ -# Config overlay for the cluster-free local E2E harness (legacy `packages/app`, Tier B). +# Config overlay for the cluster-free local E2E harness (NFS `packages/app`, Tier B). # # Layered on top of app-config.yaml and app-config.dynamic-plugins.yaml to run # Playwright E2E without an OpenShift/Kubernetes cluster or container images: # -# yarn --cwd e2e-tests e2e:legacy-local +# yarn --cwd e2e-tests e2e:local # # It enables guest sign-in (the auth backend rejects guest unless a provider is # configured) and pins the in-memory SQLite database so a single `run` is fully @@ -63,212 +63,234 @@ catalog: rules: - allow: [User, Group] -# The e2e specs are written against the CI deployment's customization -# (.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml): APIs and -# Learning Paths nest under a "References" sidebar group, and the home page -# carries the extra test cards (Placeholder/Markdown/Featured Docs/Random -# Joke/Top + Recently Visited). Mirror those blocks here so the specs pass -# off-cluster. The home-page mountPoints array REPLACES the one from the -# static app-config.dynamic-plugins.yaml (arrays substitute on config merge), -# so it is the CI list verbatim, superset of the static one. -dynamicPlugins: - frontend: - default.main-menu-items: - menuItems: - default.list: - title: References - icon: bookmarks - default.apis: - parent: default.list - default.learning-path: - parent: default.list - default.test_enabled: - title: Test enabled - enabled: true - default.test_nested_enabled: - parent: default.test_enabled - title: Test nested enabled - enabled: true - default.test_nested_disabled: - parent: default.test_enabled - title: Test nested disabled - enabled: false - default.test_disabled: - title: Test disabled - enabled: false - backstage.plugin-techdocs: - menuItems: - favorites: - title: Favorites - icon: star - priority: 10 - docs: - parent: favorites - priority: 1 - test_i_enabled: - title: Test_i enabled - priority: 20 - enabled: true - test_i_disabled: - title: Test_i disabled - priority: 20 - enabled: false - red-hat-developer-hub.backstage-plugin-homepage: - mountPoints: - - mountPoint: application/listener - importName: VisitListener - - mountPoint: home.page/cards - importName: SearchBar - config: - layouts: - xl: { w: 10, h: 1, x: 1 } - lg: { w: 10, h: 1, x: 1 } - md: { w: 10, h: 1, x: 1 } - sm: { w: 10, h: 1, x: 1 } - xs: { w: 12, h: 1 } - xxs: { w: 12, h: 1 } - - mountPoint: home.page/cards - importName: QuickAccessCard - config: - layouts: - xl: { w: 7, h: 8 } - lg: { w: 7, h: 8 } - md: { w: 7, h: 8 } - sm: { w: 12, h: 8 } - xs: { w: 12, h: 8 } - xxs: { w: 12, h: 8 } - - mountPoint: home.page/cards - importName: CatalogStarredEntitiesCard - config: - layouts: - xl: { w: 5, h: 4, x: 7 } - lg: { w: 5, h: 4, x: 7 } - md: { w: 5, h: 4, x: 7 } - sm: { w: 12, h: 4 } - xs: { w: 12, h: 4 } - xxs: { w: 12, h: 4 } - - mountPoint: home.page/cards - importName: Headline - config: - layouts: - xl: { w: 12, h: 1 } - lg: { w: 12, h: 1 } - md: { w: 12, h: 1 } - sm: { w: 12, h: 1 } - xs: { w: 12, h: 1 } - xxs: { w: 12, h: 1 } - props: - title: Placeholder tests - align: center - - mountPoint: home.page/cards - importName: Placeholder - config: - layouts: - xl: { x: 1, y: 0, w: 10, h: 1 } - lg: { x: 1, y: 0, w: 10, h: 1 } - md: { x: 1, y: 0, w: 10, h: 1 } - sm: { x: 0, y: 0, w: 12, h: 1 } - xs: { x: 0, y: 0, w: 12, h: 1 } - xxs: { x: 0, y: 0, w: 12, h: 1 } - props: - showBorder: true - debugContent: Home page customization test 1 - - mountPoint: home.page/cards - importName: Placeholder - config: - layouts: - xl: { x: 0, y: 0, w: 7, h: 4 } - lg: { x: 0, y: 0, w: 7, h: 4 } - md: { x: 0, y: 0, w: 7, h: 4 } - sm: { x: 0, y: 0, w: 12, h: 4 } - xs: { x: 0, y: 0, w: 12, h: 4 } - xxs: { x: 0, y: 0, w: 12, h: 4 } - props: - showBorder: true - debugContent: Home page customization test 2 - - mountPoint: home.page/cards - importName: Placeholder - config: - layouts: - xl: { x: 7, y: 0, w: 5, h: 4 } - lg: { x: 7, y: 0, w: 5, h: 4 } - md: { x: 7, y: 0, w: 5, h: 4 } - sm: { x: 0, y: 0, w: 12, h: 4 } - xs: { x: 0, y: 0, w: 12, h: 4 } - xxs: { x: 0, y: 0, w: 12, h: 4 } - props: - showBorder: true - debugContent: Home page customization test 3 - - mountPoint: home.page/cards - importName: Headline - config: - layouts: - xl: { w: 12, h: 1 } - lg: { w: 12, h: 1 } - md: { w: 12, h: 1 } - sm: { w: 12, h: 1 } - xs: { w: 12, h: 1 } - xxs: { w: 12, h: 1 } - props: - title: Markdown tests - align: center - - mountPoint: home.page/cards - importName: MarkdownCard - config: - layouts: - xl: { w: 6, h: 4 } - lg: { w: 6, h: 4 } - md: { w: 6, h: 4 } - sm: { w: 6, h: 4 } - xs: { w: 6, h: 4 } - xxs: { w: 6, h: 4 } - props: - title: Company links - content: | - ### RHDH - - * [Website](https://developers.redhat.com/rhdh/overview) - * [Documentation](https://docs.redhat.com/en/documentation/red_hat_developer_hub/) - * [GitHub Showcase](https://github.com/redhat-developer/rhdh) - * [GitHub Plugins](https://github.com/janus-idp/backstage-plugins) - - mountPoint: home.page/cards - importName: Markdown - config: - layouts: - xl: { w: 6, h: 4, x: 6 } - lg: { w: 6, h: 4, x: 6 } - md: { w: 6, h: 4, x: 6 } - sm: { w: 6, h: 4, x: 6 } - xs: { w: 6, h: 4, x: 6 } - xxs: { w: 6, h: 4, x: 6 } - props: - title: Important company links - content: | - ### RHDH - - * [Website](https://developers.redhat.com/rhdh/overview) - * [Documentation](https://docs.redhat.com/en/documentation/red_hat_developer_hub/) - * [GitHub Showcase](https://github.com/redhat-developer/rhdh) - * [GitHub Plugins](https://github.com/janus-idp/backstage-plugins) - - mountPoint: home.page/cards - importName: FeaturedDocsCard - - mountPoint: home.page/cards - importName: RecentlyVisitedCard - config: - layouts: - xl: { w: 6, h: 4, x: 6 } - lg: { w: 6, h: 4, x: 6 } - md: { w: 6, h: 4, x: 6 } - sm: { w: 6, h: 4, x: 6 } - xs: { w: 6, h: 4, x: 6 } - xxs: { w: 6, h: 4, x: 6 } - - mountPoint: home.page/cards - importName: TopVisitedCard - config: - layouts: - xl: { w: 6, h: 4 } - lg: { w: 6, h: 4 } - md: { w: 6, h: 4 } - sm: { w: 6, h: 4 } - xs: { w: 6, h: 4 } - xxs: { w: 6, h: 4 } +# The app (NFS, default) reads `app.extensions` below, mirroring the CI ConfigMap +# (.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml): the home page route +# plus a customized home-page widget layout. `page:home` is disabled by default upstream +# (@backstage/plugin-home) and must be explicitly enabled — see the +# dynamic-home-page plugin's README ("New Frontend System" section) — otherwise "/" 404s +# even though the plugin's widget/layout extensions load fine via Module Federation. NFS +# home-page cards are a fixed set of built-in widgets (home-page-widget:home/*) — +# arbitrary cards like the old OFS Placeholder/Markdown/JokeCard/Headline mount points +# have no NFS equivalent, and nested sidebar menu groups (References/Favorites) have no +# NFS equivalent either (the sidebar is a fixed NavContentBlueprint in +# packages/app/src/modules/nav/Sidebar.tsx, not menuItems-config-driven). See +# home-page-customization.spec.ts and sidebar.spec.ts for the assertions this drives — +# guest-signin-happy-path.spec.ts additionally covers the Search and Starred Catalog +# Entities widgets. +app: + extensions: + - page:home: + config: + path: / + - api:home/visits: true + # app-root-element:home/visit-listener (also enabled in the CI ConfigMap) is + # deliberately NOT enabled here. It calls VisitsStorageApi.save() — an unguarded + # backend call — on every route change, which 404s because userSettings.persistence: + # browser (above) means the backend never loads @backstage/plugin-user-settings-backend. + # In a production build that 404 is just a background error; under this harness's dev + # server (backstage-cli package start), the resulting unhandled rejection triggers + # webpack's react-refresh error overlay, which is a full-screen transparent iframe that + # then intercepts every click for the rest of the test. api:home/visits above is enough + # to keep the Top/Recently Visited home-page widgets rendering (just always empty here). + # The global-header plugin's own default extensions (gh-component:global-header/*, + # gh-menu-item:global-header/*) are enabled by default — see + # e2e-tests/local-harness/dynamic-plugins.yaml. Only the wrapper + translation + # module need explicit enabling here, since they attach to the core `app` + # plugin rather than being scoped to the global-header plugin itself. + - app-root-wrapper:app/global-header + - translation:app/global-header-translations + # NFS AppLanguageApi reads availableLanguages here — not from i18n.locales above. + # Without this, UserSettingsLanguageToggle hides itself (languages.length <= 1). + - api:app/app-language: + config: + availableLanguages: [en, de, es, fr, it, ja] + defaultLanguage: en + # Backstage 1.54 added home-page-widget:search/search-bar on @backstage/plugin-search. + # The RHDH homepage plugin already provides home-page-widget:home/search-bar; leaving + # both enabled duplicates the Search input and fails guest-signin-happy-path.spec.ts. + - home-page-widget:search/search-bar: false + # customizable: false so `priority` actually governs ordering (with customizable: + # true, the layout extension keeps the plugin-registration order and ignores + # priority entirely). Keys must match each widget's `params.name` (not its blueprint + # `name`) — see + # rhdh-plugins/workspaces/homepage/plugins/homepage/src/alpha/extensions/homePageCards.tsx. + # Breakpoints mirror app-config.local.yaml / the CI ConfigMap so default NFS widgets + # render in a stable layout for home-page-customization.spec.ts. + - home-page-layout:home/dynamic-homepage-layout: + config: + customizable: false + widgetLayout: + "Red Hat Developer Hub - Explore templates": + priority: 300 + breakpoints: + xl: + w: 12 + h: 5 + lg: + w: 12 + h: 5 + md: + w: 12 + h: 5 + sm: + w: 12 + h: 5 + xs: + w: 12 + h: 7.5 + xxs: + w: 12 + h: 13.5 + "Red Hat Developer Hub - Software Catalog": + priority: 200 + breakpoints: + xl: + w: 12 + h: 7 + lg: + w: 12 + h: 7 + md: + w: 12 + h: 8 + sm: + w: 12 + h: 9 + xs: + w: 12 + h: 11 + xxs: + w: 12 + h: 15 + "Red Hat Developer Hub - Onboarding": + priority: 100 + breakpoints: + xl: + w: 12 + h: 6 + lg: + w: 12 + h: 6 + md: + w: 12 + h: 7 + sm: + w: 12 + h: 8 + xs: + w: 12 + h: 9 + xxs: + w: 12 + h: 14 + "Quick Access Card": + priority: 90 + breakpoints: + xl: + w: 6 + h: 8 + x: 6 + lg: + w: 6 + h: 8 + x: 6 + md: + w: 6 + h: 8 + x: 6 + sm: + w: 12 + h: 8 + x: 6 + xs: + w: 12 + h: 8 + x: 6 + xxs: + w: 12 + h: 8 + x: 6 + "Featured docs": + priority: 80 + breakpoints: + xl: + w: 6 + h: 4 + lg: + w: 6 + h: 4 + md: + w: 6 + h: 4 + sm: + w: 12 + h: 4 + xs: + w: 12 + h: 4 + xxs: + w: 12 + h: 4 + "Catalog starred": + priority: 70 + breakpoints: + xl: + w: 6 + h: 4 + lg: + w: 6 + h: 4 + md: + w: 6 + h: 4 + sm: + w: 12 + h: 4 + xs: + w: 12 + h: 4 + xxs: + w: 12 + h: 4 + "Recently visited": + priority: 60 + breakpoints: + xl: + w: 6 + h: 4 + lg: + w: 6 + h: 4 + md: + w: 6 + h: 4 + sm: + w: 12 + h: 4 + xs: + w: 12 + h: 4 + xxs: + w: 12 + h: 4 + "Top visited": + priority: 50 + breakpoints: + xl: + w: 6 + h: 4 + lg: + w: 6 + h: 4 + md: + w: 6 + h: 4 + sm: + w: 12 + h: 4 + xs: + w: 12 + h: 4 + xxs: + w: 12 + h: 4 diff --git a/app-config.yaml b/app-config.yaml index 57a26f086b..ee3a32543e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -1,8 +1,13 @@ -developerHub: - flavor: rhdh app: title: Red Hat Developer Hub baseUrl: http://localhost:3000 + extensions: + # @backstage/plugin-home disables page:home by default; enable it for the NFS shell. + - page:home: + config: + path: / + # Backstage 1.54 search plugin duplicates RHDH homepage's search bar widget. + - home-page-widget:search/search-bar: false branding: fullLogoWidth: ${FULL_LOGO_WIDTH} theme: diff --git a/build/containerfiles/Containerfile b/build/containerfiles/Containerfile index 6e04b04708..56aa272a2f 100644 --- a/build/containerfiles/Containerfile +++ b/build/containerfiles/Containerfile @@ -69,11 +69,9 @@ COPY $EXTERNAL_SOURCE_NESTED/yarn.lock ./ COPY $EXTERNAL_SOURCE_NESTED/plugins/scalprum-backend/package.json ./plugins/scalprum-backend/package.json COPY $EXTERNAL_SOURCE_NESTED/plugins/licensed-users-info-backend/package.json ./plugins/licensed-users-info-backend/package.json COPY $EXTERNAL_SOURCE_NESTED/plugins/dynamic-plugins-info-backend/package.json ./plugins/dynamic-plugins-info-backend/package.json -COPY $EXTERNAL_SOURCE_NESTED/packages/theme-wrapper/package.json ./packages/theme-wrapper/package.json COPY $EXTERNAL_SOURCE_NESTED/packages/plugin-utils/package.json ./packages/plugin-utils/package.json COPY $EXTERNAL_SOURCE_NESTED/packages/backend/package.json ./packages/backend/package.json COPY $EXTERNAL_SOURCE_NESTED/packages/app/package.json ./packages/app/package.json -COPY $EXTERNAL_SOURCE_NESTED/packages/app-next/package.json ./packages/app-next/package.json COPY $EXTERNAL_SOURCE_NESTED/package.json ./package.json # END COPY package.json files diff --git a/docs/customization.md b/docs/customization.md index a9d90507a2..cfe290d9bd 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -2,10 +2,12 @@ To customize the look of your showcase instance, you can edit the `app-config.yaml` file in the root of this repository. The customizations used to configure the app are loaded by the backstage app at startup, with certain values being queried by the app at runtime. -Plugins can use field `developerHub.flavor` of the `app-config.yaml` to identify which Backstage flavor they are running on (e.g `backstage`, `rhdh` or `rhtap`). +> **NFS default:** RHDH ships the Backstage new frontend system (`packages/app`). For NFS-specific configuration, see [Migrating RHDH Frontend Configuration to the Backstage New Frontend System](dynamic-plugins/migrating-config-to-new-frontend-system.md). Sections below marked **legacy OFS only** do not apply to the default NFS app shell. ## Changing the Sidebar Logo +The NFS app (`packages/app`) renders the sidebar logo in the nav module. Default logo comes from `@red-hat-developer-hub/backstage-plugin-theme` (`LogoFull` / `LogoIcon`). When the global-header plugin is enabled, branding stays in the sidebar; the header company-logo slot is hidden by app shell CSS so it does not duplicate the sidebar logo. + The sidebar uses two logos - one for the expanded sidebar and one for the collapsed sidebar. - To customize the logo for the expanded sidebar, provide a both light and dark mode Base64 encoded images of your logo in the `app.branding.fullLogo` field of the `app-config.yaml`. You may also opt to provide a single Base64 encoded image that will be used for both light and dark modes. @@ -25,20 +27,25 @@ app: ![Default Icon Logo when side bar is collapsed](images/default-collapsed-icon.png) ![Default Full Logo when side bar is expanded](images/default-expanded-logo.png) -## Hiding Sidebar search, logo, settings, administration +## Hiding Sidebar search, settings, administration (legacy OFS only) + +The NFS app (`packages/app`) always shows the sidebar logo. Branding is owned by the sidebar; hiding it is not supported because it would leave an empty gap with no replacement in the header. + +On the legacy OFS app shell, the flags below can hide sidebar chrome. Search and settings are also available from the global-header toolbar when that plugin is installed. ```yaml title="app-config.yaml" app: sidebar: - search: false # optional, when set to false hides sidebar search - logo: false # optional, when set to false hides sidebar logo - settings: false # optional, when set to false hides settings menu item - administration: false # # optional, when set to false hides administration menu item + search: false # optional — hides sidebar search + settings: false # optional — hides settings menu item + administration: false # optional — hides administration menu item ``` -![Hidden Sidebar search, logo, settings, administration](images/sidebar-search-hidden.png) +![Hidden Sidebar search, settings, and administration](images/sidebar-search-hidden.png) -## Customizing the Sidebar Menu Items +## Customizing the Sidebar Menu Items (legacy OFS only — obsolete for NFS) + +> **Obsolete for NFS.** The default NFS app (`packages/app`) uses a code-defined flat sidebar in `packages/app/src/modules/nav/Sidebar.tsx`. The `dynamicPlugins.frontend.default.main-menu-items` configuration below only applied to the legacy OFS app shell and has no effect on NFS. For NFS navigation, see [Migrating RHDH Frontend Configuration to the Backstage New Frontend System](dynamic-plugins/migrating-config-to-new-frontend-system.md). Order and parent-children relationship of sidebar main menu items can be customized using the `dynamicPlugins.frontend.default.main-menu-items.menuItems` field in the `app-config.yaml`. To ensure the menu item is recognized as a main menu item, the key must be prefixed with `default.`. @@ -62,18 +69,9 @@ dynamicPlugins: See [Menu items](dynamic-plugins/frontend-plugin-wiring.md#menu-items) from dynamic-plugins documentation for more details. -See [DefaultMainMenuItems](https://github.com/redhat-developer/rhdh/blob/main/packages/app/src/consts.ts#L1) for a list of main menu items, including their default priorities. - ## Changing the favicon and tab title -Currently, the favicon customization utilizes the same Base64 encoded image as the icon logo provided via `app.branding.iconLogo` in the `app-config.yaml` -Currently the favicon is only loaded once at startup, so if any changes need to be made to the favicon, the app will need to be restarted. - -```yaml title="app-config.yaml" -app: - branding: - iconLogo: ${BASE64_EMBEDDED_ICON_LOGO} -``` +> **Favicon from `iconLogo` (legacy OFS only):** The legacy OFS app shell could update the favicon from `app.branding.iconLogo` at runtime. The NFS app uses static favicon assets in `packages/app/public/` and does not apply `iconLogo` to the tab icon. To customize the tab title, provide a string value to the `app.title` field in the `app-config.yaml`: @@ -443,7 +441,7 @@ proxy: '/developer-hub': target: https://raw.githubusercontent.com/ # i.e https://raw.githubusercontent.com/ pathRewrite: - '^/api/proxy/developer-hub$': .json # i.e /redhat-developer/rhdh/main/packages/app/public/homepage/data.json + '^/api/proxy/developer-hub$': .json # e.g. a hosted JSON file for Quick Access card data changeOrigin: true secure: true ``` diff --git a/docs/dynamic-plugins/frontend-plugin-wiring.md b/docs/dynamic-plugins/frontend-plugin-wiring.md index 138f1cce19..38920ae7da 100644 --- a/docs/dynamic-plugins/frontend-plugin-wiring.md +++ b/docs/dynamic-plugins/frontend-plugin-wiring.md @@ -406,7 +406,9 @@ Each global header entry requires the following attributes: Users can configure multiple global headers at different positions by adding entries to the `mountPoints` field. -### Adding application listeners +### Adding application listeners (legacy OFS only — obsolete for NFS) + +> **Obsolete for NFS.** The `application/listener` mount point was part of the legacy OFS app shell. The default NFS app (`packages/app`) has no application/listener renderer. See [Migrating RHDH Frontend Configuration to the Backstage New Frontend System](migrating-config-to-new-frontend-system.md). The users can add application listeners using the `application/listener` mount point. Below is an example that uses the aforesaid mount point: @@ -422,7 +424,9 @@ dynamicPlugins: Users can configure multiple application listeners by adding entries to the `mountPoints` field. -### Adding application providers +### Adding application providers (legacy OFS only — obsolete for NFS) + +> **Obsolete for NFS.** The `application/provider` mount point was part of the legacy OFS app shell. The default NFS app (`packages/app`) has no application/provider renderer. See [Migrating RHDH Frontend Configuration to the Backstage New Frontend System](migrating-config-to-new-frontend-system.md). The users can add application providers using the `application/provider` mount point. Below is an example that uses the aforesaid mount point to configure a context provider: diff --git a/docs/dynamic-plugins/migrating-config-to-new-frontend-system.md b/docs/dynamic-plugins/migrating-config-to-new-frontend-system.md index ecd586d7f0..e9f982f361 100644 --- a/docs/dynamic-plugins/migrating-config-to-new-frontend-system.md +++ b/docs/dynamic-plugins/migrating-config-to-new-frontend-system.md @@ -6,30 +6,13 @@ This guide helps **operators and platform administrators** customize Red Hat Dev > > **Plugin authors** → [Migrating Plugins to the New Frontend System](migrating-plugins-to-new-frontend-system.md). -## Transition: the new frontend system is not the default yet +## Default: new frontend system (NFS) -RHDH still ships the legacy `app` frontend package by default. The new frontend system lives in the `app-next` package and will become the default after the app-shell switch. Until then, enable **both** of the following on your RHDH **backend** deployment (OpenShift, Helm, Operator, [rhdh-local](https://github.com/redhat-developer/rhdh-local), or any environment where the backend runs as a container): - -| Setting | How to apply | Purpose | -| --- | --- | --- | -| `app.packageName: app-next` | Environment variable `APP_CONFIG_app_packageName=app-next`, **or** in `app-config.yaml` under `app.packageName` | Tells the app backend to serve the `app-next` frontend (new frontend system) instead of `app`. | -| `ENABLE_STANDARD_MODULE_FEDERATION=true` | Environment variable on the backend container only | Enables the backend to serve standard Module Federation assets for dynamic frontend plugins. Without this, RHDH disables that service because the legacy frontend does not use it. | - -Example environment variables for the RHDH backend pod or deployment: - -```bash -APP_CONFIG_app_packageName=app-next -ENABLE_STANDARD_MODULE_FEDERATION=true -``` - -Equivalent `app-config` fragment (you still need `ENABLE_STANDARD_MODULE_FEDERATION` in the environment): - -```yaml -app: - packageName: app-next -``` - -These requirements are temporary. Once RHDH completes the switch to `app-next`, they will become the default and this transition note can be removed. +RHDH ships the Backstage new frontend system (`packages/app`) by default. The backend +serves standard Module Federation assets for dynamic frontend plugins out of the box — +no configuration needed (OpenShift, Helm, Operator, +[rhdh-local](https://github.com/redhat-developer/rhdh-local), or any environment where +the backend runs as a container). ## Who should read this @@ -39,7 +22,7 @@ These requirements are temporary. Once RHDH completes the switch to `app-next`, ## Prerequisites -- RHDH is running with the new frontend system enabled — see [Transition: the new frontend system is not the default yet](#transition-the-new-frontend-system-is-not-the-default-yet) above. +- RHDH is running with the new frontend system — see [Default: new frontend system (NFS)](#default-new-frontend-system-nfs) above. - You understand where your deployment stores `dynamic-plugins.yaml` and `app-config` — see [Installing Plugins](installing-plugins.md) and the [Red Hat product documentation](https://docs.redhat.com/en/documentation/red_hat_developer_hub/) for Helm and Operator paths. - Installed plugins support the new frontend system. Configuration alone cannot add UI that a plugin does not register as an extension. diff --git a/docs/e2e-tests/local-e2e-harness.md b/docs/e2e-tests/local-e2e-harness.md index 5375fa2868..720cf21543 100644 --- a/docs/e2e-tests/local-e2e-harness.md +++ b/docs/e2e-tests/local-e2e-harness.md @@ -7,14 +7,9 @@ backend dynamic-plugin loader from RHIDP-13508. ## Goal Run real Playwright E2E against RHDH **without** an OpenShift/Kubernetes cluster or -container images — a single `run` that boots the backend and the legacy frontend dev +container images — a single `run` that boots the backend and the NFS frontend dev server in-process and drives a browser against them. -The harness targets the legacy frontend (`packages/app`, Tier B): it is what RHDH ships -today, and **the existing Playwright specs already target it**, so they run unmodified. -Dynamic frontend plugins load through Scalprum exactly as in-cluster (the legacy -`scalprum-backend` serves the plugin config by default). - The guest-auth + in-memory-SQLite overlay `app-config.local-e2e.yaml` is layered on top of `app-config.yaml`. Guest sign-in must be configured explicitly — the auth backend otherwise rejects guest with _"you must … configure the auth backend to support guest @@ -23,13 +18,22 @@ sign in."_ ### 1. Populate `dynamic-plugins-root` (one-time) Run the same script CI uses — it installs the harness plugin set -(`e2e-tests/local-harness/dynamic-plugins.yaml`) from the public OCI registry (ghcr) -via `install-dynamic-plugins` + skopeo, pinned to the same CLI version as CI. No -source build needed; works from a fresh clone. Requires skopeo (preinstalled in CI; -`brew install skopeo` on macOS): +(`e2e-tests/local-harness/dynamic-plugins.yaml`) from the public OCI registry (quay.io) +via `install-dynamic-plugins` + skopeo, with `{{inherit}}` resolved against +`dynamic-plugins.default.yaml` extracted from a catalog-index OCI image. +**`CATALOG_INDEX_IMAGE` is required** to extract the full catalog DPDY from a catalog-index OCI image +`dynamic-plugins.default.yaml`. Pinned to the same CLI version as CI. No source build +needed; works from a fresh clone. Requires skopeo (preinstalled in CI; `brew install skopeo` on macOS): + +```bash +CATALOG_INDEX_IMAGE=quay.io/rhdh/plugin-catalog-index:next \ + ./e2e-tests/local-harness/populate.sh +``` + +Use a specific catalog index image (for example a release branch tag): ```bash -./e2e-tests/local-harness/populate.sh +CATALOG_INDEX_IMAGE=quay.io/rhdh/plugin-catalog-index:2.0 ./e2e-tests/local-harness/populate.sh ``` `populate.sh` takes an optional install-config path as its first argument @@ -53,12 +57,15 @@ Alternatives: ### 2. Run ```bash -yarn --cwd e2e-tests e2e:legacy-local +yarn --cwd e2e-tests e2e:local ``` -Playwright (`playwright.legacy-local.config.ts`) boots the backend and the legacy app -dev server with `app-config.yaml` + `app-config.dynamic-plugins.yaml` + -`app-config.local-e2e.yaml`. A `globalSetup` first fails fast with the populate command +Playwright (`playwright.local.config.ts`) boots the backend and the NFS app dev server +with `app-config.yaml` + `app-config.dynamic-plugins.yaml` + +`app-config.local-e2e.yaml` + `dynamic-plugins-root/app-config.dynamic-plugins.yaml`. +The harness install config (`e2e-tests/local-harness/dynamic-plugins.yaml`) disables +DPDY plugins that are not needed off-cluster (segment, adoption-insights, extensions, +…). A `globalSetup` first fails fast with the populate command if `dynamic-plugins-root` has no plugins. The run is scoped to tests tagged `@cluster-free-capable` within the spec files @@ -70,69 +77,86 @@ extra plugins, add them (with their `pluginConfig`) to ### Verified -With plugins populated, the legacy app renders the full production RHDH UI off-cluster -(branding, sidebar, global header, and Quick Access from the dynamic plugins). The -existing specs **pass unmodified**: - -- `guest-signin-happy-path` — all three tests: home page (dynamic-home-page plugin), - Settings and Sign-out (navigation via the global-header profile dropdown, using the - plugin's canonical `pluginConfig` merged through the generated - `dynamic-plugins-root/app-config.dynamic-plugins.yaml`, exactly as in-cluster). -- `learning-path-page` — renders from the static fallback data bundled with - `packages/app`; the "References" sidebar group mirrors the CI menu customization via - `app-config.local-e2e.yaml`. +With plugins populated, the NFS app renders the full production RHDH UI off-cluster +(branding, sidebar, the home-page widgets from the dynamic plugins, and the full +GlobalHeader — CompanyLogo, Search, StarredDropdown, ApplicationLauncherDropdown, +HelpDropdown, NotificationButton, and ProfileDropdown all render — see "Known issues" +for how that's wired). + +- `guest-signin-happy-path` — the `@cluster-free-capable` test (home page: Welcome heading, + Search and Starred Entities, all dynamic-home-page-plugin widgets, plus Settings and + Sign-out via the GlobalHeader's profile menu). A fourth, untagged test in the same + file exercises Quick Access against the real `/developer-hub` proxy; it only runs in + the full cluster-based CI suite (see "Known issues"). +- `learning-path-page` — navigates via the NFS sidebar flat nav to `/learning-paths` + and renders from the static fallback data bundled with the app. See + `plugins/frontend/sidebar` for the same sidebar entry. - `instance-health-check` — `GET /healthcheck` against the frontend origin. The app dev server proxies `/healthcheck` to the backend (`proxy` field in `packages/app/package.json`), mirroring the single-origin production container where the backend serves both the app and the health endpoint. - `smoke-test` — guest sign-in plus the home-page welcome heading (dynamic-home-page plugin); its readiness poll uses the same proxied `/healthcheck`. -- `home-page-customization` — all three tests. The CI home-page card customization - (Placeholder/Markdown/Featured Docs/Random Joke/Top + Recently Visited, from - `.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml`) is mirrored in - `app-config.local-e2e.yaml`; the Random Joke card fetches jokes from the public - Official Joke API in the browser, so it needs outbound network access. -- `plugins/frontend/sidebar` — sidebar menu customization (References group, Test - enabled/nested items, techdocs Favorites → Docs and Test_i items) mirrored from the - same CI configmap into `app-config.local-e2e.yaml`. The `/docs` index page needs the - techdocs frontend OCI plugin in the harness set (its route/menu config already lives - in the static `app-config.dynamic-plugins.yaml`). -- `settings` — language toggle (needs the CI `i18n.locales` list mirrored in the - overlay) plus the identity card ownership ("Guest User, team-a"). CI gets those - entities from Keycloak ingestion; the harness ingests the equivalent minimal - User/Group pair from `e2e-tests/local-harness/guest-ownership-entities.yaml` via a - `catalog.locations` file entry in the overlay (file targets resolve relative to the - backend cwd, `packages/backend`). The guest sign-in resolver picks the entity up and - issues ownership refs including `team-a` exactly as in-cluster. +- `home-page-customization` — the Quick Access, Featured Docs, Top Visited, and + Recently Visited widgets, using the `home-page-layout:home/dynamic-homepage-layout` + `app.extensions` customization mirrored from + `.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml` into + `app-config.local-e2e.yaml`. NFS home-page cards are a fixed set of built-in widgets + (`home-page-widget:home/*`) — the legacy Placeholder/Markdown/Random Joke mount-point + cards have no NFS equivalent and are no longer asserted. Its `@cluster-free-capable` "Quick + Access" assertion only checks the card's title text, not real link data (see "Known + issues" for why); the file's other, untagged "Verify Customized Quick Access" test + covers the real link data and only runs in full CI. +- `plugins/frontend/sidebar` — the `@cluster-free-capable` tests verify flat-nav behavior: + Docs and Learning Paths sidebar items navigate to the expected pages (NFS + `PageBlueprint` title for TechDocs: "Docs", not the legacy OFS `pageWrapper.title` + "Documentation"). NFS's sidebar (`packages/app/src/modules/nav/Sidebar.tsx`) is a + fixed, code-defined flat nav with no config-driven nested-group equivalent of the + legacy References/Favorites `menuItems`. The Docs test stops at the index page — + this harness's catalog has no `techdocs-ref`-annotated entities (see "Known issues"), + so there's nothing to click into. The file's other, untagged "Verify Docs entity + page renders real content" test opens a real entity's docs and checks for actual + content; it only runs in full CI, where `catalog-entities/components/showcase.yaml`/ + `community-plugins.yaml` provide real `techdocs-ref` entities. - `plugins/user-settings-info-card` — the CI `buildInfo` card customization ("RHDH - Build info") mirrored in the overlay. -- `plugins/application-provider` and `plugins/application-listener` — the - application-provider-test / application-listener-test OCI plugins with the same - pluginConfig CI uses in its Helm values (`values_showcase.yaml`); they are OCI-only - builds, not part of the repo's dynamic-plugins source tree. + Build info") mirrored in the overlay. Reaches the Settings page via the same + `goToSettingsPage()` helper `settings.spec.ts` uses, through the GlobalHeader's + profile menu. +- `settings.spec.ts` — language toggle (needs `api:app/app-language.availableLanguages` + in `app.extensions`, not just `i18n.locales`), French label switching, pin-sidebar + toggle, and identity-card ownership ("Guest User, team-a"). NFS sidebar page titles + stay in upstream English ("Home") even after switching AppLanguageApi to French — + only GlobalHeader chrome translates; the final assertion checks "Home", not + `menuItem.home` ("Accueil"). + +Not enablable yet: -Not enablable yet: `plugins/licensed-users-info-backend` — the -`licensed-users-info-backend` plugin is not published to the overlays OCI registry -(ghcr). +- `plugins/application-provider` and `plugins/application-listener` — the + application-provider-test / application-listener-test OCI plugins only publish an + OFS ("." Module Federation) entry point, no NFS/alpha extensions. `packages/app` + (NFS) has no application/provider or application/listener renderer yet — so neither + spec can currently pass here. Not installed in the harness; re-add once NFS support + exists for either the fixture plugins or a generic provider/listener renderer in + `packages/app`. +- `plugins/licensed-users-info-backend` — the + `licensed-users-info-backend` plugin is not published to the overlays OCI registry + (ghcr). ## CI `.github/workflows/e2e-cluster-free.yaml` runs this harness on GitHub Actions in a -cluster-free phase: it installs deps + skopeo, populates `dynamic-plugins-root` via -`./e2e-tests/local-harness/populate.sh` (the harness plugin set from the public OCI -registry, ghcr), then runs `yarn e2e:legacy-local`. No cluster or container image is -built. It triggers on `e2e-tests/**` and `app-config*.yaml` changes; the scope can -widen to `packages/app/**` / `packages/backend/**` once it is proven stable. - -## Why the legacy app, not app-next - -The harness targets the legacy app because **dynamic frontend plugins do not load on -`packages/app-next` yet**: app-next's `dynamicFrontendFeaturesLoader()` fetches Module -Federation remotes from the backend, but that endpoint is no-op'd unless -`ENABLE_STANDARD_MODULE_FEDERATION=true`, and even then RHDH's exported dynamic frontend -plugins do not contain standard MF assets (see `packages/backend/src/index.ts`). Until -that lands upstream, app-next can only exercise core/static plugin UIs. An app-next -harness is tracked as a follow-up (RHIDP-13501 / spike RHIDP-15075). +cluster-free phase: it installs deps + skopeo, resolves a catalog index image, populates +`dynamic-plugins-root` via `CATALOG_INDEX_IMAGE=… ./e2e-tests/local-harness/populate.sh` +(harness plugin set from quay.io), then runs `yarn e2e:local`. No cluster or container +image is built. It triggers on `e2e-tests/**` and `app-config*.yaml` changes; the scope +can widen to `packages/app/**` / `packages/backend/**` once it is proven stable. + +## NFS frontend (`packages/app`) + +The harness targets the NFS frontend (`packages/app`). The backend serves standard +Module Federation assets by default, so dynamic frontend plugins load via Module +Federation remotes from the backend (see `packages/backend/src/index.ts` and +`playwright.local.config.ts`). ## vs. rhdh-local @@ -145,25 +169,92 @@ just `run`), which is why this harness boots the dev servers directly instead. ## Known issues / limits -- **Workspace must be reconciled for the offline (from-source) populate path.** If - `node_modules` is out of sync with `yarn.lock` (e.g. just after a rebase that changed - dependency versions), backend dynamic-plugin builds fail with version-mismatch errors - and yarn may not surface workspace bins. Run `yarn install` first. The - `install-dynamic-plugins` populate path avoids building from source and is unaffected. -- **Re-run `populate.sh` after changing the harness plugin set.** The `pluginConfig` - blocks in `e2e-tests/local-harness/dynamic-plugins.yaml` (e.g. the global-header - mount points) only take effect through the generated - `dynamic-plugins-root/app-config.dynamic-plugins.yaml`, which the webServer loads - last. A stale populate leaves plugins loaded but unconfigured (the header renders - empty). -- **Specs that need CI test data are not enabled yet.** `settings.spec.ts` asserts - ownership entities ("Guest User, team-a") that come from catalog locations in the CI - config map; `home-page-customization.spec.ts` needs the home-page card customization - from `.ci/pipelines/resources/config_map/dynamic-plugins-config.yaml`. Enabling them - means mirroring that data/config into the harness overlay. +- **Re-run `populate.sh` after changing the harness plugin set.** Overrides in + `e2e-tests/local-harness/dynamic-plugins.yaml` (`enabled: false` + `{{inherit}}`) only + take effect through the generated `dynamic-plugins-root/app-config.dynamic-plugins.yaml`, + which the webServer loads last. A stale populate leaves unwanted plugins installed. +- **Plugin sanity uses a separate config overlay.** Default `populate.sh` disables + segment, adoption-insights, extensions (dist), and other plugins not needed for + cluster-free specs. If you switch to `populate-catalog-index.sh` (full index), use + `app-config.plugin-sanity.yaml` (via `e2e:plugin-sanity`) for provider stubs. - **Live-external-service specs** (real k8s cluster, GitHub org, Quay, Tekton, Keycloak) still need those services or mocks; this harness covers UI/plugin-rendering scenarios that don't require live external infra. -- **`janus-cli` / `backstage-cli`** live in the repo-root `node_modules/.bin`, which yarn +- **`backstage-cli`** lives in the repo-root `node_modules/.bin`, which yarn does not surface for the `app`/`backend` workspaces, so the webServer commands invoke - them directly with the root `.bin` prepended to `PATH`. + it directly with the root `.bin` prepended to `PATH`. +- **`VisitsStorageApi` 404s under this config, pre-dating NFS.** `app-config.local-e2e.yaml` + sets `userSettings.persistence: browser` (added in `#4020`, before the NFS migration, + to opt CI out of database storage, and mirrored in the CI ConfigMap), which means + `@backstage/plugin-user-settings-backend` is never loaded (see + `packages/backend/src/modules/userSettings.ts`). `VisitsStorageApi` calls the backend + regardless, so `/api/user-settings/multiget`/`/api/user-settings/buckets/...` 404 — + in a production build that's just a background error, but under this harness's dev + server the unhandled rejection trips webpack's react-refresh error overlay (a + full-screen transparent `