Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
00f52ab
Add tagging field to S3 upload destination response
ErykKul May 4, 2026
b001447
Add feature-flagged React uploader mount
ErykKul May 4, 2026
797b41a
Polish: REACT_UPLOADER javadoc and CSS isolation note
ErykKul May 4, 2026
7d42599
Add dataverse.reusable-components.base-url JVM setting
ErykKul May 5, 2026
9889056
Add reusable-components operator guide and release notes
ErykKul May 5, 2026
d7dd360
Refactor reusable_frontend_components.md as backend integration guide
ErykKul May 5, 2026
31f6265
Add paginated tree listing endpoint for dataset versions (#6691)
ErykKul May 5, 2026
7e6e470
Add DatasetsTreeIT integration tests for /versions/{vid}/tree
ErykKul May 5, 2026
4acfa4d
Add ETag and 304 support to tree listing endpoint
ErykKul May 5, 2026
9ce7b4a
docs: describe ETag caching for tree listing endpoint
ErykKul May 5, 2026
fbb6102
Add feature-flagged React tree-view mount on the JSF Files tab (#6691…
ErykKul May 5, 2026
e16af5f
Note streaming-zip download in tree-view release notes
ErykKul May 5, 2026
0fd5969
Refresh tree-view section in reusable-components arch doc
ErykKul May 5, 2026
a127642
Datasets: import MessageDigest / NoSuchAlgorithmException
ErykKul May 5, 2026
6a93953
Fix Sphinx title-underline-too-short on tree-view API section
ErykKul May 5, 2026
669be93
Promote tree listing endpoint to native SQL keyset paginator
ErykKul May 6, 2026
d40c801
merged develop
ErykKul May 6, 2026
861f0dc
Bake reusable-components bundle into the WAR + flip dev feature flags
ErykKul May 6, 2026
1f05782
Refresh in-WAR reusable-components bundle to use Shadow DOM mounting
ErykKul May 6, 2026
0fd3d83
Fix tree-listing folder query: repeat expression in GROUP BY / ORDER BY
ErykKul May 6, 2026
49e8033
Add bucket CORS to localstack init
ErykKul May 7, 2026
294fe94
Hide legacy Done button when React uploader is active
ErykKul May 7, 2026
719fc6c
Rebuild standalone bundles with checkbox column and stability fix
ErykKul May 7, 2026
96448b2
Use var for citation-js inline script to survive PF partial updates
ErykKul May 7, 2026
7643778
Rebuild bundles with checkbox color fallbacks
ErykKul May 7, 2026
807637e
Rebuild bundles for streaming-zip credentials fix
ErykKul May 7, 2026
6276f57
Rebuild bundles for useFileTree reset fix
ErykKul May 7, 2026
f52185f
Rebuild bundles for header select-all + JSF remount
ErykKul May 7, 2026
b8f18fb
Cache-bust reusable-components bundles with file mtime instead of pin…
ErykKul May 7, 2026
b4825f2
Update 6691 release notes with iteration fixes and dev-stack notes
ErykKul May 7, 2026
5e9b434
MinIO direct-upload: flip upload/download-redirect, add bucket init a…
ErykKul May 7, 2026
d3962f0
Audit fixes: cursor markers, URL validation, narrow exception, index-…
ErykKul May 7, 2026
47d940d
Rebuild bundles for siteUrl validation and rel=noopener
ErykKul May 7, 2026
1925bff
Address review: SQL LIKE escape, folder keyset tiebreak, expired emba…
ErykKul May 7, 2026
565c14c
Rebuild bundles for review fixes (downloadsDisabled, P3 fixes)
ErykKul May 7, 2026
c3173f4
Rebuild bundles for observer-init race fix
ErykKul May 7, 2026
9429115
Rebuild bundles without sourceMappingURL comments
ErykKul May 7, 2026
c48d58f
Don't include literal </script> in JSF inline script comments (it was…
ErykKul May 7, 2026
318dc03
Fix dev_minio command + dev_minio_init entrypoint YAML quoting
ErykKul May 7, 2026
238fb67
Trim MinIO compose: drop redundant CORS env var and console-address o…
ErykKul May 7, 2026
d90c58d
Rebuild bundles for toast top-offset fix
ErykKul May 7, 2026
686e46b
Hoist Dropbox/Globus/Webloader into independent panels and refresh re…
ErykKul May 8, 2026
ff1d479
Merge remote-tracking branch 'origin/develop' into 6691_reusable_comp…
ErykKul May 8, 2026
6ec3704
Merge branch 'develop' into 6691_reusable_components
ErykKul May 8, 2026
815a71c
Pin tree endpoint Cache-Control to private to keep auth-required resp…
ErykKul May 8, 2026
f09603d
Sync tree-endpoint Cache-Control, session-auth-hardening, and index-m…
ErykKul May 8, 2026
2627f4d
Rebuild reusable-component bundles for the recent shadow-DOM focus an…
ErykKul May 8, 2026
c7749de
Rebuild reusable-component bundles with leave-modal race fix
ErykKul May 8, 2026
c34ccf6
Drop premature api-session-auth-hardening env var and rephrase docs f…
ErykKul May 8, 2026
69b9197
Rebuild reusable-component bundles with the SDK x-amz-tagging default…
ErykKul May 9, 2026
04d82ff
Drop merged-in changes from PRs #11963 and #12195 plus dev compose tw…
ErykKul May 9, 2026
1eed993
Drop drive-by changes and restore accidentally-deleted develop files
ErykKul May 9, 2026
920b1ee
Hide upload section on create-dataset when react-uploader flag is on
ErykKul May 9, 2026
78b3aaf
Show Tree/Table toggle for flat multi-file datasets too (drop folders…
ErykKul May 9, 2026
5b53e61
Retrigger CI
ErykKul May 10, 2026
03590b1
Omit tree checksum for ingested tabular files served as TSV
ErykKul May 10, 2026
db09e49
Add recursive byte total per folder in tree response
ErykKul May 10, 2026
cb3ca04
Add per-access folder counts (restricted, embargoed) to tree response
ErykKul May 10, 2026
3ff2280
Document new tree response counts and tabular checksum semantics
ErykKul May 10, 2026
e3b2ba4
Rebuild reusable-components bundle from latest SPA branch
ErykKul May 10, 2026
3c5e836
Rebuild reusable-components bundle with streaming-zip checksum verifi…
ErykKul May 10, 2026
c6cd8e3
Merge branch 'develop' into 6691_reusable_components
ErykKul May 22, 2026
2764981
reverted unrealted change that sneaked in
ErykKul May 24, 2026
961aa2b
removed unneeded doc; not accurate enough and rather confusing than h…
ErykKul May 24, 2026
c5baee3
fix the documentation about hosting pre-built bundles
ErykKul May 24, 2026
08674e0
rename webapp/dvwebloader to webapp/reusable-components, collapse inn…
ErykKul May 24, 2026
19cb711
gate flat-dataset Tree/Table toggle on react-tree-view feature flag
ErykKul May 25, 2026
9e1b57c
drop Bug fixes section from release notes; both fixes scoped out of t…
ErykKul May 25, 2026
eb7caad
enabled api session auth
ErykKul May 25, 2026
fb81ed9
6691: report original-form size under originals=true on the tree endp…
ErykKul May 29, 2026
92610f4
Merge remote-tracking branch 'origin/develop' into 6691_reusable_comp…
ErykKul May 29, 2026
b06c078
6691: LogoutIT expects 400 (no session cookie) now that api-session-a…
ErykKul May 29, 2026
f232b71
Merge branch 'develop' into 6691_reusable_components
ErykKul May 29, 2026
e4a2c51
6691: re-trigger Jenkins build
ErykKul May 30, 2026
cc8cd47
6691: fix tree pagination, caching, S3 tagging and upload-panel guard…
ErykKul Jul 3, 2026
a1b128d
Merge remote-tracking branch 'origin/develop' into 6691_reusable_comp…
ErykKul Jul 6, 2026
307a6f5
Rebuild reusable-component bundles: retentionExpired access display, …
ErykKul Jul 6, 2026
92cb2fa
Fix tree ITs on merged develop: unique-content fixture files (duplica…
ErykKul Jul 6, 2026
bf6df94
Rebuild reusable-component bundles from frontend HEAD: develop-merge …
ErykKul Jul 6, 2026
bc9e2b1
Tree endpoint applies the LocallyFAIR visibility gate like sibling da…
ErykKul Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions doc/release-notes/6691-dataset-version-tree-listing-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
A new API endpoint allows lazy, paginated browsing of the folder hierarchy inside a dataset version:

- `GET /api/datasets/{id}/versions/{versionId}/tree`

The endpoint is the backend half of the **tree view selection and download** work tracked in [#6691](https://github.com/IQSS/dataverse/issues/6691) and the SPA work in [IQSS/dataverse-frontend#622](https://github.com/IQSS/dataverse-frontend/issues/622) / [#117](https://github.com/IQSS/dataverse-frontend/issues/117). It returns the immediate folders and files under a given path, with folders first and a stable name ordering, plus an opaque keyset cursor for pagination.

Query parameters:

- `path` — folder path within the dataset version. Root is `""` or omit. Forward-slash separated. Normalized with the write side's rules (slash/backslash runs collapse, leading dots/dashes/spaces stripped); folder-path tails round-trip, and junk-only paths like `..` yield `400`.
- `limit` — page size; default `100`, clamped to `1000`.
- `cursor` — opaque server-issued token from a previous response. Invalid/stale cursors return `400`.
- `include` — `all` (default), `folders`, or `files`.
- `order` — `NameAZ` (default) or `NameZA`. Folders sort first regardless.
- `includeDeaccessioned` — same semantics as `/files`.
- `originals` — when `true`, ingested tabular files are reported in their original-upload form: `downloadUrl` requests `?format=original`, and both `checksum` and the per-file `size` reflect the saved original rather than the converted TSV.

Response shape:

```json
{
"path": "data/raw",
"items": [
{ "type": "folder", "name": "2024", "path": "data/raw/2024",
"counts": { "files": 12, "folders": 1, "bytes": 4194304,
"restricted": 0, "embargoed": 0, "retentionExpired": 0 } },
{ "type": "file", "id": 42, "name": "data.csv", "path": "data/raw/data.csv",
"size": 1024, "contentType": "text/csv", "access": "public",
"checksum": { "type": "MD5", "value": "abc" },
"downloadUrl": "/api/access/datafile/42" }
],
"nextCursor": "b2Zmc2V0PTEwMA",
"limit": 100,
"order": "NameAZ",
"include": "all",
"approximateCount": 137
}
```

`approximateCount` (total folders + files at the path) is snapshotted on the first page of a walk and carried through the cursor for the rest of it.

Folder rows carry recursive aggregates over their subtree: `counts.files` is the total file count, `counts.folders` is the immediate-subfolder count, `counts.bytes` is the total size of files in the subtree (using `df.filesize` — the served-form size, intended as a "downloading this folder = N GB" UX hint, not authoritative under `originals=true`). `counts.restricted`, `counts.embargoed` and `counts.retentionExpired` mirror the per-file `access` resolution and are mutually exclusive: retention-expired wins (the file cannot be served at all), then restricted (even if it also carries an embargo), then non-restricted files with an active embargo. Public files are implied as `files - restricted - embargoed - retentionExpired`.

The per-file `checksum` object is present only when the digest matches the bytes the corresponding `downloadUrl` would serve. For ingested tabular files the default `downloadUrl` resolves to a converted TSV whose digest Dataverse does not store, so `checksum` is omitted on those rows; passing `originals=true` flips both the URL (`?format=original`) and the checksum back on, since the saved-original aux blob's bytes match `df.checksumvalue`. The per-file `size` follows the same rule: under `originals=true` it reports the saved original's size (`dt.originalfilesize`) so it matches the original bytes, while the folder `counts.bytes` rollup stays the served-form total. Clients can therefore treat "checksum present" as an unconditional commitment that the value matches the bytes they would receive — and, under `originals=true`, that the reported `size` matches them too.

The per-file `access` marker is one of `retentionExpired`, `restricted`, `embargoed`, `public` — resolved in that order. The embargo and retention date checks match Dataverse's actual download enforcement (`FileUtil.isActivelyEmbargoed` / `isRetentionExpired`): a file whose embargo lifts today is reported `public` and is downloadable. Note this differs by one day, at the boundary, from the `files` endpoint's access-status *filter*, which uses an inclusive comparison and treats a file as embargoed through the lift date itself.

For published, non-deaccessioned versions the response carries an `ETag` (derived from the request inputs plus the current date) and `Cache-Control: private, no-cache`; clients pass the ETag back in `If-None-Match` and receive a body-less `304 Not Modified` while it still matches. A released version's file list is frozen, but the response is still time-dependent — `access` flips when an embargo lapses or a retention period expires — which is why the contract is revalidate-always (`no-cache`) with a date-scoped validator rather than `immutable`. `private` keeps responses out of shared proxies because the endpoint is auth-required. Drafts and deaccessioned versions emit no caching headers.

## Performance

The endpoint is backed by two native keyset SQL queries (folder rollup + file listing) against the `filemetadata` table, driven by a covering index added in Flyway migration `V6.10.1.2`:

```
ix_filemetadata_tree(datasetversion_id, directorylabel text_pattern_ops, lower(label), datafile_id)
```

(`text_pattern_ops` on `directorylabel` is what lets the folder query's `LIKE 'path/%'` run as an index range scan on the UTF-8/ICU-collated databases virtually all installs use; without it the scan is bounded only by the version id.)

Listing one folder is independent of the dataset's total file count — the queries scan only the rows under the requested path, with the keyset cursor (`(lower(label), datafile_id)` for files, last folder name for folders) avoiding the offset penalty. The wire format and cursor opacity are unchanged from the first cut; clients keep echoing back `nextCursor` exactly as before.

The `dataverse-client-javascript` SDK ships matching helpers in the same release wave: `listDatasetTreeNode` and `iterateDatasetTreeNode`.
56 changes: 56 additions & 0 deletions doc/release-notes/6691-reusable-frontend-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## Reusable Frontend Components — JSF Mount

This release introduces the first reusable React component built in `dataverse-frontend` and embedded into the classic JSF UI: the React file uploader (DVWebloader v2). It is the foundation for further dual-mode components (e.g. the file tree view tracked in [#6691](https://github.com/IQSS/dataverse/issues/6691)).

### What changed

- **New feature flag** `dataverse.feature.react-uploader` (off by default). When enabled, the classic PrimeFaces upload widget on the dataset edit page is replaced with the React uploader. The file *replace* flow keeps using the JSF widget.
- **New feature flag** `dataverse.feature.react-tree-view` (off by default). When enabled, the dataset Files tab's "Tree" view (selectable via the existing Table/Tree toggle) is rendered by the same React lazy tree the SPA uses, instead of the classic PrimeFaces tree. The table view is unchanged. The tree supports lazy folder loading, tri-state selection (per-row checkboxes plus a header select-all), full keyboard navigation (WAI-ARIA tree pattern), URL-bookmarkable folder paths (`?view=tree&path=…`), and **client-side streaming-zip download** of the user's selection — the bundle pipes per-file response bodies into a single zip without any server-side ZIP endpoint.
- **New JVM setting** `dataverse.reusable-components.base-url` (default `/reusable-components`) tells the JSF page where to load the reusable component bundle from. The default value points at the bundle files baked into the WAR; operators who want to host the bundle elsewhere can copy those files out and override the setting.
- **Server-authoritative S3 tagging.** Single-part direct-upload responses from `S3AccessIO.generateTemporaryS3UploadUrls` now always include a `tagging` field: the exact value to send as the `x-amz-tagging` header on the presigned PUT, or the empty string `""` when `dataverse.files.<driverId>.disable-tagging` is set — meaning "do not send the header" (the URL was signed without it, and sending it anyway fails the signature check on MinIO-style stores, the case disable-tagging exists for). The dataverse-client-javascript SDK reads this and sends the header only for a non-empty value; a *missing* key falls back to `dv-state=temp` for compatibility with older servers. Multipart responses carry no `tagging` field: the server applies the temp tag itself at `CreateMultipartUpload`, and clients must not send `x-amz-tagging` on part uploads. **Operator note:** because multipart objects are now tagged too, a store that rejects object tagging needs `disable-tagging` set for multipart uploads to work — previously only single-part uploads required it (multipart objects were silently left untagged, escaping temp-object lifecycle cleanup). A rejected initiation now fails with an error message naming the setting.
- **Bundle cache-busting that actually changes per build.** The script tags now use a token derived from the bundle file's mtime, not the pinned `getVersion()` string — so browsers pick up new builds automatically without a hard-refresh. Falls back to `getVersion()` if the bundle file isn't reachable on the local filesystem (e.g. when the operator has rehosted the bundle off the WAR).
- **JSF partial-update survival.** PrimeFaces re-inserts the host `<div>` for the React mount on certain partial responses (e.g. when toggling between Table and Tree views). The standalone bundles now use a `MutationObserver` to detect when the host element is replaced and remount cleanly, so toggling no longer leaves the React tree orphaned on a removed div.
- **Hide the legacy "Done" button when the React uploader is wired.** The classic Done button below the upload component duplicated the React uploader's own finish action and confused testers; it's now suppressed when the uploader feature flag is active and direct upload is enabled.
- **Tree view toggle now appears for flat datasets when the React tree view is enabled.** Previously the JSF Tree/Table toggle (`DatasetPage.isFileTreeViewRequired`) was gated on the dataset having `directoryLabel` set on at least one file. Flat datasets (>1 file, no folders) didn't show the toggle, locking those users into the table view. With `dataverse.feature.react-tree-view=true`, the React tree's bulk-download UX (checkbox selection + client-side streaming zip with per-file resume / two-pass recovery) is a strict upgrade over the legacy server-zipped bulk download regardless of folder structure, so the gate drops to `> 1 file` on those installs. When the feature flag is off, the original "folders present" gate stays — the legacy PrimeFaces tree adds no value on a flat list, so behaviour is unchanged for installs that haven't opted in. Single-file and empty datasets still hide the toggle (no-op).
- **Create-dataset moves to a two-step file flow when the flag is on.** The React uploader is API-driven and needs the dataset to exist (PID assigned) before it can request upload URLs or register files. On the create-dataset page the dataset is still transient. When `dataverse.feature.react-uploader` is enabled, the create page therefore no longer renders the upload section; users save the metadata first, then add files on the persisted dataset's edit-files page where the React uploader runs against a real PID. This matches the SPA's flow, where the React uploader is the only uploader and "metadata first, files after" is the canonical create flow. Instances that haven't enabled the feature flag keep the existing one-step "create + upload" UX with the legacy JSF widgets unchanged. We chose this over a fall-back-to-JSF approach because mixing two uploaders (legacy on create, React on edit) creates a divergent UX (different folder-upload handling, progress UI, file-list rendering).
- **Documentation.** A new guide page covers how to host the reusable component bundle and wire it into Dataverse: see [Reusable Frontend Components](https://guides.dataverse.org/en/latest/container/running/reusable-components.html). The matching frontend-side contract lives in the [`dataverse-frontend` repo](https://github.com/IQSS/dataverse-frontend/blob/develop/docs/reusable-components.md).

### Operator note: covering index migration

This release ships a new Flyway migration (`V6.10.1.2.sql`) that creates `ix_filemetadata_tree` over `(datasetversion_id, directorylabel, lower(label), datafile_id)` to keep the new tree endpoint's keyset paginator fast.

On large production deployments (multi-million-row `filemetadata`), plain `CREATE INDEX` takes an `ACCESS EXCLUSIVE` lock and stalls all writes for the duration of the build (potentially several minutes). Flyway runs migrations inside a transaction, which prevents using `CREATE INDEX CONCURRENTLY` in the migration file itself.

Recommended for large installs: pre-create the index out-of-band before deploying the new release:

```sql
CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_filemetadata_tree
ON filemetadata (datasetversion_id, directorylabel text_pattern_ops, lower(label), datafile_id);
```

The migration uses `CREATE INDEX IF NOT EXISTS`, so a pre-created index is a no-op when Flyway runs. Use this exact definition — `IF NOT EXISTS` matches by name only. As a safety net, `V6.10.1.3.sql` detects an existing `ix_filemetadata_tree` that lacks `text_pattern_ops` (for example one pre-created from an earlier draft of this note) and rebuilds it with the correct definition; indexes that already match — including ones built with `CONCURRENTLY` — are left untouched.

### LocalStack dev-stack notes

The `dev_localstack` storage profile in `docker-compose-dev.yml` ships with `upload-redirect=true` / `download-redirect=true`, so the browser PUTs/GETs to S3 directly. Two operator-side things had to be set up explicitly to make that path work in dev:

- Bucket-level CORS: the init script `conf/localstack/buckets.sh` now puts a permissive CORS rule on `mybucket` after creation. LocalStack (matching real AWS S3) ships with no default CORS rules; without this, the browser preflight returns 403.
- Hostname resolution: Dataverse signs the presigned URLs against `http://localstack:4566` (the docker-internal hostname). For the browser to use the same URL, add `127.0.0.1 localstack` to `/etc/hosts` on the developer's machine. The same applies to `keycloak.mydomain.com` for the OIDC redirect target. This is a fundamental property of presigned-URL networking, not a Dataverse bug.

### Prerequisites for using the React uploader

1. `dataverse.feature.api-session-auth=true` so the bundle can call the API with the user's session cookie. **For production, also enable `dataverse.feature.api-session-auth-hardening`** to mitigate CSRF risk via Origin/Referer + `X-Dataverse-CSRF-Token` enforcement.
2. The reusable component bundle must be reachable from the user's browser. The default setup (`dataverse.reusable-components.base-url=/reusable-components`) serves the bundle files baked into the WAR same-origin and needs no extra setup. Operators who want to host the bundle off the WAR (separate static-file server, CDN, etc.) can copy `webapp/reusable-components/` to their host of choice and point the setting at that URL.
3. `dataverse.siteUrl` must match the URL the browser actually uses, so that Origin/Referer checks pass when session-auth hardening is enabled.

### What didn't change

- File replace, batch operations, and any classic JSF panels render exactly as before when the flag is off.
- No new Java / Maven dependency on npm or Node tooling. The bundle ships as pre-built JS inside the WAR (`webapp/reusable-components/`).

### Cross-repo

This release pairs with:

- [`@iqss/dataverse-client-javascript`](https://github.com/IQSS/dataverse-client-javascript) for the `tagging` field on the upload destination response.
- [`dataverse-frontend`](https://github.com/IQSS/dataverse-frontend) is the source repo for the React uploader and tree-view bundles. The pre-built JS files in this PR were produced from that repo's build.
Loading