This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-8383-905c17d93e0975eb501581aab3716f9e98a50b0d
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 1facefb183c96e7b426ff6be35e5b6aaa7b33345
Author: Tanishq Gandhi <[email protected]>
AuthorDate: Thu Sep 3 22:11:36 2026 +0000

    fix(frontend): render workflow covers on the hub landing page (#8383)
    
    ### What changes were proposed in this PR?
    
    Workflow covers never rendered on the Hub landing page: every card under
    **Top Loved Workflows** and **Top Cloned Workflows** showed the grey
    placeholder, even for a workflow whose owner had set a cover, while the
    same workflow showed it correctly in Your Work → Workflows.
    
    Covers reach the frontend two different ways. A **workflow** cover is a
    downscaled **data URL** that arrives inline on the list payload and
    lands on `DashboardEntry.coverImageUrl`. A **dataset** or **model**
    cover is a committed file, so what arrives is a *path* and the card has
    to fetch a presigned URL from `/{id}/cover-url`.
    
    `browse-section` only ever handled the second kind — it asks the
    descriptor for a `coverUrl` and bails when there is none, which is
    always the case for a workflow, since `WorkflowResourceDescriptor`
    deliberately declares none. So nothing was ever cached for a workflow
    and `getCoverImage` fell through to the default.
    
    `getCoverImage` now reads a workflow's cover straight off the entry,
    mirroring the branch `card-item.component.ts:197-202` already had.
    
    
    Also included, since it is one line in the same area and needs no
    separate issue: `frontend/proxy.config.json` declared `"/api/model/**"`
    **twice** (both pointing at `:9092`, so the last silently won). The
    duplicate is removed, leaving the entry beside `/api/dataset`, so the
    file reads `dataset`, `model`, `access/dataset`, `access/model`.
    
    **Before** — both workflows are public; the left one has a cover, the
    right one does not:
    
    <img width="1440" height="900" alt="issue5-1-hub-landing-before"
    
src="https://github.com/user-attachments/assets/120c2cf3-2ee9-49e0-97df-a77e3746275c";
    />
    
    **After** — the left card renders its cover, the right one still shows
    the placeholder:
    
    <img width="1440" height="900" alt="issue5-1-hub-landing-after"
    
src="https://github.com/user-attachments/assets/9c0294a0-f081-4428-a230-80281a40cb7a";
    />
    
    ### Any related issues, documentation, discussions?
    
    Closes #8382.
    
    ### How was this PR tested?
    
    `browse-section.component.spec.ts`, 25 passed:
    
    - `renders a workflow's cover from the entry, since no cover is ever
    fetched for one` — a workflow
      with a cover resolves to it, one without still gets the default.
    - `keeps a file-backed kind on the placeholder rather than rendering its
    stored cover path` — a
    dataset whose presigned fetch answers with an empty URL stays on the
    placeholder instead of
      rendering `v1/images/preview.png`.
    - `skips an entity whose descriptor resolves no cover, rather than
    calling undefined` was already
    there and asserted `getCoverImage(workflow) === defaultBackground` — it
    pinned the bug, so it now
    asserts the cover comes off the entry, with the unregistered-kind row
    still falling back.
    
    `landing-page.component.spec.ts` also run, 17 passed.
    
    ```
    cd frontend
    npx ng test --include 
src/app/hub/component/browse-section/browse-section.component.spec.ts
    npx ng test --include 
src/app/hub/component/landing-page/landing-page.component.spec.ts
    ```
    
    Checked by hand against a local stack: a public workflow with a cover
    set from the dashboard now
    shows it in both hub sections, and a public workflow without one is
    unchanged.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 5)
---
 frontend/proxy.config.json                         |  7 +----
 .../browse-section.component.spec.ts               | 35 +++++++++++++++++++++-
 .../browse-section/browse-section.component.ts     |  7 +++++
 3 files changed, 42 insertions(+), 7 deletions(-)

diff --git a/frontend/proxy.config.json b/frontend/proxy.config.json
index 7801e0c256..47988f6b46 100755
--- a/frontend/proxy.config.json
+++ b/frontend/proxy.config.json
@@ -15,7 +15,7 @@
     "secure": false,
     "changeOrigin": true
   },
