From b4ab0df3e871d9a85a0c9ac7cd285e7b65756c46 Mon Sep 17 00:00:00 2001 From: Tanishq Gandhi Date: Thu, 3 Sep 2026 11:29:14 -0700 Subject: [PATCH] fix(frontend): render workflow covers 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 rendered it correctly in Your Work. Covers reach the frontend two different ways. A workflow's is a downscaled data URL that arrives inline on the list payload and lands on DashboardEntry.coverImageUrl. A dataset's or model's is a committed file, so what arrives is a path and the card fetches a presigned URL from /{id}/cover-url. browse-section only 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. Nothing was ever cached for a workflow, so getCoverImage fell through to the default. getCoverImage now reads a workflow's cover off the entry, mirroring the branch card-item already had. It is deliberately not a blanket fallback for every kind: a dataset or model carries a stored path such as v1/cover.png, which no img can load, so a blanket fallback would swap a clean placeholder for a broken image whenever the presigned fetch returned nothing. A spec pins that, and datasets and models keep resolving exactly as before. Also removes a duplicate "/api/model/**" key in frontend/proxy.config.json, where both entries pointed at :9092 and the last silently won. Closes #8382. --- frontend/proxy.config.json | 7 +--- .../browse-section.component.spec.ts | 35 ++++++++++++++++++- .../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 7801e0c256f..47988f6b46e 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 8531fb5247e..b85bb72b982 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 + // 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 65f2075b21c..7d1773e50fa 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 can load. + if (entity.type === EntityType.Workflow) { + return entity.coverImageUrl ?? this.defaultBackground; + } return this.coverImageUrls.get(this.cacheKey(entity)) || this.defaultBackground; } }