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-7318-86865f39926e82f5fba74574a79b05341f4e37b5 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 0f2cd49c39781d683f35e02547da45fbe5adc3c3 Author: Xinyuan Lin <[email protected]> AuthorDate: Tue Aug 4 23:32:46 2026 -0700 fix(amber, frontend): serve created datasets to the admin quota modal (#7318) ### What changes were proposed in this PR? The admin user-quota modal (`UserQuotaComponent` opened from the admin user list) fails to load its "created datasets" panel: the request 404s. **Root cause:** `AdminUserService.getCreatedDatasets()` targets `/admin/user/created_datasets`, but `AdminUserResource` never exposed that endpoint. The only `created_datasets` endpoint lives on `UserQuotaResource` (`/quota/created_datasets`), and it derives the uid from the authenticated session (`@Auth current_user`) with no `user_id` parameter — so it cannot serve "datasets of the user an admin is inspecting" (it would return the admin's own datasets instead). | | Before | After | |---|---|---| | Frontend request | `GET /admin/user/created_datasets` (no params) | `GET /admin/user/created_datasets?user_id=<uid>` | | Backend | no such route → 404 | `AdminUserResource.getCreatedDatasets(user_id)` → datasets owned by `user_id` | ``` admin quota modal ──> AdminUserService.getCreatedDatasets(uid) │ before: GET /admin/user/created_datasets ──> 404 (route missing) │ after: GET /admin/user/created_datasets?user_id=N ──> AdminUserResource │ └─> DatasetStatisticsUtils.getUserCreatedDatasets(N) ``` Changes: - `AdminUserResource.scala`: add `GET /admin/user/created_datasets?user_id=N` (ADMIN-only, like the sibling `created_workflows` / `access_workflows` / `user_quota_size` endpoints), delegating to `DatasetStatisticsUtils.getUserCreatedDatasets`. - `admin-user.service.ts`: `getCreatedDatasets(uid)` now sends the uid as the `user_id` query param (it previously accepted the uid and silently dropped it). ### Any related issues, documentation, discussions? Closes #7317 ### How was this PR tested? Written test-first (both new tests were confirmed failing before the fix): - `AdminUserResourceSpec`: two new tests for `getCreatedDatasets` — empty list for a user with no datasets, and only the queried user's datasets are returned (a second user's dataset is excluded). - `admin-user.service.spec.ts`: the `getCreatedDatasets` test now asserts the `user_id` query param is sent (previously asserted no params were sent). ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 5) --------- Signed-off-by: Xinyuan Lin <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> --- .../dashboard/admin/user/AdminUserResource.scala | 11 ++++++ .../admin/user/AdminUserResourceSpec.scala | 45 +++++++++++++++++++++- .../service/admin/user/admin-user.service.spec.ts | 6 +-- .../service/admin/user/admin-user.service.ts | 3 +- 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResource.scala index b50d8f3ea8..cb426f787b 100644 --- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResource.scala +++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResource.scala @@ -28,6 +28,7 @@ import org.apache.texera.dao.jooq.generated.tables.pojos.User import org.apache.texera.web.resource.EmailTemplate.createRoleChangeTemplate import org.apache.texera.web.resource.GmailResource.sendEmail import org.apache.texera.web.resource.dashboard.admin.user.AdminUserResource.userDao +import org.apache.texera.web.resource.dashboard.user.dataset.utils.DatasetStatisticsUtils.getUserCreatedDatasets import org.apache.texera.web.resource.dashboard.user.quota.UserQuotaResource._ import org.jasypt.util.password.StrongPasswordEncryptor @@ -126,6 +127,16 @@ class AdminUserResource { userDao.insert(newUser) } + @GET + @Path("/created_datasets") + @Produces(Array(MediaType.APPLICATION_JSON)) + def getCreatedDatasets(@QueryParam("user_id") user_id: Integer): List[DatasetQuota] = { + if (user_id == null) { + throw new BadRequestException("user_id is required") + } + getUserCreatedDatasets(user_id) + } + @GET @Path("/created_workflows") @Produces(Array(MediaType.APPLICATION_JSON)) diff --git a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResourceSpec.scala b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResourceSpec.scala index 741dbba464..fd35653c7a 100644 --- a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResourceSpec.scala +++ b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/admin/user/AdminUserResourceSpec.scala @@ -23,6 +23,7 @@ import org.apache.texera.dao.MockTexeraDB import org.apache.texera.dao.jooq.generated.Tables._ import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum} import org.apache.texera.dao.jooq.generated.tables.daos.{ + DatasetDao, UserDao, WorkflowDao, WorkflowExecutionsDao, @@ -31,6 +32,7 @@ import org.apache.texera.dao.jooq.generated.tables.daos.{ WorkflowVersionDao } import org.apache.texera.dao.jooq.generated.tables.pojos.{ + Dataset, User, Workflow, WorkflowExecutions, @@ -44,7 +46,7 @@ import org.scalatest.matchers.should.Matchers import java.sql.Timestamp import java.util.UUID -import javax.ws.rs.WebApplicationException +import javax.ws.rs.{BadRequestException, WebApplicationException} import scala.jdk.CollectionConverters._ class AdminUserResourceSpec @@ -59,6 +61,7 @@ class AdminUserResourceSpec private val testWid = 90000 + scala.util.Random.nextInt(5000) private var userDao: UserDao = _ + private var datasetDao: DatasetDao = _ private var workflowDao: WorkflowDao = _ private var workflowVersionDao: WorkflowVersionDao = _ private var workflowExecutionsDao: WorkflowExecutionsDao = _ @@ -70,6 +73,7 @@ class AdminUserResourceSpec override protected def beforeAll(): Unit = { initializeDBAndReplaceDSLContext() userDao = new UserDao(getDSLContext.configuration()) + datasetDao = new DatasetDao(getDSLContext.configuration()) workflowDao = new WorkflowDao(getDSLContext.configuration()) workflowVersionDao = new WorkflowVersionDao(getDSLContext.configuration()) workflowExecutionsDao = new WorkflowExecutionsDao(getDSLContext.configuration()) @@ -92,6 +96,10 @@ class AdminUserResourceSpec .execute() getDSLContext.deleteFrom(WORKFLOW_OF_USER).where(WORKFLOW_OF_USER.WID.eq(testWid)).execute() getDSLContext.deleteFrom(WORKFLOW).where(WORKFLOW.WID.eq(testWid)).execute() + getDSLContext + .deleteFrom(DATASET) + .where(DATASET.OWNER_UID.in(primaryUid, secondaryUid)) + .execute() getDSLContext.deleteFrom(USER).where(USER.UID.in(primaryUid, secondaryUid)).execute() // addUser() inserts an INACTIVE user with an auto-generated uid and a "User<millis>" name. getDSLContext @@ -124,6 +132,19 @@ class AdminUserResourceSpec workflow } + private def seedDataset(uid: Int): Dataset = { + val dataset = new Dataset + dataset.setOwnerUid(uid) + dataset.setName("admin_user_spec_ds_" + UUID.randomUUID().toString.substring(0, 8)) + dataset.setRepositoryName("repo-" + UUID.randomUUID().toString.substring(0, 8)) + dataset.setIsPublic(false) + dataset.setIsDownloadable(true) + dataset.setDescription("") + dataset.setCreationTime(new Timestamp(System.currentTimeMillis())) + datasetDao.insert(dataset) + dataset + } + private def seedExecution(uid: Int): WorkflowExecutions = { seedWorkflow() val version = new WorkflowVersion @@ -218,6 +239,28 @@ class AdminUserResourceSpec a[WebApplicationException] should be thrownBy resource.updateUser(edit) } + // ─── getCreatedDatasets ─────────────────────────────────────────────────── + + "getCreatedDatasets" should "return an empty list for a user with no datasets" in { + userDao.insert(makeUser(primaryUid, "dataset_user")) + resource.getCreatedDatasets(primaryUid) shouldBe empty + } + + it should "reject a missing user_id with a BadRequestException" in { + assertThrows[BadRequestException](resource.getCreatedDatasets(null)) + } + + it should "return only the datasets owned by the queried user" in { + userDao.insert(makeUser(primaryUid, "dataset_owner")) + userDao.insert(makeUser(secondaryUid, "other_owner")) + val owned = seedDataset(primaryUid) + seedDataset(secondaryUid) + + val created = resource.getCreatedDatasets(primaryUid) + created.map(_.name) shouldBe List(owned.getName) + created.head.size shouldBe 0L + } + // ─── getCreatedWorkflow ─────────────────────────────────────────────────── "getCreatedWorkflow" should "return an empty list for a user with no created workflows" in { diff --git a/frontend/src/app/dashboard/service/admin/user/admin-user.service.spec.ts b/frontend/src/app/dashboard/service/admin/user/admin-user.service.spec.ts index 19a8aa7976..746522b9ac 100644 --- a/frontend/src/app/dashboard/service/admin/user/admin-user.service.spec.ts +++ b/frontend/src/app/dashboard/service/admin/user/admin-user.service.spec.ts @@ -100,12 +100,12 @@ describe("AdminUserService", () => { req.flush([]); }); - it("getCreatedDatasets() GETs without a query param", () => { + it("getCreatedDatasets() sends the uid as a user_id query param", () => { service.getCreatedDatasets(9).subscribe(); - const req = httpMock.expectOne(USER_CREATED_DATASETS); + const req = httpMock.expectOne(r => r.url === USER_CREATED_DATASETS); expect(req.request.method).toEqual("GET"); - expect(req.request.params.keys().length).toEqual(0); + expect(req.request.params.get("user_id")).toEqual("9"); req.flush([]); }); diff --git a/frontend/src/app/dashboard/service/admin/user/admin-user.service.ts b/frontend/src/app/dashboard/service/admin/user/admin-user.service.ts index 481c5e5302..c7fb1b1a1c 100644 --- a/frontend/src/app/dashboard/service/admin/user/admin-user.service.ts +++ b/frontend/src/app/dashboard/service/admin/user/admin-user.service.ts @@ -68,7 +68,8 @@ export class AdminUserService { } public getCreatedDatasets(uid: number): Observable<ReadonlyArray<DatasetQuota>> { - return this.http.get<ReadonlyArray<DatasetQuota>>(`${USER_CREATED_DATASETS}`); + let params = new HttpParams().set("user_id", uid.toString()); + return this.http.get<ReadonlyArray<DatasetQuota>>(`${USER_CREATED_DATASETS}`, { params: params }); } public getCreatedWorkflows(uid: number): Observable<ReadonlyArray<Workflow>> {
