This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 1facefb183 fix(frontend): render workflow covers on the hub landing
page (#8383)
1facefb183 is described below
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;
}
}