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-8693-fcaf27a561bc0d29bad052b191062019f75eafa1 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 662387be647006356ed45e45ab988140bf178b94 Author: Xinyuan Lin <[email protected]> AuthorDate: Sun Sep 27 22:07:51 2026 +0000 refactor(frontend): remove the legacy user-file types (#8693) ### What changes were proposed in this PR? Removes the frontend type layer for "user files", a dashboard entry kind the backend has not served since #2781. **+127/−337 lines.** There is no behaviour change for workflow, dataset or model entries. - **Types:** `DashboardFile`, `UserFile` and the duplicate `SortMethod` enum in `dashboard-file.interface.ts`. `FileUploadItem` stays, since the uploaders use it. - **Dashboard entry:** `isDashboardFile`, the file branch and `file` getter of `DashboardEntry`, and `SearchResultItem.file` with its two readers (search-bar `convertToName`, search-service result filter). - **Registry:** `FileResourceDescriptor` and its wiring in `ResourceRegistryService`, plus `EntityType.File`. - **Search types:** `"file"` in the search resource-type unions and their JSDoc. `DashboardResource` throws `Unknown resource type: file` for it. ### History | | | | --- | --- | | **Introduced by** | #756 (2020-05-26), "User System File Uploading" (`UserFile`). Wired into the dashboard by #1251 (2021-08-05, `DashboardFile`) and #1966 / #1982 (June 2023, unified search + `DashboardEntry` file branch) | | **Usage removed by** | #2781 (2024-08-20), "Remove File-Related Components". It deleted `UserFileResource`, `UserFileAccessResource`, the file search query builder and the user-file components | It has been dead for about two years: no file entry can reach the frontend. #8068 (2026-08-28) later registered `FileResourceDescriptor` for a kind that could no longer appear. > Reviewer note: most of the spec churn is not coverage loss. The File descriptor was the only registered kind without rename, description, share, download or publish support, so several specs used a File entry to reach those guard branches. They now reach the same branches with a stand-in descriptor instead: > - `stubBareDescriptor()` in the card-item and list-item specs; > - an injected `ModelResourceDescriptor` override in the registry spec; > - `UNPUBLISHABLE_DESCRIPTOR` in the share-access spec. > > Only the cases that asserted File-specific behaviour were deleted. ### Any related issues, documentation, discussions? Closes #8692 ### How was this PR tested? No new tests. The guard-branch specs are kept and re-pointed at a stand-in descriptor, as described above. From `frontend/`: - `npx ng test --watch=false --include='src/app/dashboard/**/*.spec.ts' --include='src/app/hub/**/*.spec.ts'`: 72 spec files, all pass. - `yarn format:ci`: clean. - `npx ng build`: success. To re-check: ``` git grep -nwE "DashboardFile|UserFile|isDashboardFile|FileResourceDescriptor" -- frontend # no hits ``` ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Claude Opus 5.5) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../card-item/card-item.component.spec.ts | 63 ++++++----------- .../user/list-item/list-item.component.spec.ts | 43 ++++++++---- .../user/search-bar/search-bar.component.spec.ts | 20 ------ .../user/search-bar/search-bar.component.ts | 2 - .../search-results.component.spec.ts | 4 +- .../share-access/share-access.component.spec.ts | 25 ++++++- .../resource-registry/file-resource.descriptor.ts | 33 --------- .../resource-registry.service.spec.ts | 79 ++++++++++------------ .../resource-registry/resource-registry.service.ts | 4 +- .../app/dashboard/service/user/search.service.ts | 12 ++-- .../dashboard/service/user/stub-search.service.ts | 4 +- .../src/app/dashboard/type/dashboard-entry.spec.ts | 40 +---------- frontend/src/app/dashboard/type/dashboard-entry.ts | 31 +-------- .../app/dashboard/type/dashboard-file.interface.ts | 34 ---------- .../app/dashboard/type/search-filter-parameters.ts | 4 +- frontend/src/app/dashboard/type/search-result.ts | 4 +- .../src/app/dashboard/type/type-predicates.spec.ts | 56 +-------------- frontend/src/app/dashboard/type/type-predicates.ts | 5 -- frontend/src/app/hub/service/hub.service.ts | 1 - 19 files changed, 127 insertions(+), 337 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.spec.ts b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.spec.ts index dd243bc9c8..69f036b19b 100644 --- a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.spec.ts @@ -47,6 +47,7 @@ import { GuiConfigService } from "../../../../../common/service/gui-config.servi import { DefaultView } from "src/app/dashboard/type/workflow-metadata.interface"; import { DatasetService, DEFAULT_DATASET_NAME } from "../../../../service/user/dataset/dataset.service"; import { DownloadService } from "src/app/dashboard/service/user/download/download.service"; +import { ResourceRegistryService } from "src/app/dashboard/service/user/resource-registry/resource-registry.service"; function makeWorkflowEntry(overrides: Partial<DashboardEntry> = {}): DashboardEntry { return { @@ -80,6 +81,19 @@ function makeDatasetEntry(overrides: Partial<DashboardEntry> = {}): DashboardEnt } as unknown as DashboardEntry; } +/** + * Makes the registry answer every lookup with a descriptor that has none of the optional + * capabilities. Every shipped kind can be renamed, re-described and shared, so the checks for those + * capabilities need one that cannot. + */ +function stubBareDescriptor(): void { + vi.spyOn(TestBed.inject(ResourceRegistryService), "get").mockReturnValue({ + type: EntityType.Workflow, + iconType: "project", + isOwner: () => true, + }); +} + describe("CardItemComponent", () => { let component: CardItemComponent; let fixture: ComponentFixture<CardItemComponent>; @@ -444,21 +458,6 @@ describe("CardItemComponent", () => { expect(component.isLiked).toBe(true); }); - it("initializeEntry uses the folder-open icon for a file entry", () => { - component.entry = { - id: 8, - name: "f", - type: "file", - likeCount: 0, - viewCount: 0, - isLiked: false, - } as unknown as DashboardEntry; - - component.initializeEntry(); - - expect(component.iconType).toBe("folder-open"); - }); - it("initializeEntry throws for an unexpected entry type", () => { component.entry = { id: 1, @@ -758,14 +757,8 @@ describe("CardItemComponent", () => { it("onClickOpenShareAccess does not open a modal for a non-shareable entry type", async () => { const modalService = TestBed.inject(NzModalService); const createSpy = vi.spyOn(modalService, "create"); - component.entry = { - id: 3, - name: "f", - type: "file", - likeCount: 0, - viewCount: 0, - isLiked: false, - } as unknown as DashboardEntry; + stubBareDescriptor(); + component.entry = makeWorkflowEntry({ id: 3 }); await component.onClickOpenShareAccess(); @@ -1086,22 +1079,6 @@ describe("CardItemComponent", () => { }); describe("guard paths", () => { - /** Files are the one registered kind with neither a rename nor a description endpoint. */ - function makeFileEntry(overrides: Partial<DashboardEntry> = {}): DashboardEntry { - return { - id: 3, - name: "notes.txt", - description: "", - type: EntityType.File, - accessibleUserIds: [], - likeCount: 0, - viewCount: 0, - isLiked: false, - size: 0, - ...overrides, - } as unknown as DashboardEntry; - } - it("ngOnChanges ignores a change set that does not carry the entry", () => { // initializeEntry resets the cover and the counters; re-running it on an unrelated input // change would discard a cover that had just finished loading. @@ -1129,8 +1106,9 @@ describe("CardItemComponent", () => { // mocked so a regression fails on the assertion rather than on a TypeError out of // updateProperty. workflowPersistService.updateWorkflowName.mockReturnValue(of({} as Response)); - component.entry = makeFileEntry({ name: "typed" }); - component.originalName = "notes.txt"; + stubBareDescriptor(); + component.entry = makeWorkflowEntry({ name: "typed" }); + component.originalName = "wf"; component.editingName = true; component.confirmUpdateCustomName("typed"); @@ -1144,7 +1122,8 @@ describe("CardItemComponent", () => { // Mocked so that a regression here fails on the assertion below rather than on a TypeError // thrown out of updateProperty. workflowPersistService.updateWorkflowDescription.mockReturnValue(of({} as Response)); - component.entry = makeFileEntry({ description: "typed" }); + stubBareDescriptor(); + component.entry = makeWorkflowEntry({ description: "typed" }); component.originalDescription = ""; component.editingDescription = true; diff --git a/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts b/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts index ce4a2310ef..997c0996b9 100644 --- a/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/list-item/list-item.component.spec.ts @@ -28,7 +28,7 @@ import { import { HttpClientTestingModule } from "@angular/common/http/testing"; import { NzModalService } from "ng-zorro-antd/modal"; import { of, Subject, throwError } from "rxjs"; -import { ActionType, HubService } from "../../../../hub/service/hub.service"; +import { ActionType, EntityType, HubService } from "../../../../hub/service/hub.service"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { RouterTestingModule } from "@angular/router/testing"; import { StubUserService } from "../../../../common/service/user/stub-user.service"; @@ -46,6 +46,20 @@ import { USER_DATASET, USER_WORKSPACE, } from "../../../../app-routing.constant"; +import { ResourceRegistryService } from "../../../service/user/resource-registry/resource-registry.service"; + +/** + * Makes the registry answer every lookup with a descriptor that has none of the optional + * capabilities. Every shipped kind can be shared and downloaded, so the checks for those + * capabilities need one that cannot. + */ +function stubBareDescriptor(): void { + vi.spyOn(TestBed.inject(ResourceRegistryService), "get").mockReturnValue({ + type: EntityType.Workflow, + iconType: "project", + isOwner: () => true, + }); +} describe("ListItemComponent", () => { let component: ListItemComponent; @@ -519,9 +533,6 @@ describe("ListItemComponent", () => { feed(entryOf({ type: "dataset", dataset: { isOwner: true } })); expect(component.iconType).toBe("database"); - - feed(entryOf({ type: "file" })); - expect(component.iconType).toBe("folder-open"); }); it("refuses an entry kind it does not know", () => { @@ -538,13 +549,19 @@ describe("ListItemComponent", () => { }); it("reduces a description to a plain preview, and blanks an empty one", () => { - feed(entryOf({ type: "file", description: undefined })); + feed(entryOf({ type: "dataset", dataset: { isOwner: true }, description: undefined })); expect(component.renderedDescription).toBe(""); - feed(entryOf({ type: "file", description: " " })); + feed(entryOf({ type: "dataset", dataset: { isOwner: true }, description: " " })); expect(component.renderedDescription).toBe(""); - feed(entryOf({ type: "file", description: "# Title with [a link](http://x) and\n*emphasis*" })); + feed( + entryOf({ + type: "dataset", + dataset: { isOwner: true }, + description: "# Title with [a link](http://x) and\n*emphasis*", + }) + ); expect(component.renderedDescription).toBe("Title with a link and emphasis"); }); @@ -596,7 +613,8 @@ describe("ListItemComponent", () => { it("opens nothing for an entry kind that cannot be shared", async () => { const create = vi.spyOn(modalService, "create"); - feed(entryOf({ type: "file" })); + stubBareDescriptor(); + feed(entryOf({ type: "workflow", workflow: { isOwner: true } })); await component.onClickOpenShareAccess(); @@ -669,7 +687,7 @@ describe("ListItemComponent", () => { }); it("ignores a change that is not the entry", () => { - feed(entryOf({ type: "file", description: "kept" })); + feed(entryOf({ type: "dataset", dataset: { isOwner: true }, description: "kept" })); const before = component.renderedDescription; component.ngOnChanges({ editable: {} as any }); @@ -722,7 +740,7 @@ describe("ListItemComponent", () => { it("downloads nothing for an entry that was never persisted", () => { const workflow = vi.spyOn(TestBed.inject(DownloadService), "downloadWorkflow"); - feed(entryOf({ type: "file", id: 0 })); + feed(entryOf({ type: "workflow", id: 0, workflow: { isOwner: true } })); component.onClickDownload(); @@ -877,7 +895,7 @@ describe("ListItemComponent", () => { expect(deleted).toBe(1); }); - it("offers the download button to workflows and datasets only", () => { + it("offers the download button only to kinds that can be downloaded", () => { const download = vi.spyOn(component, "onClickDownload").mockImplementation(() => {}); render(); @@ -887,7 +905,8 @@ describe("ListItemComponent", () => { render({ type: "dataset" }); expect(button("Download")).not.toBeNull(); - render({ type: "file" }); + stubBareDescriptor(); + render(); expect(button("Download")).toBeNull(); }); diff --git a/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.spec.ts b/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.spec.ts index 5160af331f..4bc8f1f8fe 100644 --- a/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.spec.ts @@ -171,26 +171,6 @@ describe("SearchBarComponent", () => { expect(component.convertToName(makeWorkflowItem("wf-name", 7))).toBe("wf-name"); }); - it("returns the file's name", () => { - const item: SearchResultItem = { - resourceType: "file", - file: { - ownerEmail: "[email protected]", - accessLevel: "WRITE", - file: { - fid: 1, - ownerUid: 1, - name: "file-name", - size: 0, - path: "", - description: "", - uploadTime: 0, - }, - } as any, - }; - expect(component.convertToName(item)).toBe("file-name"); - }); - it("returns the dataset's name", () => { const item: SearchResultItem = { resourceType: "dataset", diff --git a/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.ts b/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.ts index 9238731060..c768ee56d6 100644 --- a/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.ts +++ b/frontend/src/app/dashboard/component/user/search-bar/search-bar.component.ts @@ -142,8 +142,6 @@ export class SearchBarComponent { convertToName(resultItem: SearchResultItem): string { if (resultItem.workflow) { return new DashboardEntry(resultItem.workflow).name; - } else if (resultItem.file) { - return new DashboardEntry(resultItem.file).name; } else if (resultItem.dataset) { return new DashboardEntry(resultItem.dataset).name; } else { diff --git a/frontend/src/app/dashboard/component/user/search-results/search-results.component.spec.ts b/frontend/src/app/dashboard/component/user/search-results/search-results.component.spec.ts index d1445ccd7c..df98d5ebe6 100644 --- a/frontend/src/app/dashboard/component/user/search-results/search-results.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/search-results/search-results.component.spec.ts @@ -129,7 +129,7 @@ describe("SearchResultsComponent", () => { it("clears entries and stores the provided loadMoreFunction", async () => { component.entries = [makeEntry("workflow", 1), makeEntry("dataset", 2)]; - const loadMoreFunction: LoadMoreFunction = vi.fn(async () => ({ entries: [makeEntry("file", 3)], more: false })); + const loadMoreFunction: LoadMoreFunction = vi.fn(async () => ({ entries: [makeEntry("model", 3)], more: false })); component.reset(loadMoreFunction); expect(component.entries).toEqual([]); @@ -138,7 +138,7 @@ describe("SearchResultsComponent", () => { // The stored function is the one loadMore invokes. await component.loadMore(); expect(loadMoreFunction).toHaveBeenCalledTimes(1); - expect(component.entries).toEqual([makeEntry("file", 3)]); + expect(component.entries).toEqual([makeEntry("model", 3)]); }); }); diff --git a/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts b/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts index 94f20b2cbd..ec41db898c 100644 --- a/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts +++ b/frontend/src/app/dashboard/component/user/share-access/share-access.component.spec.ts @@ -36,14 +36,29 @@ import { DatasetService } from "../../../service/user/dataset/dataset.service"; import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service"; import { Privilege } from "../../../type/share-access.interface"; +import { ResourceRegistryService } from "../../../service/user/resource-registry/resource-registry.service"; +import { ResourceDescriptor } from "../../../type/resource-descriptor"; +import { EntityType } from "../../../../hub/service/hub.service"; interface SetupOptions { type?: string; id?: number; inWorkspace?: boolean; currentEmail?: string | undefined; + /** What the registry answers for the kind, in place of the shipped descriptor. */ + descriptor?: ResourceDescriptor; } +/** + * A registered kind that cannot be published. Every shipped kind can, so the checks for publishing + * need this one. + */ +const UNPUBLISHABLE_DESCRIPTOR: ResourceDescriptor = { + type: EntityType.Dataset, + iconType: "database", + isOwner: () => true, +}; + describe("ShareAccessComponent", () => { let gmailSpy: { sendEmail: ReturnType<typeof vi.fn> }; let accessServiceSpy: { @@ -105,6 +120,9 @@ describe("ShareAccessComponent", () => { { provide: WorkflowActionService, useValue: workflowActionSpy }, ], }); + if (opts.descriptor) { + vi.spyOn(TestBed.inject(ResourceRegistryService), "find").mockReturnValue(opts.descriptor); + } fixture = TestBed.createComponent(ShareAccessComponent); fixture.detectChanges(); return fixture.componentInstance; @@ -215,8 +233,8 @@ describe("ShareAccessComponent", () => { expect(c.isPublic).toBe(true); }); - it("does not query publish state for non-workflow/dataset types", () => { - setupComponent({ type: "file", id: 4 }); + it("does not query publish state for a kind that cannot be published", () => { + setupComponent({ type: "dataset", id: 4, descriptor: UNPUBLISHABLE_DESCRIPTOR }); expect(workflowPersistSpy.getWorkflowIsPublished).not.toHaveBeenCalled(); expect(datasetServiceSpy.getDataset).not.toHaveBeenCalled(); }); @@ -601,10 +619,11 @@ describe("ShareAccessComponent", () => { }); it("does nothing for a registered kind that cannot be published", () => { - const c = setupComponent({ type: "file", id: 4 }); + const c = setupComponent({ type: "dataset", id: 4, descriptor: UNPUBLISHABLE_DESCRIPTOR }); c.setPublished(true); expect(c.isPublic).toBeNull(); expect(notificationSpy.success).not.toHaveBeenCalled(); + expect(datasetServiceSpy.updateDatasetPublicity).not.toHaveBeenCalled(); }); it("does nothing for a kind the registry does not carry at all", () => { diff --git a/frontend/src/app/dashboard/service/user/resource-registry/file-resource.descriptor.ts b/frontend/src/app/dashboard/service/user/resource-registry/file-resource.descriptor.ts deleted file mode 100644 index ba90117b27..0000000000 --- a/frontend/src/app/dashboard/service/user/resource-registry/file-resource.descriptor.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Injectable } from "@angular/core"; -import { ResourceDescriptor } from "../../../type/resource-descriptor"; -import { EntityType } from "../../../../hub/service/hub.service"; - -@Injectable({ - providedIn: "root", -}) -export class FileResourceDescriptor implements ResourceDescriptor { - readonly type = EntityType.File; - readonly iconType = "folder-open"; - // Files have no page of their own, so entries of this kind stay unrouted. - - isOwner = (): boolean => true; -} diff --git a/frontend/src/app/dashboard/service/user/resource-registry/resource-registry.service.spec.ts b/frontend/src/app/dashboard/service/user/resource-registry/resource-registry.service.spec.ts index 4e83312d7a..72f4d042a3 100644 --- a/frontend/src/app/dashboard/service/user/resource-registry/resource-registry.service.spec.ts +++ b/frontend/src/app/dashboard/service/user/resource-registry/resource-registry.service.spec.ts @@ -28,7 +28,8 @@ import { DatasetService } from "../dataset/dataset.service"; import { ModelService } from "../model/model.service"; import { WorkflowPersistService } from "../../../../common/service/workflow-persist/workflow-persist.service"; import { DownloadService } from "../download/download.service"; -import { FileResourceDescriptor } from "./file-resource.descriptor"; +import { ModelResourceDescriptor } from "./model-resource.descriptor"; +import { ResourceDescriptor } from "../../../type/resource-descriptor"; import { HUB_DATASET_RESULT_DETAIL, HUB_MODEL_RESULT_DETAIL, @@ -151,7 +152,6 @@ describe("ResourceRegistryService", () => { it("resolves every kind the dashboard renders", () => { expect(registry.get(EntityType.Workflow).iconType).toBe("project"); expect(registry.get(EntityType.Dataset).iconType).toBe("database"); - expect(registry.get(EntityType.File).iconType).toBe("folder-open"); expect(registry.get(EntityType.Model).iconType).toBe(MODEL_ICON); }); @@ -169,38 +169,29 @@ describe("ResourceRegistryService", () => { // ─── capability checks ──────────────────────────────────────────────────── - it("exposes rename and description only for the kinds that support them", () => { + it("exposes rename and description for every kind", () => { for (const type of [EntityType.Workflow, EntityType.Dataset, EntityType.Model]) { expect(registry.get(type).rename).toBeDefined(); expect(registry.get(type).updateDescription).toBeDefined(); } - for (const type of [EntityType.File]) { - expect(registry.get(type).rename).toBeUndefined(); - expect(registry.get(type).updateDescription).toBeUndefined(); - } }); - it("offers a download and a file preview only for the kinds that hold files", () => { + it("offers a download for every kind and a file preview only for the kinds that hold files", () => { for (const type of [EntityType.Workflow, EntityType.Dataset, EntityType.Model]) { expect(registry.get(type).download).toBeDefined(); } - for (const type of [EntityType.File]) { - expect(registry.get(type).download).toBeUndefined(); - } // Workflows are one file, not a version tree, so nothing previews them. expect(registry.get(EntityType.Workflow).retrieveSingleFile).toBeUndefined(); expect(registry.get(EntityType.Dataset).retrieveSingleFile).toBeDefined(); expect(registry.get(EntityType.Model).retrieveSingleFile).toBeDefined(); }); - it("offers publishing and covers only to the kinds the backend supports", () => { + it("offers publishing to every kind and covers only to the file-backed kinds", () => { for (const type of [EntityType.Workflow, EntityType.Dataset, EntityType.Model]) { expect(registry.get(type).retrieveOwners).toBeDefined(); expect(registry.get(type).isPublic).toBeDefined(); expect(registry.get(type).setPublished).toBeDefined(); } - expect(registry.get(EntityType.File).isPublic).toBeUndefined(); - expect(registry.get(EntityType.File).setPublished).toBeUndefined(); // A workflow cover is a data URL on the entry, so only the file-backed kinds resolve one. expect(registry.get(EntityType.Workflow).coverUrl).toBeUndefined(); expect(registry.get(EntityType.Dataset).coverUrl).toBeDefined(); @@ -271,8 +262,6 @@ describe("ResourceRegistryService", () => { expect(registry.get(EntityType.Workflow).isOwner(entry({ workflow: { isOwner: false } }))).toBe(false); expect(registry.get(EntityType.Dataset).isOwner(entry({ dataset: { isOwner: true } }))).toBe(true); expect(registry.get(EntityType.Model).isOwner(entry({ model: { isOwner: false } }))).toBe(false); - // File entries carry no ownership payload at all, so theirs must not read one. - expect(registry.get(EntityType.File).isOwner(entry({}))).toBe(true); }); // ─── entryLink ──────────────────────────────────────────────────────────── @@ -295,20 +284,12 @@ describe("ResourceRegistryService", () => { expect(registry.entryLink(model, 99)).toEqual([HUB_MODEL_RESULT_DETAIL, "9"]); }); - it("leaves an unroutable or unsaved entry unlinked", () => { - expect(registry.entryLink(entry({ type: EntityType.File, id: 8 }), 42)).toEqual([]); - expect(registry.entryLink(entry({ type: EntityType.Dataset, id: undefined }), 42)).toEqual([]); - expect(registry.entryLink(entry({ type: EntityType.Workflow, id: "draft" }), 42)).toEqual([]); - }); - /** - * `hubRoute` is optional on the descriptor contract, and the shipped kinds happen to declare - * both routes or neither — so nothing had ever asked what a private-page-only kind links to. - * The answer must not depend on the viewer: with nowhere else to send them, the private page is - * the only link there is, and the access check further down would otherwise route an outsider to - * `undefined`. Descriptors reach the registry by injection, so the kind is supplied as one. + * Both routes are optional on the descriptor contract, but every shipped kind declares both, so + * only a stand-in descriptor can exercise a kind that lacks one. Descriptors reach the registry by + * injection, so the stand-in is supplied in place of the model descriptor. */ - it("links a kind with a private page and no hub page straight to its private page", () => { + const registryWithModelAs = (descriptor: ResourceDescriptor): ResourceRegistryService => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], @@ -317,24 +298,38 @@ describe("ResourceRegistryService", () => { { provide: WorkflowPersistService, useValue: workflowPersistService }, { provide: DatasetService, useValue: datasetService }, { provide: ModelService, useValue: modelService }, - { - provide: FileResourceDescriptor, - useValue: { - type: EntityType.File, - iconType: "folder-open", - privateRoute: "/private-files", - isOwner: () => true, - }, - }, + { provide: ModelResourceDescriptor, useValue: descriptor }, ...commonTestProviders, ], }); - const privatePageOnly = TestBed.inject(ResourceRegistryService); - const file = entry({ type: EntityType.File, id: 7, accessibleUserIds: [42] }); + return TestBed.inject(ResourceRegistryService); + }; + + it("leaves an unroutable or unsaved entry unlinked", () => { + expect(registry.entryLink(entry({ type: EntityType.Dataset, id: undefined }), 42)).toEqual([]); + expect(registry.entryLink(entry({ type: EntityType.Workflow, id: "draft" }), 42)).toEqual([]); + + const unrouted = registryWithModelAs({ type: EntityType.Model, iconType: MODEL_ICON, isOwner: () => true }); + expect(unrouted.entryLink(entry({ type: EntityType.Model, id: 8 }), 42)).toEqual([]); + }); + + /** + * The answer must not depend on the viewer: with nowhere else to send them, the private page is + * the only link there is, and the access check further down would otherwise route an outsider to + * `undefined`. + */ + it("links a kind with a private page and no hub page straight to its private page", () => { + const privatePageOnly = registryWithModelAs({ + type: EntityType.Model, + iconType: MODEL_ICON, + privateRoute: "/private-models", + isOwner: () => true, + }); + const model = entry({ type: EntityType.Model, id: 7, accessibleUserIds: [42] }); - expect(privatePageOnly.entryLink(file, 42)).toEqual(["/private-files", "7"]); + expect(privatePageOnly.entryLink(model, 42)).toEqual(["/private-models", "7"]); // Same link for a viewer with no access, and for an anonymous one. - expect(privatePageOnly.entryLink(file, 99)).toEqual(["/private-files", "7"]); - expect(privatePageOnly.entryLink(file, undefined)).toEqual(["/private-files", "7"]); + expect(privatePageOnly.entryLink(model, 99)).toEqual(["/private-models", "7"]); + expect(privatePageOnly.entryLink(model, undefined)).toEqual(["/private-models", "7"]); }); }); diff --git a/frontend/src/app/dashboard/service/user/resource-registry/resource-registry.service.ts b/frontend/src/app/dashboard/service/user/resource-registry/resource-registry.service.ts index 40ef1fbc86..a65ce89cf3 100644 --- a/frontend/src/app/dashboard/service/user/resource-registry/resource-registry.service.ts +++ b/frontend/src/app/dashboard/service/user/resource-registry/resource-registry.service.ts @@ -26,7 +26,6 @@ import { catchError, map } from "rxjs/operators"; import { OwnerScope } from "../../../type/owner-scope"; import { WorkflowResourceDescriptor } from "./workflow-resource.descriptor"; import { DatasetResourceDescriptor } from "./dataset-resource.descriptor"; -import { FileResourceDescriptor } from "./file-resource.descriptor"; import { ModelResourceDescriptor } from "./model-resource.descriptor"; /** @@ -43,12 +42,11 @@ export class ResourceRegistryService { constructor( workflow: WorkflowResourceDescriptor, dataset: DatasetResourceDescriptor, - file: FileResourceDescriptor, model: ModelResourceDescriptor ) { // Computing units are deliberately absent: nothing in the dashboard renders them as entries. this.descriptors = new Map<EntityType, ResourceDescriptor>( - [workflow, dataset, file, model].map(descriptor => [descriptor.type, descriptor]) + [workflow, dataset, model].map(descriptor => [descriptor.type, descriptor]) ); } diff --git a/frontend/src/app/dashboard/service/user/search.service.ts b/frontend/src/app/dashboard/service/user/search.service.ts index 04d65ff975..c0442e2f59 100644 --- a/frontend/src/app/dashboard/service/user/search.service.ts +++ b/frontend/src/app/dashboard/service/user/search.service.ts @@ -59,7 +59,7 @@ export class SearchService { * @param params - Additional search filter parameters. * @param start - The starting index for paginated results. * @param count - The number of results to retrieve. - * @param type - The type of resource to search for ("workflow", "dataset", "file", "model", or null (all resource type)). + * @param type - The type of resource to search for ("workflow", "dataset", "model", or null (all resource type)). * @param orderBy - Specifies the sorting method. * @param isLogin - Indicates if the user is logged in. * - `isLogin = true`: Use the authenticated search endpoint, retrieving both user-accessible and public resources based on `includePublic`. @@ -73,7 +73,7 @@ export class SearchService { params: SearchFilterParameters, start: number, count: number, - type: "workflow" | "file" | "dataset" | "model" | null, + type: "workflow" | "dataset" | "model" | null, orderBy: SortMethod, isLogin: boolean, includePublic: boolean = false @@ -106,9 +106,7 @@ export class SearchService { // and the search bar are unaffected. Not corrected here: that belongs in the // `LoadMoreFunction` contract shared by all five callers of `SearchResultsComponent.reset`, // and removing the backend half (#7461) ends the only condition that produces such rows. - results: result.results.filter( - item => item.workflow != null || item.file != null || item.dataset != null || item.model != null - ), + results: result.results.filter(item => item.workflow != null || item.dataset != null || item.model != null), })) ); } @@ -134,7 +132,7 @@ export class SearchService { * @param params Additional search filter parameters. * @param start The starting index for paginated results. * @param count The number of results to retrieve. - * @param type The type of resource to search for ("workflow", "dataset", "file", "model", or null (all resource type)). + * @param type The type of resource to search for ("workflow", "dataset", "model", or null (all resource type)). * @param orderBy Specifies the sorting method. * @param isLogin Indicates if the user is logged in. * @param includePublic Specifies whether to include public resources in the search results. @@ -149,7 +147,7 @@ export class SearchService { params: SearchFilterParameters, start: number, count: number, - type: "workflow" | "dataset" | "file" | "model" | null, + type: "workflow" | "dataset" | "model" | null, orderBy: SortMethod, isLogin: boolean, includePublic: boolean diff --git a/frontend/src/app/dashboard/service/user/stub-search.service.ts b/frontend/src/app/dashboard/service/user/stub-search.service.ts index 18e07d7ddf..10ee9c7264 100644 --- a/frontend/src/app/dashboard/service/user/stub-search.service.ts +++ b/frontend/src/app/dashboard/service/user/stub-search.service.ts @@ -43,7 +43,7 @@ export class StubSearchService { params: SearchFilterParameters, start: number, count: number, - type: "workflow" | "file" | "dataset" | "model" | null, + type: "workflow" | "dataset" | "model" | null, orderBy: SortMethod, isLogin: boolean = true, includePublic: boolean = false @@ -79,7 +79,7 @@ export class StubSearchService { params: SearchFilterParameters, start: number, count: number, - type: "workflow" | "dataset" | "file" | "model" | null, + type: "workflow" | "dataset" | "model" | null, orderBy: SortMethod, isLogin: boolean, includePublic: boolean diff --git a/frontend/src/app/dashboard/type/dashboard-entry.spec.ts b/frontend/src/app/dashboard/type/dashboard-entry.spec.ts index 76ad406673..5dcdd53db3 100644 --- a/frontend/src/app/dashboard/type/dashboard-entry.spec.ts +++ b/frontend/src/app/dashboard/type/dashboard-entry.spec.ts @@ -20,7 +20,6 @@ import { DashboardEntry } from "./dashboard-entry"; import { EntityType } from "../../hub/service/hub.service"; import { DashboardWorkflow } from "./dashboard-workflow.interface"; -import { DashboardFile } from "./dashboard-file.interface"; import { DashboardDataset } from "./dashboard-dataset.interface"; import { DashboardModel } from "./dashboard-model.interface"; import { DashboardWorkflowComputingUnit } from "../../common/type/workflow-computing-unit"; @@ -57,22 +56,6 @@ function makeWorkflow(coverImage: string | null = "http://example.com/cover.png" }; } -function makeFile(): DashboardFile { - return { - ownerEmail: "[email protected]", - accessLevel: "WRITE", - file: { - ownerUid: 30, - fid: 303, - size: 1234, - name: "data.csv", - path: "/files/data.csv", - description: "A sample file", - uploadTime: 1700000003000, - }, - }; -} - function makeDataset(): DashboardDataset { return { isOwner: false, @@ -173,21 +156,6 @@ describe("DashboardEntry", () => { expect(entry.coverImageUrl).toBeUndefined(); }); - it("maps a DashboardFile to the File entity and copies file fields", () => { - const entry = new DashboardEntry(makeFile()); - - expect(entry.type).toBe(EntityType.File); - expect(entry.id).toBe(303); - expect(entry.name).toBe("data.csv"); - expect(entry.description).toBe("A sample file"); - expect(entry.creationTime).toBe(1700000003000); - expect(entry.lastModifiedTime).toBe(1700000003000); - expect(entry.accessLevel).toBe("WRITE"); - expect(entry.ownerEmail).toBe("[email protected]"); - expect(entry.ownerId).toBe(30); - expect(entry.size).toBe(1234); - }); - it("maps a DashboardDataset to the Dataset entity and copies dataset fields", () => { const entry = new DashboardEntry(makeDataset()); @@ -307,13 +275,7 @@ describe("DashboardEntry", () => { it("workflow getter returns the value for a workflow entry and throws for others", () => { const workflowValue = makeWorkflow(); expect(new DashboardEntry(workflowValue).workflow).toBe(workflowValue); - expect(() => new DashboardEntry(makeFile()).workflow).toThrowError("Value is not of type DashboardWorkflow."); - }); - - it("file getter returns the value for a file entry and throws for others", () => { - const fileValue = makeFile(); - expect(new DashboardEntry(fileValue).file).toBe(fileValue); - expect(() => new DashboardEntry(makeWorkflow()).file).toThrowError("Value is not of type DashboardFile."); + expect(() => new DashboardEntry(makeDataset()).workflow).toThrowError("Value is not of type DashboardWorkflow."); }); it("dataset getter returns the value for a dataset entry and throws for others", () => { diff --git a/frontend/src/app/dashboard/type/dashboard-entry.ts b/frontend/src/app/dashboard/type/dashboard-entry.ts index 6a11ba0ef0..ca569a6e73 100644 --- a/frontend/src/app/dashboard/type/dashboard-entry.ts +++ b/frontend/src/app/dashboard/type/dashboard-entry.ts @@ -17,14 +17,12 @@ * under the License. */ -import { DashboardFile } from "./dashboard-file.interface"; import { DashboardWorkflow } from "./dashboard-workflow.interface"; import { DashboardDataset } from "./dashboard-dataset.interface"; import { DashboardModel } from "./dashboard-model.interface"; import { DashboardWorkflowComputingUnit } from "../../common/type/workflow-computing-unit"; import { isDashboardDataset, - isDashboardFile, isDashboardModel, isDashboardWorkflow, isDashboardWorkflowComputingUnit, @@ -57,9 +55,7 @@ export class DashboardEntry { accessibleUserIds: number[]; coverImageUrl?: string; - constructor( - public value: DashboardWorkflow | DashboardFile | DashboardDataset | DashboardModel | DashboardWorkflowComputingUnit - ) { + constructor(public value: DashboardWorkflow | DashboardDataset | DashboardModel | DashboardWorkflowComputingUnit) { if (isDashboardWorkflow(value)) { this.type = EntityType.Workflow; this.id = value.workflow.wid; @@ -79,24 +75,6 @@ export class DashboardEntry { this.isLiked = false; this.accessibleUserIds = []; this.coverImageUrl = value.coverImage ?? undefined; - } else if (isDashboardFile(value)) { - this.type = EntityType.File; - this.id = value.file.fid; - this.name = value.file.name; - this.description = value.file.description; - this.creationTime = value.file.uploadTime; - this.lastModifiedTime = value.file.uploadTime; - this.accessLevel = value.accessLevel; - this.ownerName = ""; - this.ownerEmail = value.ownerEmail; - this.ownerAvatar = ""; - this.ownerId = value.file.ownerUid; - this.size = value.file.size; - this.viewCount = 0; - this.cloneCount = 0; - this.likeCount = 0; - this.isLiked = false; - this.accessibleUserIds = []; } else if (isDashboardDataset(value)) { this.type = EntityType.Dataset; this.id = value.dataset.did; @@ -187,13 +165,6 @@ export class DashboardEntry { return this.value; } - get file(): DashboardFile { - if (!isDashboardFile(this.value)) { - throw new Error("Value is not of type DashboardFile."); - } - return this.value; - } - get dataset(): DashboardDataset { if (!isDashboardDataset(this.value)) { throw new Error("Value is not of type DashboardDataset"); diff --git a/frontend/src/app/dashboard/type/dashboard-file.interface.ts b/frontend/src/app/dashboard/type/dashboard-file.interface.ts index fa394904c2..7509e408f5 100644 --- a/frontend/src/app/dashboard/type/dashboard-file.interface.ts +++ b/frontend/src/app/dashboard/type/dashboard-file.interface.ts @@ -17,29 +17,6 @@ * under the License. */ -export interface DashboardFile - extends Readonly<{ - ownerEmail: string; - accessLevel: string; - file: UserFile; - }> {} - -/** - * This interface stores the information about the users' files. - * These information is used to locate the file for the operators. - * Corresponds to `src/main/scala/org/apache/texera/web/resource/dashboard/file/UserFileResource.scala` (backend); - * and `sql/texera_ddl.sql`, table `file` (database). - */ -export interface UserFile { - ownerUid: number; - fid: number; - size: number; - name: string; - path: string; - description: string; - uploadTime: number; -} - /** * This interface stores the information about the users' files when uploading. * These information is used to upload the file to the backend. @@ -52,14 +29,3 @@ export interface FileUploadItem { isUploadingFlag: boolean; restart: boolean; } - -/** - * This enum type helps indicate the method in which DashboardUserFileEntry[] is sorted - */ -export enum SortMethod { - NameAsc, - NameDesc, - SizeDesc, - UploadTimeAsc, - UploadTimeDesc, -} diff --git a/frontend/src/app/dashboard/type/search-filter-parameters.ts b/frontend/src/app/dashboard/type/search-filter-parameters.ts index 7ead364e4f..1aa785c25f 100644 --- a/frontend/src/app/dashboard/type/search-filter-parameters.ts +++ b/frontend/src/app/dashboard/type/search-filter-parameters.ts @@ -35,7 +35,7 @@ export const toQueryStrings = ( params: SearchFilterParameters, start?: number, count?: number, - type?: "workflow" | "file" | "dataset" | "model" | null, + type?: "workflow" | "dataset" | "model" | null, orderBy?: SortMethod ): string => { const toLocalDate = (date: Date) => @@ -84,7 +84,7 @@ export const searchTestEntries = ( keywords: string[], params: SearchFilterParameters, testEntries: DashboardEntry[], - type: "workflow" | "file" | "dataset" | "model" | null + type: "workflow" | "dataset" | "model" | null ): DashboardEntry[] => { const endOfDay = (date: Date) => { date.setHours(23); diff --git a/frontend/src/app/dashboard/type/search-result.ts b/frontend/src/app/dashboard/type/search-result.ts index 905aa47bc0..bccf7e8220 100644 --- a/frontend/src/app/dashboard/type/search-result.ts +++ b/frontend/src/app/dashboard/type/search-result.ts @@ -17,16 +17,14 @@ * under the License. */ -import { DashboardFile } from "./dashboard-file.interface"; import { DashboardWorkflow } from "./dashboard-workflow.interface"; import { DashboardDataset } from "./dashboard-dataset.interface"; import { DashboardModel } from "./dashboard-model.interface"; import { DashboardEntry } from "./dashboard-entry"; export interface SearchResultItem { - resourceType: "workflow" | "file" | "dataset" | "model" | "computing-unit"; + resourceType: "workflow" | "dataset" | "model" | "computing-unit"; workflow?: DashboardWorkflow; - file?: DashboardFile; dataset?: DashboardDataset; model?: DashboardModel; } diff --git a/frontend/src/app/dashboard/type/type-predicates.spec.ts b/frontend/src/app/dashboard/type/type-predicates.spec.ts index 6033636607..90a3894da7 100644 --- a/frontend/src/app/dashboard/type/type-predicates.spec.ts +++ b/frontend/src/app/dashboard/type/type-predicates.spec.ts @@ -17,14 +17,8 @@ * under the License. */ -import { - isDashboardDataset, - isDashboardFile, - isDashboardWorkflow, - isDashboardWorkflowComputingUnit, -} from "./type-predicates"; +import { isDashboardDataset, isDashboardWorkflow, isDashboardWorkflowComputingUnit } from "./type-predicates"; import { DashboardWorkflow } from "./dashboard-workflow.interface"; -import { DashboardFile } from "./dashboard-file.interface"; import { DashboardDataset } from "./dashboard-dataset.interface"; import { DashboardWorkflowComputingUnit } from "../../common/type/workflow-computing-unit"; import { ExecutionMode } from "../../common/type/workflow"; @@ -56,20 +50,6 @@ const workflowFixture: DashboardWorkflow = { coverImage: null, }; -const fileFixture: DashboardFile = { - ownerEmail: "[email protected]", - accessLevel: "READ", - file: { - ownerUid: 10, - fid: 7, - size: 1024, - name: "data.csv", - path: "/files/data.csv", - description: "A sample file", - uploadTime: 1700000000000, - }, -}; - const datasetFixture: DashboardDataset = { isOwner: false, ownerEmail: "[email protected]", @@ -141,38 +121,6 @@ describe("isDashboardWorkflow", () => { }); }); -describe("isDashboardFile", () => { - it("should return true for a realistic DashboardFile", () => { - expect(isDashboardFile(fileFixture)).toBe(true); - }); - - it("should return false for null and undefined", () => { - expect(isDashboardFile(null)).toBe(false); - expect(isDashboardFile(undefined)).toBe(false); - }); - - it("should return false for an empty object", () => { - expect(isDashboardFile({})).toBe(false); - }); - - it("should return false when ownerEmail is missing", () => { - expect(isDashboardFile({ file: fileFixture.file })).toBe(false); - }); - - it("should return false when file is missing", () => { - expect(isDashboardFile({ ownerEmail: "[email protected]" })).toBe(false); - }); - - it("should return false when ownerEmail is not a string", () => { - expect(isDashboardFile({ ownerEmail: 42, file: fileFixture.file })).toBe(false); - }); - - it("should return false when file is null", () => { - // A null payload must be rejected even though typeof null === "object". - expect(isDashboardFile({ ownerEmail: "[email protected]", file: null })).toBe(false); - }); -}); - describe("isDashboardDataset", () => { it("should return true for a realistic DashboardDataset", () => { expect(isDashboardDataset(datasetFixture)).toBe(true); @@ -227,14 +175,12 @@ describe("isDashboardWorkflowComputingUnit", () => { describe("type predicate cross-classification", () => { const fixtures: ReadonlyArray<[string, unknown, string]> = [ ["DashboardWorkflow fixture", workflowFixture, "isDashboardWorkflow"], - ["DashboardFile fixture", fileFixture, "isDashboardFile"], ["DashboardDataset fixture", datasetFixture, "isDashboardDataset"], ["DashboardWorkflowComputingUnit fixture", computingUnitFixture, "isDashboardWorkflowComputingUnit"], ]; const predicates: ReadonlyArray<[string, (value: unknown) => boolean]> = [ ["isDashboardWorkflow", isDashboardWorkflow], - ["isDashboardFile", isDashboardFile], ["isDashboardDataset", isDashboardDataset], ["isDashboardWorkflowComputingUnit", isDashboardWorkflowComputingUnit], ]; diff --git a/frontend/src/app/dashboard/type/type-predicates.ts b/frontend/src/app/dashboard/type/type-predicates.ts index cfd4603a35..92e7de423b 100644 --- a/frontend/src/app/dashboard/type/type-predicates.ts +++ b/frontend/src/app/dashboard/type/type-predicates.ts @@ -18,7 +18,6 @@ */ import { DashboardWorkflow } from "./dashboard-workflow.interface"; -import { DashboardFile } from "./dashboard-file.interface"; import { DashboardDataset } from "./dashboard-dataset.interface"; import { DashboardModel } from "./dashboard-model.interface"; import { DashboardWorkflowComputingUnit } from "../../common/type/workflow-computing-unit"; @@ -28,10 +27,6 @@ export function isDashboardWorkflow(value: any): value is DashboardWorkflow { return !!value && isNonNullObject(value.workflow); } -export function isDashboardFile(value: any): value is DashboardFile { - return !!value && typeof value.ownerEmail === "string" && isNonNullObject(value.file); -} - export function isDashboardDataset(value: any): value is DashboardDataset { return !!value && isNonNullObject(value.dataset); } diff --git a/frontend/src/app/hub/service/hub.service.ts b/frontend/src/app/hub/service/hub.service.ts index 7618616e21..c1d337b124 100644 --- a/frontend/src/app/hub/service/hub.service.ts +++ b/frontend/src/app/hub/service/hub.service.ts @@ -30,7 +30,6 @@ export enum EntityType { Workflow = "workflow", Dataset = "dataset", Model = "model", - File = "file", ComputingUnit = "computing-unit", }
