The GitHub Actions job "Required Checks" on texera.git/main has succeeded.
Run started by GitHub user github-merge-queue[bot] (triggered by 
github-merge-queue[bot]).

Head commit for run:
7d21555bceec552a307c0d4572ff0ee8cd477f94 / Matthew B. <[email protected]>
feat(dashboard): add custom cover image for workflow cards (#5704)

### What changes were proposed in this PR?

Adds an optional, per-workflow custom cover image for dashboard workflow
cards, persisted on the backend so the cover syncs across sessions and
browsers for anyone with access to the workflow.

- Add a `workflow_cover_image(wid, image)` table (`wid` PK, FK to
`workflow` with `ON DELETE CASCADE`; the cover is stored as a downscaled
JPEG data URL) to `sql/texera_ddl.sql`, plus a `sql/updates/27.sql`
migration registered as Liquibase changeSet `27`.
- Add access-gated `GET`/`PUT`/`DELETE /workflow/{wid}/cover` endpoints
on `WorkflowResource`:
- `GET` requires read access (403 otherwise) and returns the stored data
URL, or 404 when no cover is set.
- `PUT` requires write access (403 otherwise), rejects an empty/blank
body (400), requires the value to be a `data:image/...` data URL (400
otherwise), and enforces a 4 MB size cap (400 otherwise). Writes are an
upsert keyed on `wid`.
  - `DELETE` requires write access and is idempotent.
- Set/delete authorize on `hasWriteAccess` (consistent with `makePublic`
/ `updateWorkflowName` / `updateWorkflowDescription`), so any WRITE
collaborator can change the shared per-workflow cover; the owner-only UI
is where the control is surfaced, not a security boundary.
- Add `WorkflowCoverService` (frontend): `getCover` (maps a 404 to
`undefined`), `setCoverFromFile` (downscales the chosen image to a 640px
longest edge and re-encodes it as a JPEG data URL at 0.8 quality before
uploading), and `clearCover`.
- Add hover-revealed controls on the workflow card: a camera button to
pick an image and a rollback button to reset, shown only to the owner in
private search (`canEditCover`). `previewImage` prefers the stored cover
and falls back to the default card background.

Note on validation: the setter currently accepts any `data:image/...`
data URL (it does not enforce a raster-only MIME allowlist), so
`data:image/svg+xml;...` and non-base64 forms are accepted. This is not
a live XSS vector today because covers are rendered via `<img src>`,
which does not execute embedded SVG scripting. Tightening this to a
raster allowlist is a possible follow-up.

### Any related issues, documentation, discussions?
Closes: #5703

Follow-up: #5920 tracks folding a `hasCover` flag into the workflow
list/search payload so cover-less cards issue no per-card cover request
(avoids the current per-card `GET /{wid}/cover` fan-out).

### How was this PR tested?
- Backend: `WorkflowResourceCoverSpec` (uses the `MockTexeraDB`
embedded-Postgres harness) covers all three endpoints: 404 when unset,
set/get round-trip, read by a user with read access, forbidden
read/write for a user without access, upsert replace, empty/blank/null
and non-`data:image/` validation rejections, size-limit rejection,
no-persist on validation failure, and idempotent delete. Run `sbt
"WorkflowExecutionService/testOnly *WorkflowResourceCoverSpec"`.
- Frontend (service): `workflow-cover.service.spec.ts` covers `getCover`
mapping the image, 404 resolving to `undefined`, `clearCover` issuing a
DELETE, `setCoverFromFile` PUTting the resized data URL, and
`fileToResizedDataUrl` itself (downscale math, canvas-context failure,
undecodable image, and file-read failure). Run `cd frontend && yarn test
--include='**/workflow-cover.service.spec.ts' --watch=false`.
- Frontend (component): `card-item.component.spec.ts` covers the cover
behaviors (owner-in-private-search gating, cover load on init,
default-image fallback, upload success / non-image rejection / upload
failure, reset success / failure, `openImagePicker`, and the no-file /
non-numeric-id early returns). Run `cd frontend && yarn test
--include='**/card-item.component.spec.ts' --watch=false`.
- Manual: open the dashboard in private search as a workflow owner,
hover a card, click the camera icon, pick an image, and expect the card
preview to update; reload in another browser and expect the same cover
(synced via the backend).
- Manual: click the rollback icon and expect the card to revert to the
default background, with the cover gone after reload.

<img width="424" height="530" alt="image"
src="https://github.com/user-attachments/assets/99f11d99-0bcc-43c0-ad93-bde4457a127a";
/>

<img width="424" height="530" alt="image"
src="https://github.com/user-attachments/assets/553c4be5-0593-43f6-af92-f6260dc80d43";
/>

### Was this PR authored or co-authored using generative AI tooling?
Co-authored with Claude Opus 4.8 in compliance with ASF

---------

Co-authored-by: Xuan Gu <[email protected]>

Report URL: https://github.com/apache/texera/actions/runs/28732415168

With regards,
GitHub Actions via GitBox

Reply via email to