-"/api/models": {
+  "/api/models": {
     "target": "http://localhost:9096";,
     "secure": false,
     "changeOrigin": true
@@ -45,11 +45,6 @@
     "secure": false,
     "changeOrigin": true
   },
-  "/api/model/**": {
-    "target": "http://localhost:9092";,
-    "secure": false,
-    "changeOrigin": true
-  },
   "/api/access/model/**": {
     "target": "http://localhost:9092";,
     "secure": false,
diff --git 
a/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
 
b/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
index 8531fb5247..b85bb72b98 100644
--- 
a/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
+++ 
b/frontend/src/app/hub/component/browse-section/browse-section.component.spec.ts
@@ -181,7 +181,40 @@ describe("BrowseSectionComponent", () => {
       expect(() => component.ngOnInit()).not.toThrow();
       expect(coverCache(component).has("workflow:10")).toBe(false);
       expect(coverCache(component).has("computing-unit:12")).toBe(false);
-      
expect(component.getCoverImage(workflow)).toBe(component.defaultBackground);
+      // Nothing is cached for a workflow, but its cover is readable straight 
off the entry.
+      expect(component.getCoverImage(workflow)).toBe("carried-on-the-entry");
+      
expect(component.getCoverImage(unregistered)).toBe(component.defaultBackground);
+    });
+
+    it("renders a workflow's cover from the entry, since no cover is ever 
fetched for one", () => {
+      const withCover = {
+        id: 20,
+        type: "workflow",
+        coverImageUrl: "data:image/png;base64,AAAA",
+        accessibleUserIds: [],
+      } as unknown as DashboardEntry;
+      const withoutCover = { id: 21, type: "workflow", accessibleUserIds: [] } 
as unknown as DashboardEntry;
+      component.entities = [withCover, withoutCover];
+      component.ngOnInit();
+
+      
expect(component.getCoverImage(withCover)).toBe("data:image/png;base64,AAAA");
+      
expect(component.getCoverImage(withoutCover)).toBe(component.defaultBackground);
+    });
+
+    it("keeps a file-backed kind on the placeholder rather than rendering its 
stored cover path", () => {
+      // A dataset's coverImageUrl is a path relative to the dataset root, not 
something an <img>
+      // can load, so it must never stand in for the presigned URL the 
descriptor resolves.
+      vi.spyOn(TestBed.inject(DatasetService) as any, 
"getDatasetCoverUrl").mockReturnValue(of({ url: "" }));
+      const entity = {
+        id: 22,
+        type: "dataset",
+        coverImageUrl: "v1/images/preview.png",
+        accessibleUserIds: [],
+      } as unknown as DashboardEntry;
+      component.entities = [entity];
+      component.ngOnInit();
+
+      
expect(component.getCoverImage(entity)).toBe(component.defaultBackground);
     });
 
     it("caches nothing when the descriptor resolves an empty cover url", () => 
{
diff --git 
a/frontend/src/app/hub/component/browse-section/browse-section.component.ts 
b/frontend/src/app/hub/component/browse-section/browse-section.component.ts
index 65f2075b21..7d1773e50f 100644
--- a/frontend/src/app/hub/component/browse-section/browse-section.component.ts
+++ b/frontend/src/app/hub/component/browse-section/browse-section.component.ts
@@ -19,6 +19,7 @@
 
 import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges 
} from "@angular/core";
 import { DashboardEntry } from "../../../dashboard/type/dashboard-entry";
+import { EntityType } from "../../service/hub.service";
 import { ResourceRegistryService } from 
"../../../dashboard/service/user/resource-registry/resource-registry.service";
 import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
 import { NgIf, NgFor, NgStyle, DatePipe } from "@angular/common";
@@ -120,6 +121,12 @@ export class BrowseSectionComponent implements OnInit, 
OnChanges {
   }
 
   getCoverImage(entity: DashboardEntry): string {
+    // A workflow's cover is a downscaled data URL carried on the entry, so 
nothing is ever fetched
+    // for it. The file-backed kinds carry a stored path instead, which only 
the cache above can
+    // turn into something an <img> can load.
+    if (entity.type === EntityType.Workflow) {
+      return entity.coverImageUrl ?? this.defaultBackground;
+    }
     return this.coverImageUrls.get(this.cacheKey(entity)) || 
this.defaultBackground;
   }
 }

Reply via email to