You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/08/22 07:20:07 UTC
(texera) branch main updated: chore(frontend, amber): remove the unreachable available-result event and orphaned list item (#7783)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 8ca9db2005 chore(frontend, amber): remove the unreachable available-result event and orphaned list item (#7783)
8ca9db2005 is described below
commit 8ca9db20051f1ba7e4ee59e164655cf55a6bd1ee
Author: Xinyuan Lin <xi...@uci.edu>
AuthorDate: Sat Aug 22 06:58:53 2026 +0000
chore(frontend, amber): remove the unreachable available-result event and orphaned list item (#7783)
### What changes were proposed in this PR?
Removes three unreachable pieces of code. **998 lines deleted, 1
added.**
**1. `WorkflowAvailableResultEvent` and its dangling client handler.**
Nothing in `src/main` constructs the event, and it is not among the 11
`classOf[...]` entries in `TexeraWebSocketEvent.scala`'s
`@JsonSubTypes`, so it is not reflectively dispatched either. Yet
`workflow-result.service.ts` still subscribed to
`"WorkflowAvailableResultEvent"` and routed it to
`handleCleanResultCache` — a handler the backend could never trigger.
A second signal that this path was never live: the TypeScript type
declared `availableOperators` as
`ReadonlyArray<OperatorAvailableResult>`, while both the Scala event and
the handler treat it as a keyed map (`Map[String,
OperatorAvailableResult]` / `Object.entries(...)`). The declared wire
shape matched neither producer nor consumer.
Removed: the Scala event, the frontend subscription,
`handleCleanResultCache`, the orphaned TS types and their
`TexeraWebsocketEventTypeMap` entry, and a then-unused `WebOutputMode`
import.
**2. `UserDatasetListItemComponent` is orphaned.** Its selector appears
repo-wide only in its own `@Component` declaration and its own spec's
test host. The single other reference was a standalone entry in
`app.module.ts`'s `imports` array with no consumer — and since
`declarations` is `[AppComponent]` only, being in `imports` merely
exposes it to `AppComponent`'s template, where the selector does not
appear. The dataset page moved to
`texera-card-item`/`texera-search-results` in #5947.
Checked beyond the selector, because a component can be reached
indirectly: only two `@NgModule`s exist in the app, no
`loadChildren`/`loadComponent` lazy routes reference it, and there is no
`createComponent`/`ngComponentOutlet`/`ComponentFactoryResolver`
instantiation.
Removed the `.ts`, `.html`, `.scss`, its spec, and both `app.module.ts`
lines.
**3. `DynamicSchemaService.dynamicSchemaExists`** — exactly one
occurrence repo-wide, its own declaration, while every sibling method
has 2 to 5 call sites.
### Two tests were removed, deliberately
Two cases in `workflow-result.service.spec.ts` existed *solely* to
exercise `handleCleanResultCache`, by pushing a synthetic event directly
onto the socket subject. Their own fixtures are corroborating evidence:
they pass `availableOperators` as a keyed object — matching the Scala
type and the handler — while the declared TS type said array. They could
not have been written from a real frame.
The `user-dataset-list-item` spec went with its component for the same
reason.
A stale scaladoc paragraph in `TexeraWebSocketEventSpec.scala` also
named this event, noting that "nothing in main constructs it, so pinning
its wire shape would only cement dead code" — an independent
confirmation of the finding from an earlier reviewer. That paragraph is
removed; the preceding one, which lists the five genuinely unregistered
producers, stays accurate. No count or reflection assertion depended on
it.
### Verification
- Full frontend suite: **200 files, 4708 passed, 1 skipped, 0 failed.**
- Amber `TexeraWebSocketEventSpec`: main and test compiled with no
errors, **10/10 passed**. Compiling clean is itself the proof that
nothing in amber referenced the deleted class.
- **AOT `ng build` succeeded** — this matters, because `ng test` and
`tsc --noEmit` both miss Angular template errors, so a removed component
could otherwise break the build silently. Bundle generation completed
with zero `NG####` diagnostics and zero errors referencing `src/`. The
build's exit code is 1 from exactly the 13 pre-existing monaco webpack
loader failures on `node_modules` assets, which are unrelated to this
change.
- `yarn format:ci` exits 0; `scalafmtCheck`, `Test/scalafmtCheck` and
`scalafixAll --check` all pass — no imports were left orphaned.
- Residual-reference greps for both removed symbols come back empty.
### Any related issues, documentation, discussions?
Closes #7782
### How was this PR tested?
```
npx ng test --watch=false
```
```
Test Files 200 passed (200)
Tests 4708 passed | 1 skipped (4709)
```
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 5)
---
.../event/WorkflowAvailableResultEvent.scala | 34 --
.../websocket/event/TexeraWebSocketEventSpec.scala | 4 -
frontend/src/app/app.module.ts | 2 -
.../user-dataset-list-item.component.html | 154 --------
.../user-dataset-list-item.component.scss | 87 -----
.../user-dataset-list-item.component.spec.ts | 424 ---------------------
.../user-dataset-list-item.component.ts | 180 ---------
.../dynamic-schema/dynamic-schema.service.ts | 4 -
.../workflow-result.service.spec.ts | 51 ---
.../workflow-result/workflow-result.service.ts | 47 +--
.../types/workflow-websocket.interface.ts | 12 -
11 files changed, 1 insertion(+), 998 deletions(-)
diff --git a/amber/src/main/scala/org/apache/texera/web/model/websocket/event/WorkflowAvailableResultEvent.scala b/amber/src/main/scala/org/apache/texera/web/model/websocket/event/WorkflowAvailableResultEvent.scala
deleted file mode 100644
index bc9e006cd3..0000000000
--- a/amber/src/main/scala/org/apache/texera/web/model/websocket/event/WorkflowAvailableResultEvent.scala
+++ /dev/null
@@ -1,34 +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.
- */
-
-package org.apache.texera.web.model.websocket.event
-
-import org.apache.texera.web.model.websocket.event.WorkflowAvailableResultEvent.OperatorAvailableResult
-import org.apache.texera.web.service.ExecutionResultService.WebOutputMode
-
-object WorkflowAvailableResultEvent {
- case class OperatorAvailableResult(
- cacheValid: Boolean,
- outputMode: WebOutputMode
- )
-}
-
-case class WorkflowAvailableResultEvent(
- availableOperators: Map[String, OperatorAvailableResult]
-) extends TexeraWebSocketEvent
diff --git a/amber/src/test/scala/org/apache/texera/web/model/websocket/event/TexeraWebSocketEventSpec.scala b/amber/src/test/scala/org/apache/texera/web/model/websocket/event/TexeraWebSocketEventSpec.scala
index 6a1e063d59..b66d4d0c99 100644
--- a/amber/src/test/scala/org/apache/texera/web/model/websocket/event/TexeraWebSocketEventSpec.scala
+++ b/amber/src/test/scala/org/apache/texera/web/model/websocket/event/TexeraWebSocketEventSpec.scala
@@ -163,10 +163,6 @@ class TexeraWebSocketEventSpec extends AnyFlatSpec with Matchers {
* `ExecutionStatsService` (duration), `RegionExecutionManager` (region state),
* `ExecutionReconfigurationService` (modify-logic completed), `ClusterListener` /
* `WorkflowWebsocketResource` (cluster status) and `Coordinator` (region update).
- *
- * `WorkflowAvailableResultEvent` is the sixth unregistered subtype but is deliberately
- * absent from this list: nothing in main constructs it, so pinning its wire shape would
- * only cement dead code.
*/
private val outboundOnlyEvents: List[(String, TexeraWebSocketEvent)] = List(
"ExecutionDurationUpdateEvent" -> ExecutionDurationUpdateEvent(1234L, isRunning = true),
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 19c94f3deb..0dfbd0c5ce 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -155,7 +155,6 @@ import { DatasetDetailComponent } from "./dashboard/component/user/user-dataset/
import { UserDatasetVersionFiletreeComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component";
import { UserDatasetFileRendererComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component";
import { NzSpinModule } from "ng-zorro-antd/spin";
-import { UserDatasetListItemComponent } from "./dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component";
import { NgxFileDropModule } from "ngx-file-drop";
import { NzTreeModule } from "ng-zorro-antd/tree";
import { NzTreeViewModule } from "ng-zorro-antd/tree-view";
@@ -322,7 +321,6 @@ registerLocaleData(en);
UserDatasetVersionCreatorComponent,
DatasetDetailComponent,
UserDatasetVersionFiletreeComponent,
- UserDatasetListItemComponent,
UserDatasetFileRendererComponent,
UserDatasetStagedObjectsListComponent,
NzModalCommentBoxComponent,
diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.html b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.html
deleted file mode 100644
index 1383680f8f..0000000000
--- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.html
+++ /dev/null
@@ -1,154 +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.
--->
-
-<nz-list-item class="dataset-list-item">
- <nz-list-item-meta class="dataset-item-meta">
- <!-- Margin need only when the checkbox is not visible -->
- <nz-list-item-meta-avatar [ngStyle]="{ 'margin-left': editable ? 'none' : '16px' }">
- <nz-avatar
- [ngStyle]="{ 'background-color': 'grey', 'vertical-align': 'middle' }"
- [nzGap]="4"
- [nzText]="'' + dataset.did"
- nzSize="default"></nz-avatar>
- </nz-list-item-meta-avatar>
-
- <!-- editable name of saved dataset -->
- <nz-list-item-meta-title class="meta-title-container">
- <div class="dataset-item-meta-title">
- <a
- *ngIf="!editingName; else customDatasetTitle "
- [routerLink]="USER_DATASET + '/' + dataset.did"
- class="dataset-name"
- >{{ dataset.name }}</a
- >
- <ng-template #customDatasetTitle>
- <input
- #customName
- (focusout)="confirmUpdateDatasetCustomName(customName.value)"
- (keyup.enter)="confirmUpdateDatasetCustomName(customName.value)"
- placeholder="{{ dataset.name }}"
- value="{{ dataset.name }}" />
- </ng-template>
- <button
- *ngIf="editable && entry.accessPrivilege === 'WRITE'"
- (click)="editingName = true"
- nz-button
- mat-card-title="Customize Dataset Name"
- nz-tooltip="Customize Dataset Name"
- nzSize="small"
- nzTooltipPlacement="bottom"
- nzType="text">
- <i
- nz-icon
- nzTheme="outline"
- nzType="edit"></i>
- </button>
- <button
- *ngIf="editable && entry.accessPrivilege === 'WRITE'"
- (click)="editingDescription = true"
- nz-button
- nz-tooltip="Add Description"
- nzSize="small"
- nzTooltipPlacement="bottom"
- nzType="text"
- class="add-description-btn">
- <i
- nz-icon
- nzTheme="outline"
- nzType="plus-square"></i>
- </button>
- <i
- class="dataset-is-owner-icon"
- *ngIf="entry.isOwner"
- nz-tooltip="You are the owner"
- nzTooltipPlacement="bottom"
- nz-icon
- nzTheme="outline"
- nzType="user"></i>
- <i
- *ngIf="!entry.isOwner"
- nz-tooltip="{{
- entry.accessPrivilege
- }} Access"
- nzTooltipPlacement="bottom"
- nz-icon
- nzTheme="outline"
- nzType="team"></i>
- </div>
- </nz-list-item-meta-title>
-
- <!-- editable description of saved dataset -->
- <nz-list-item-meta-description>
- <div class="dataset-item-meta-description">
- <label
- *ngIf="!editingDescription; else customDatasetDescription "
- (click)="editingDescription = editable && entry.accessPrivilege === 'WRITE'"
- class="dataset-description-label">
- {{ dataset.description }}
- </label>
- <ng-template #customDatasetDescription>
- <input
- title="Description"
- #customDescription
- (focusout)="confirmUpdateDatasetCustomDescription(customDescription.value)"
- (keyup.enter)="confirmUpdateDatasetCustomDescription(customDescription.value)"
- class="dataset-editable-description-input"
- value="{{ dataset.description }}"
- maxlength="500" />
- </ng-template>
- </div>
- </nz-list-item-meta-description>
- </nz-list-item-meta>
-
- <ul nz-list-item-actions>
- <nz-list-item-action>
- <button
- (click)="onClickOpenShareAccess()"
- nz-button
- nz-tooltip="Share the dataset {{
- dataset.name
- }} to others"
- nzTooltipPlacement="bottom"
- type="button">
- <i
- nz-icon
- nzTheme="outline"
- nzType="share-alt"></i>
- </button>
- </nz-list-item-action>
- <nz-list-item-action *ngIf="editable">
- <button
- (nzOnConfirm)="deleted.emit()"
- nz-popconfirm
- nzPopconfirmTitle="Confirm to delete this dataset."
- [disabled]="!entry.isOwner"
- class="delete-dataset-btn"
- nz-button
- nz-tooltip="Delete the dataset {{
- dataset.name
- }}"
- nzTooltipPlacement="bottom">
- <i
- nz-icon
- nzTheme="outline"
- nzType="delete"></i>
- </button>
- </nz-list-item-action>
- </ul>
-</nz-list-item>
diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.scss b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.scss
deleted file mode 100644
index daabfebd53..0000000000
--- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.scss
+++ /dev/null
@@ -1,87 +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.
- */
-
-@use "../../../section-style" as *;
-@use "../../../dashboard.component.scss" as *;
-
-/**
- * css for project label, shared by workflow section and file section
-**/
-.dataset-list-item {
- margin-bottom: 10px;
- min-height: 70px;
- padding: 5px 0 5px 0;
-
- .dataset-item-meta {
- margin: 0 20px;
- }
-
- .dataset-item-meta-title {
- display: flex;
- align-items: center;
-
- .dataset-name {
- color: black;
- font-size: 20px;
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
- text-align: center;
- }
-
- i {
- position: relative;
- font-size: 17px;
- margin-bottom: 6px;
- }
-
- i.dataset-is-owner-icon {
- margin-left: 7px;
- }
- }
-
- .dataset-item-meta-description {
- display: flex;
- align-items: center;
- padding: 2px 8px 2px 10px;
- margin-bottom: 8px;
-
- .dataset-description-label {
- font-size: 13px;
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
-
- display: inline-block;
- min-width: 17ch;
- border: 0 none;
- outline: none;
-
- &:hover {
- cursor: pointer;
- box-shadow: 0 0 0 1px rgb(202, 202, 202);
- }
- }
-
- .dataset-editable-description-input {
- margin-bottom: 5px;
- display: inline-block;
- min-width: 17ch;
- border: 0 none;
- outline: none;
- box-shadow: 0 0 0 2px #007bff;
- }
- }
-}
diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.spec.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.spec.ts
deleted file mode 100644
index b023f10f3f..0000000000
--- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.spec.ts
+++ /dev/null
@@ -1,424 +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 { Component, EventEmitter, ViewChild } from "@angular/core";
-import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { By } from "@angular/platform-browser";
-import { HttpClientTestingModule } from "@angular/common/http/testing";
-import { provideRouter } from "@angular/router";
-import { NzListComponent } from "ng-zorro-antd/list";
-import { NzModalService } from "ng-zorro-antd/modal";
-import { NzTooltipDirective } from "ng-zorro-antd/tooltip";
-import { of, throwError } from "rxjs";
-import type { Mocked } from "vitest";
-import { UserDatasetListItemComponent } from "./user-dataset-list-item.component";
-import { DatasetService } from "../../../../service/user/dataset/dataset.service";
-import { NotificationService } from "../../../../../common/service/notification/notification.service";
-import { ShareAccessComponent } from "../../share-access/share-access.component";
-import { DashboardDataset } from "../../../../type/dashboard-dataset.interface";
-import { commonTestProviders } from "../../../../../common/testing/test-utils";
-
-// UserDatasetListItemComponent is rooted at <nz-list-item>; instantiating it
-// outside an <nz-list> host throws "No provider found for NzListComponent".
-@Component({
- standalone: true,
- imports: [NzListComponent, UserDatasetListItemComponent],
- template: `
- <nz-list>
- <texera-user-dataset-list-item
- [entry]="entry"
- [editable]="editable"></texera-user-dataset-list-item>
- </nz-list>
- `,
-})
-class TestHostComponent {
- entry!: DashboardDataset;
- editable = true;
- @ViewChild(UserDatasetListItemComponent, { static: true }) inner!: UserDatasetListItemComponent;
-}
-
-function makeEntry(overrides: Partial<DashboardDataset> = {}): DashboardDataset {
- return {
- isOwner: true,
- ownerEmail: "owner@example.com",
- accessPrivilege: "WRITE",
- size: 0,
- dataset: {
- did: 1,
- ownerUid: 1,
- name: "dataset-1",
- isPublic: false,
- isDownloadable: true,
- storagePath: undefined,
- description: "original description",
- creationTime: 0,
- coverImage: undefined,
- },
- ...overrides,
- };
-}
-
-describe("UserDatasetListItemComponent", () => {
- let component: UserDatasetListItemComponent;
- let fixture: ComponentFixture<TestHostComponent>;
- let datasetService: Mocked<DatasetService>;
- let notificationService: Mocked<NotificationService>;
- let modalService: NzModalService;
-
- beforeEach(async () => {
- const datasetServiceSpy = {
- updateDatasetName: vi.fn(),
- updateDatasetDescription: vi.fn(),
- };
- const notificationServiceSpy = {
- error: vi.fn(),
- success: vi.fn(),
- info: vi.fn(),
- warning: vi.fn(),
- loading: vi.fn(),
- blank: vi.fn(),
- };
-
- await TestBed.configureTestingModule({
- imports: [TestHostComponent, HttpClientTestingModule],
- providers: [
- { provide: DatasetService, useValue: datasetServiceSpy },
- { provide: NotificationService, useValue: notificationServiceSpy },
- NzModalService,
- provideRouter([]),
- ...commonTestProviders,
- ],
- }).compileComponents();
-
- datasetService = TestBed.inject(DatasetService) as unknown as Mocked<DatasetService>;
- notificationService = TestBed.inject(NotificationService) as unknown as Mocked<NotificationService>;
- modalService = TestBed.inject(NzModalService);
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(TestHostComponent);
- fixture.componentInstance.entry = makeEntry();
- fixture.componentInstance.editable = true;
- fixture.detectChanges();
- component = fixture.componentInstance.inner;
- });
-
- it("should create", () => {
- expect(component).toBeTruthy();
- });
-
- describe("entry getter", () => {
- it("throws when accessed before being set", () => {
- // Build a bare component without going through the host (the host always
- // assigns entry on first change detection).
- const bare = new UserDatasetListItemComponent(
- {} as NzModalService,
- {} as DatasetService,
- {} as NotificationService
- );
- expect(() => bare.entry).toThrowError("entry property must be provided to UserDatasetListItemComponent.");
- });
-
- it("returns the value once set", () => {
- const entry = makeEntry({ accessPrivilege: "READ" });
- const bare = new UserDatasetListItemComponent(
- {} as NzModalService,
- {} as DatasetService,
- {} as NotificationService
- );
- bare.entry = entry;
- expect(bare.entry).toBe(entry);
- });
- });
-
- describe("dataset getter", () => {
- it("returns entry.dataset when present", () => {
- expect(component.dataset).toBe(component.entry.dataset);
- });
-
- it("throws when entry.dataset is missing", () => {
- const bare = new UserDatasetListItemComponent(
- {} as NzModalService,
- {} as DatasetService,
- {} as NotificationService
- );
- bare.entry = { ...makeEntry(), dataset: undefined as unknown as DashboardDataset["dataset"] };
- expect(() => bare.dataset).toThrowError(
- "Incorrect type of DashboardEntry provided to UserDatasetListItemComponent. Entry must be dataset."
- );
- });
- });
-
- describe("confirmUpdateDatasetCustomName", () => {
- it("is a no-op when the name has not changed", () => {
- const current = component.entry.dataset.name;
- component.confirmUpdateDatasetCustomName(current);
- expect(datasetService.updateDatasetName).not.toHaveBeenCalled();
- });
-
- it("updates the dataset name on success and clears editingName", () => {
- const newName = "renamed-dataset";
- component.editingName = true;
- datasetService.updateDatasetName.mockReturnValue(of({} as Response));
-
- component.confirmUpdateDatasetCustomName(newName);
-
- expect(datasetService.updateDatasetName).toHaveBeenCalledExactlyOnceWith(1, newName);
- expect(component.entry.dataset.name).toBe(newName);
- expect(component.editingName).toBe(false);
- });
-
- it("notifies the user on error and still clears editingName", () => {
- const originalName = component.entry.dataset.name;
- component.editingName = true;
- datasetService.updateDatasetName.mockReturnValue(throwError(() => new Error("boom")));
-
- component.confirmUpdateDatasetCustomName("renamed-dataset");
-
- expect(notificationService.error).toHaveBeenCalledExactlyOnceWith("boom");
- expect(component.entry.dataset.name).toBe(originalName);
- expect(component.editingName).toBe(false);
- });
-
- ["", "a/b", "has space", "名前"].forEach(invalidName => {
- it(`rejects the invalid name '${invalidName}' without calling the service`, () => {
- const originalName = component.entry.dataset.name;
- component.editingName = true;
-
- component.confirmUpdateDatasetCustomName(invalidName);
-
- expect(datasetService.updateDatasetName).not.toHaveBeenCalled();
- expect(notificationService.error).toHaveBeenCalledExactlyOnceWith(
- "Invalid dataset name: only letters, numbers, underscores, and hyphens are allowed (max 128 characters)"
- );
- expect(component.entry.dataset.name).toBe(originalName);
- expect(component.editingName).toBe(false);
- });
- });
-
- it("surfaces the backend error message when the rename is rejected", () => {
- const originalName = component.entry.dataset.name;
- component.editingName = true;
- datasetService.updateDatasetName.mockReturnValue(
- throwError(() => ({ error: { message: "Dataset with the same name already exists" } }))
- );
-
- component.confirmUpdateDatasetCustomName("renamed-dataset");
-
- expect(notificationService.error).toHaveBeenCalledExactlyOnceWith("Dataset with the same name already exists");
- expect(component.entry.dataset.name).toBe(originalName);
- expect(component.editingName).toBe(false);
- });
- });
-
- describe("confirmUpdateDatasetCustomDescription", () => {
- it("is a no-op when the description has not changed", () => {
- const current = component.entry.dataset.description;
- component.confirmUpdateDatasetCustomDescription(current);
- expect(datasetService.updateDatasetDescription).not.toHaveBeenCalled();
- });
-
- it("updates the dataset description on success and clears editingDescription", () => {
- const newDescription = "updated description";
- component.editingDescription = true;
- datasetService.updateDatasetDescription.mockReturnValue(of({} as Response));
-
- component.confirmUpdateDatasetCustomDescription(newDescription);
-
- expect(datasetService.updateDatasetDescription).toHaveBeenCalledExactlyOnceWith(1, newDescription);
- expect(component.entry.dataset.description).toBe(newDescription);
- expect(component.editingDescription).toBe(false);
- });
-
- it("notifies the user on error and still clears editingDescription", () => {
- const originalDescription = component.entry.dataset.description;
- component.editingDescription = true;
- datasetService.updateDatasetDescription.mockReturnValue(throwError(() => new Error("boom")));
-
- component.confirmUpdateDatasetCustomDescription("updated description");
-
- expect(notificationService.error).toHaveBeenCalledExactlyOnceWith("Update dataset description failed");
- expect(component.entry.dataset.description).toBe(originalDescription);
- expect(component.editingDescription).toBe(false);
- });
- });
-
- describe("onClickOpenShareAccess", () => {
- it("opens the share-access modal with the expected nzData and options", () => {
- const createSpy = vi.spyOn(modalService, "create").mockReturnValue({} as any);
-
- component.onClickOpenShareAccess();
-
- expect(createSpy).toHaveBeenCalledExactlyOnceWith({
- nzContent: ShareAccessComponent,
- nzData: {
- writeAccess: true,
- type: "dataset",
- id: 1,
- },
- nzFooter: null,
- nzTitle: "Share this dataset with others",
- nzCentered: true,
- });
- });
-
- it("sets writeAccess to false when accessPrivilege is not WRITE", () => {
- fixture.componentInstance.entry = makeEntry({ accessPrivilege: "READ" });
- fixture.detectChanges();
- const inner = fixture.componentInstance.inner;
- const createSpy = vi.spyOn(modalService, "create").mockReturnValue({} as any);
-
- inner.onClickOpenShareAccess();
-
- expect(createSpy).toHaveBeenCalledExactlyOnceWith(
- expect.objectContaining({
- nzData: expect.objectContaining({ writeAccess: false }),
- })
- );
- });
- });
-
- describe("inputs and outputs", () => {
- it("defaults editable to false", () => {
- const bare = new UserDatasetListItemComponent(
- {} as NzModalService,
- {} as DatasetService,
- {} as NotificationService
- );
- expect(bare.editable).toBe(false);
- });
-
- it("exposes deleted, duplicated, and refresh as EventEmitters", () => {
- expect(component.deleted).toBeInstanceOf(EventEmitter);
- expect(component.duplicated).toBeInstanceOf(EventEmitter);
- expect(component.refresh).toBeInstanceOf(EventEmitter);
- });
- });
- /**
- * Whether this row offers any editing is decided in the template, and by TWO conditions rather
- * than one: the list must be editable AND the viewer must hold WRITE on the dataset. The suite
- * above exercises the component's methods and never renders, so neither condition was pinned.
- */
- describe("rendered row", () => {
- /** Re-renders the host with the given entry and list-level editability. */
- function render(over: Partial<DashboardDataset> = {}, editable = true): HTMLElement {
- fixture.componentInstance.entry = makeEntry(over);
- fixture.componentInstance.editable = editable;
- fixture.detectChanges();
- component = fixture.componentInstance.inner;
- return fixture.nativeElement as HTMLElement;
- }
-
- /** Titles of every tooltip on the row; interpolated ones never reach the DOM as attributes. */
- function tooltipTitles(): unknown[] {
- return fixture.debugElement
- .queryAll(By.directive(NzTooltipDirective))
- .map(d => (d.injector.get(NzTooltipDirective) as NzTooltipDirective).directiveTitle);
- }
-
- function hasTooltip(pred: (t: string) => boolean): boolean {
- return tooltipTitles().some(t => typeof t === "string" && pred(t));
- }
-
- it("offers the editing controls to a writer on an editable list", () => {
- render({ accessPrivilege: "WRITE" }, true);
-
- expect(hasTooltip(t => t === "Customize Dataset Name")).toBe(true);
- expect(hasTooltip(t => t === "Add Description")).toBe(true);
- });
-
- it("withholds them from a reader, even on an editable list", () => {
- // READ access must not be offered a rename it cannot persist; the list being editable is not
- // on its own permission to change someone else's dataset.
- render({ accessPrivilege: "READ" }, true);
-
- expect(hasTooltip(t => t === "Customize Dataset Name")).toBe(false);
- expect(hasTooltip(t => t === "Add Description")).toBe(false);
- });
-
- it("withholds them on a non-editable list, even from a writer", () => {
- // Both controls carry the same pair of conditions, so both are asserted: checking only the
- // rename would let the add-description button lose its `editable` half unnoticed.
- render({ accessPrivilege: "WRITE" }, false);
-
- expect(hasTooltip(t => t === "Customize Dataset Name")).toBe(false);
- expect(hasTooltip(t => t === "Add Description")).toBe(false);
- });
-
- it("applies the same pair of conditions to the inline description", () => {
- render({ accessPrivilege: "READ" }, true);
-
- (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>(".dataset-description-label")?.click();
- fixture.detectChanges();
-
- expect(component.editingDescription).toBe(false);
- });
-
- it("keeps the inline description shut on a non-editable list, even for a writer", () => {
- // The other half of the same `&&`. Without this case the gate could be reduced to the
- // privilege check alone and every remaining test would still pass.
- render({ accessPrivilege: "WRITE" }, false);
-
- (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>(".dataset-description-label")?.click();
- fixture.detectChanges();
-
- expect(component.editingDescription).toBe(false);
- });
-
- it("opens the inline description for a writer", () => {
- render({ accessPrivilege: "WRITE" }, true);
-
- (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>(".dataset-description-label")?.click();
- fixture.detectChanges();
-
- expect(component.editingDescription).toBe(true);
- });
-
- it("marks a dataset the viewer owns, and only that marker", () => {
- // The two markers are mutually exclusive; showing both would tell an owner their own dataset
- // had been shared with them.
- render({ isOwner: true, accessPrivilege: "WRITE" });
-
- expect(hasTooltip(t => t === "You are the owner")).toBe(true);
- expect(hasTooltip(t => t.endsWith(" Access"))).toBe(false);
- });
-
- it("tells a non-owner what access they hold instead", () => {
- // The marker interpolates the privilege, so a reader and a writer are told different things.
- render({ isOwner: false, accessPrivilege: "READ" });
-
- expect(hasTooltip(t => t === "You are the owner")).toBe(false);
- expect(hasTooltip(t => t === "READ Access")).toBe(true);
- });
-
- it("swaps the name for an input once renaming starts", () => {
- const el = render({ accessPrivilege: "WRITE" }, true);
- expect(el.querySelector("nz-list-item-meta-title input")).toBeNull();
-
- component.editingName = true;
- fixture.detectChanges();
-
- const input = (fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>(
- "nz-list-item-meta-title input"
- )!;
- expect(input).not.toBeNull();
- expect(input.value).toBe(component.dataset.name);
- });
- });
-});
diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.ts
deleted file mode 100644
index e6e583c4f3..0000000000
--- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-list-item/user-dataset-list-item.component.ts
+++ /dev/null
@@ -1,180 +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 { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
-import { Component, EventEmitter, Input, Output } from "@angular/core";
-import { Dataset } from "../../../../../common/type/dataset";
-import { DatasetService, validateDatasetName } from "../../../../service/user/dataset/dataset.service";
-import { ShareAccessComponent } from "../../share-access/share-access.component";
-import { NotificationService } from "../../../../../common/service/notification/notification.service";
-import { extractErrorMessage } from "../../../../../common/util/error";
-import { NzModalService } from "ng-zorro-antd/modal";
-import { DashboardDataset } from "../../../../type/dashboard-dataset.interface";
-import { USER_DATASET } from "../../../../../app-routing.constant";
-import {
- NzListItemComponent,
- NzListItemMetaComponent,
- NzListItemMetaAvatarComponent,
- NzListItemMetaTitleComponent,
- NzListItemMetaDescriptionComponent,
- NzListItemActionsComponent,
- NzListItemActionComponent,
-} from "ng-zorro-antd/list";
-import { NgStyle, NgIf } from "@angular/common";
-import { NzAvatarComponent } from "ng-zorro-antd/avatar";
-import { RouterLink } from "@angular/router";
-import { NzSpaceCompactItemDirective } from "ng-zorro-antd/space";
-import { NzButtonComponent } from "ng-zorro-antd/button";
-import { ɵNzTransitionPatchDirective } from "ng-zorro-antd/core/transition-patch";
-import { NzTooltipDirective } from "ng-zorro-antd/tooltip";
-import { NzIconDirective } from "ng-zorro-antd/icon";
-import { NzWaveDirective } from "ng-zorro-antd/core/wave";
-import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm";
-
-@UntilDestroy()
-@Component({
- selector: "texera-user-dataset-list-item",
- templateUrl: "./user-dataset-list-item.component.html",
- styleUrls: ["./user-dataset-list-item.component.scss"],
- imports: [
- NzListItemComponent,
- NzListItemMetaComponent,
- NzListItemMetaAvatarComponent,
- NgStyle,
- NzAvatarComponent,
- NzListItemMetaTitleComponent,
- NgIf,
- RouterLink,
- NzSpaceCompactItemDirective,
- NzButtonComponent,
- ɵNzTransitionPatchDirective,
- NzTooltipDirective,
- NzIconDirective,
- NzListItemMetaDescriptionComponent,
- NzListItemActionsComponent,
- NzListItemActionComponent,
- NzWaveDirective,
- NzPopconfirmDirective,
- ],
-})
-export class UserDatasetListItemComponent {
- protected readonly USER_DATASET = USER_DATASET;
-
- private _entry?: DashboardDataset;
-
- @Output()
- refresh = new EventEmitter<void>();
-
- @Input()
- get entry(): DashboardDataset {
- if (!this._entry) {
- throw new Error("entry property must be provided to UserDatasetListItemComponent.");
- }
- return this._entry;
- }
-
- set entry(value: DashboardDataset) {
- this._entry = value;
- }
-
- get dataset(): Dataset {
- if (!this.entry.dataset) {
- throw new Error(
- "Incorrect type of DashboardEntry provided to UserDatasetListItemComponent. Entry must be dataset."
- );
- }
- return this.entry.dataset;
- }
-
- @Input() editable = false;
- @Output() deleted = new EventEmitter<void>();
- @Output() duplicated = new EventEmitter<void>();
-
- editingName = false;
- editingDescription = false;
-
- constructor(
- private modalService: NzModalService,
- private datasetService: DatasetService,
- private notificationService: NotificationService
- ) {}
-
- public confirmUpdateDatasetCustomName(name: string) {
- if (this.entry.dataset.name === name) {
- return;
- }
-
- const nameError = validateDatasetName(name);
- if (nameError) {
- this.notificationService.error(nameError);
- this.editingName = false;
- return;
- }
-
- if (this.entry.dataset.did)
- this.datasetService
- .updateDatasetName(this.entry.dataset.did, name)
- .pipe(untilDestroyed(this))
- .subscribe({
- next: () => {
- this.entry.dataset.name = name;
- this.editingName = false;
- },
- error: (err: unknown) => {
- this.notificationService.error(extractErrorMessage(err));
- this.editingName = false;
- },
- });
- }
-
- public confirmUpdateDatasetCustomDescription(description: string) {
- if (this.entry.dataset.description === description) {
- return;
- }
-
- if (this.entry.dataset.did)
- this.datasetService
- .updateDatasetDescription(this.entry.dataset.did, description)
- .pipe(untilDestroyed(this))
- .subscribe({
- next: () => {
- this.entry.dataset.description = description;
- this.editingDescription = false;
- },
- error: () => {
- this.notificationService.error("Update dataset description failed");
- this.editingDescription = false;
- },
- });
- }
-
- public onClickOpenShareAccess() {
- this.modalService.create({
- nzContent: ShareAccessComponent,
- nzData: {
- writeAccess: this.entry.accessPrivilege === "WRITE",
- type: "dataset",
- id: this.dataset.did,
- },
- nzFooter: null,
- nzTitle: "Share this dataset with others",
- nzCentered: true,
- });
- }
-}
diff --git a/frontend/src/app/workspace/service/dynamic-schema/dynamic-schema.service.ts b/frontend/src/app/workspace/service/dynamic-schema/dynamic-schema.service.ts
index 50c33e7e86..9425de4a60 100644
--- a/frontend/src/app/workspace/service/dynamic-schema/dynamic-schema.service.ts
+++ b/frontend/src/app/workspace/service/dynamic-schema/dynamic-schema.service.ts
@@ -102,10 +102,6 @@ export class DynamicSchemaService {
return this.dynamicSchemaMap;
}
- public dynamicSchemaExists(operatorID: string): boolean {
- return this.dynamicSchemaMap.has(operatorID);
- }
-
/**
* Based on the operatorID, get the current dynamic operator schema that is created through autocomplete
*/
diff --git a/frontend/src/app/workspace/service/workflow-result/workflow-result.service.spec.ts b/frontend/src/app/workspace/service/workflow-result/workflow-result.service.spec.ts
index e73f28b39a..7842661761 100644
--- a/frontend/src/app/workspace/service/workflow-result/workflow-result.service.spec.ts
+++ b/frontend/src/app/workspace/service/workflow-result/workflow-result.service.spec.ts
@@ -148,57 +148,6 @@ describe("WorkflowResultService", () => {
expect(service.hasPaginatedResult("op")).toBe(false);
});
- it("handleCleanResultCache drops operators no longer available and resets invalidated caches", () => {
- const ws = TestBed.inject(WorkflowWebsocketService);
- pushWsEvent(ws, {
- type: "WebResultUpdateEvent",
- updates: {
- goneData: snapshotUpdate([{ a: 1 }]),
- gonePag: paginationUpdate(3),
- resetData: snapshotUpdate([{ keep: 1 }]),
- },
- tableStats: {},
- });
- expect(service.getResultService("resetData")!.getCurrentResultSnapshot()).toEqual([{ keep: 1 }]);
-
- pushWsEvent(ws, {
- type: "WorkflowAvailableResultEvent",
- availableOperators: {
- keepPag: { cacheValid: true, outputMode: { type: "PaginationMode" } },
- resetData: { cacheValid: false, outputMode: { type: "SetSnapshotMode" } },
- },
- });
-
- // operators absent from the available set are dropped from both caches
- expect(service.hasResult("goneData")).toBe(false);
- expect(service.hasPaginatedResult("gonePag")).toBe(false);
- // a freshly-available operator gets a new (paginated) service
- expect(service.hasPaginatedResult("keepPag")).toBe(true);
- // an available-but-invalidated operator keeps its service but has its cache reset
- expect(service.hasResult("resetData")).toBe(true);
- expect(service.getResultService("resetData")!.getCurrentResultSnapshot()).toBeUndefined();
- });
-
- it("handleCleanResultCache reports removed and invalidated operators on the result-update stream", () => {
- const ws = TestBed.inject(WorkflowWebsocketService);
- pushWsEvent(ws, {
- type: "WebResultUpdateEvent",
- updates: { goneData: snapshotUpdate([{ a: 1 }]) },
- tableStats: {},
- });
-
- const updateEvents: Record<string, unknown>[] = [];
- service.getResultUpdateStream().subscribe(u => updateEvents.push(u));
-
- pushWsEvent(ws, {
- type: "WorkflowAvailableResultEvent",
- availableOperators: { resetPag: { cacheValid: false, outputMode: { type: "PaginationMode" } } },
- });
-
- expect(updateEvents.length).toBe(1);
- expect(updateEvents[0]).toEqual({ goneData: undefined, resetPag: undefined });
- });
-
it("determineOutputTypes distinguishes binary from non-binary paginated output", () => {
const ws = TestBed.inject(WorkflowWebsocketService);
pushWsEvent(ws, {
diff --git a/frontend/src/app/workspace/service/workflow-result/workflow-result.service.ts b/frontend/src/app/workspace/service/workflow-result/workflow-result.service.ts
index 26133a636e..89d4e453fa 100644
--- a/frontend/src/app/workspace/service/workflow-result/workflow-result.service.ts
+++ b/frontend/src/app/workspace/service/workflow-result/workflow-result.service.ts
@@ -28,7 +28,7 @@ import {
WorkflowResultUpdate,
} from "../../types/execute-workflow.interface";
import { WorkflowWebsocketService } from "../workflow-websocket/workflow-websocket.service";
-import { PaginatedResultEvent, WorkflowAvailableResultEvent } from "../../types/workflow-websocket.interface";
+import { PaginatedResultEvent } from "../../types/workflow-websocket.interface";
import { map, Observable, of, pairwise, ReplaySubject, Subject } from "rxjs";
import { v4 as uuid } from "uuid";
import { IndexableObject } from "../../types/result-table.interface";
@@ -57,9 +57,6 @@ export class WorkflowResultService {
this.handleResultUpdate(event.updates);
this.handleTableStatsUpdate(event.tableStats);
});
- this.wsService
- .subscribeToEvent("WorkflowAvailableResultEvent")
- .subscribe(event => this.handleCleanResultCache(event));
this.resultTableStats.next({});
}
@@ -117,48 +114,6 @@ export class WorkflowResultService {
this.resultClearedStream.next();
}
- private handleCleanResultCache(event: WorkflowAvailableResultEvent): void {
- const removedOrInvalidatedOperators = new Set<string>();
- // remove operators that no longer have results
- this.operatorResultServices.forEach((_, op) => {
- if (!(op in event.availableOperators)) {
- this.operatorResultServices.delete(op);
- removedOrInvalidatedOperators.add(op);
- }
- });
- this.paginatedResultServices.forEach((_, op) => {
- if (!(op in event.availableOperators)) {
- this.paginatedResultServices.delete(op);
- removedOrInvalidatedOperators.add(op);
- }
- });
- // for each operator that has results:
- Object.entries(event.availableOperators).forEach(availableOp => {
- const op = availableOp[0];
- const cacheValid = availableOp[1].cacheValid;
- const outputMode = availableOp[1].outputMode;
-
- // make sure to init or reuse result service for each operator
- const resultService = (() => {
- if (outputMode.type === "PaginationMode") {
- return this.getOrInitPaginatedResultService(op);
- } else {
- return this.getOrInitResultService(op);
- }
- })();
-
- // invalidate frontend cache if needed
- if (!cacheValid) {
- resultService.reset();
- removedOrInvalidatedOperators.add(op);
- }
- });
-
- const invalidatedOperatorsUpdate: Record<string, undefined> = {};
- removedOrInvalidatedOperators.forEach(op => (invalidatedOperatorsUpdate[op] = undefined));
- this.resultUpdateStream.next(invalidatedOperatorsUpdate);
- }
-
private handleResultUpdate(event: WorkflowResultUpdate): void {
Object.keys(event).forEach(operatorID => {
const update = event[operatorID];
diff --git a/frontend/src/app/workspace/types/workflow-websocket.interface.ts b/frontend/src/app/workspace/types/workflow-websocket.interface.ts
index afd5ea6f04..475a9a2734 100644
--- a/frontend/src/app/workspace/types/workflow-websocket.interface.ts
+++ b/frontend/src/app/workspace/types/workflow-websocket.interface.ts
@@ -22,7 +22,6 @@ import {
LogicalOperator,
LogicalPlan,
OperatorStatsUpdate,
- WebOutputMode,
WorkflowResultUpdateEvent,
} from "./execute-workflow.interface";
import { IndexableObject } from "./result-table.interface";
@@ -124,16 +123,6 @@ export type ResultExportResponse = Readonly<{
message: string;
}>;
-export type OperatorAvailableResult = Readonly<{
- operatorID: string;
- cacheValid: boolean;
- outputMode: WebOutputMode;
-}>;
-
-export type WorkflowAvailableResultEvent = Readonly<{
- availableOperators: ReadonlyArray<OperatorAvailableResult>;
-}>;
-
export type OperatorResultCacheStatus = "cache invalid" | "cache valid";
export interface CacheStatusUpdateEvent
@@ -233,7 +222,6 @@ export type TexeraWebsocketEventTypeMap = {
OperatorCurrentTuplesUpdateEvent: OperatorCurrentTuples;
PaginatedResultEvent: PaginatedResultEvent;
ResultExportResponse: ResultExportResponse;
- WorkflowAvailableResultEvent: WorkflowAvailableResultEvent;
CacheStatusUpdateEvent: CacheStatusUpdateEvent;
PythonExpressionEvaluateResponse: PythonExpressionEvaluateResponse;
WorkerAssignmentUpdateEvent: WorkerAssignmentUpdateEvent;