From dc7909f55f9d2dc7a2758f04712b873985bd5d28 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Fri, 10 Apr 2026 16:36:37 -0700 Subject: [PATCH 01/23] feat(dataset-selector): add dataset selector operator and property UI --- .../texera/amber/operator/LogicalOp.scala | 2 + .../dataset/DatasetSelectorSourceOpDesc.scala | 66 ++++++++++++++ .../dataset/DatasetSelectorSourceOpExec.scala | 54 ++++++++++++ frontend/src/app/app.module.ts | 2 + .../src/app/common/formly/formly-config.ts | 2 + .../dataset-version-selector.component.html | 45 ++++++++++ .../dataset-version-selector.component.ts | 83 ++++++++++++++++++ .../operator-property-edit-frame.component.ts | 4 + .../operator_images/DatasetSelector.png | Bin 0 -> 22499 bytes 9 files changed, 258 insertions(+) create mode 100644 common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDesc.scala create mode 100644 common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpExec.scala create mode 100644 frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html create mode 100644 frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts create mode 100644 frontend/src/assets/operator_images/DatasetSelector.png diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala index 931596b1bf8..7ccbb073d6e 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala @@ -75,6 +75,7 @@ import org.apache.texera.amber.operator.source.apis.twitter.v2.{ TwitterFullArchiveSearchSourceOpDesc, TwitterSearchSourceOpDesc } +import org.apache.texera.amber.operator.source.dataset.DatasetSelectorSourceOpDesc import org.apache.texera.amber.operator.source.fetcher.URLFetcherOpDesc import org.apache.texera.amber.operator.source.scan.FileScanSourceOpDesc import org.apache.texera.amber.operator.source.scan.arrow.ArrowSourceOpDesc @@ -158,6 +159,7 @@ trait StateTransferFunc new Type(value = classOf[IfOpDesc], name = "If"), new Type(value = classOf[SankeyDiagramOpDesc], name = "SankeyDiagram"), new Type(value = classOf[IcicleChartOpDesc], name = "IcicleChart"), + new Type(value = classOf[DatasetSelectorSourceOpDesc], name = "DatasetSelector"), new Type(value = classOf[CSVScanSourceOpDesc], name = "CSVFileScan"), // disabled the ParallelCSVScanSourceOpDesc so that it does not confuse user. it can be re-enabled when doing experiments. // new Type(value = classOf[ParallelCSVScanSourceOpDesc], name = "ParallelCSVFileScan"), diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDesc.scala new file mode 100644 index 00000000000..c0aa8a1d04a --- /dev/null +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDesc.scala @@ -0,0 +1,66 @@ +/* + * 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.amber.operator.source.dataset + +import com.fasterxml.jackson.annotation.JsonProperty +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle +import org.apache.texera.amber.core.executor.OpExecWithClassName +import org.apache.texera.amber.core.tuple.{AttributeType, Schema} +import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, WorkflowIdentity} +import org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc} +import org.apache.texera.amber.operator.LogicalOp +import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} +import org.apache.texera.amber.util.JSONUtils.objectMapper + +class DatasetSelectorSourceOpDesc extends LogicalOp { + + @JsonProperty(required = true) + @JsonSchemaTitle("Dataset") + var datasetVersionPath: String = _ + + override def getPhysicalOp( + workflowId: WorkflowIdentity, + executionId: ExecutionIdentity + ): PhysicalOp = + PhysicalOp + .sourcePhysicalOp( + workflowId, + executionId, + operatorIdentifier, + OpExecWithClassName( + "org.apache.texera.amber.operator.source.dataset.DatasetSelectorSourceOpExec", + objectMapper.writeValueAsString(this) + ) + ) + .withInputPorts(operatorInfo.inputPorts) + .withOutputPorts(operatorInfo.outputPorts) + .withPropagateSchema( + SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> Schema().add("filename", AttributeType.STRING))) + ) + + override def operatorInfo: OperatorInfo = + OperatorInfo( + userFriendlyName = "Dataset Selector", + operatorDescription = "Select a dataset version and output one filename tuple per file", + operatorGroupName = OperatorGroupConstants.INPUT_GROUP, + inputPorts = List.empty, + outputPorts = List(OutputPort()) + ) +} diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpExec.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpExec.scala new file mode 100644 index 00000000000..eee7bc40511 --- /dev/null +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpExec.scala @@ -0,0 +1,54 @@ +/* + * 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.amber.operator.source.dataset + +import org.apache.texera.amber.core.executor.SourceOperatorExecutor +import org.apache.texera.amber.core.storage.util.LakeFSStorageClient +import org.apache.texera.amber.core.tuple.TupleLike +import org.apache.texera.amber.util.JSONUtils.objectMapper +import org.apache.texera.dao.SqlServer +import org.apache.texera.dao.jooq.generated.tables.Dataset.DATASET +import org.apache.texera.dao.jooq.generated.tables.DatasetVersion.DATASET_VERSION +import org.apache.texera.dao.jooq.generated.tables.User.USER + +class DatasetSelectorSourceOpExec private[dataset] (descString: String) extends SourceOperatorExecutor { + private val desc: DatasetSelectorSourceOpDesc = objectMapper.readValue(descString, classOf[DatasetSelectorSourceOpDesc]) + + override def produceTuple(): Iterator[TupleLike] = { + val Seq(_, ownerEmail, datasetName, versionName) = + desc.datasetVersionPath.split("/").toSeq + + val (repositoryName, versionHash) = + SqlServer.getInstance().createDSLContext() + .select(DATASET.REPOSITORY_NAME, DATASET_VERSION.VERSION_HASH) + .from(DATASET) + .join(USER).on(USER.UID.eq(DATASET.OWNER_UID)) + .join(DATASET_VERSION).on(DATASET_VERSION.DID.eq(DATASET.DID)) + .where(USER.EMAIL.eq(ownerEmail)) + .and(DATASET.NAME.eq(datasetName)) + .and(DATASET_VERSION.NAME.eq(versionName)) + .fetchOne(r => (r.value1(), r.value2())) + + LakeFSStorageClient + .retrieveObjectsOfVersion(repositoryName, versionHash) + .map(obj => TupleLike("filename" -> s"${desc.datasetVersionPath}/${obj.getPath}")) + .iterator + } +} diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6a92d4be2e9..d3e3339f8f1 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -104,6 +104,7 @@ import { AgentPanelComponent } from "./workspace/component/agent-panel/agent-pan import { AgentChatComponent } from "./workspace/component/agent-panel/agent-chat/agent-chat.component"; import { AgentRegistrationComponent } from "./workspace/component/agent-panel/agent-registration/agent-registration.component"; import { InputAutoCompleteComponent } from "./workspace/component/input-autocomplete/input-autocomplete.component"; +import { DatasetVersionSelectorComponent } from "./workspace/component/dataset-version-selector/dataset-version-selector.component"; import { CollabWrapperComponent } from "./common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; import { TexeraCopilot } from "./workspace/service/copilot/texera-copilot"; import { NzSwitchModule } from "ng-zorro-antd/switch"; @@ -257,6 +258,7 @@ registerLocaleData(en); AgentChatComponent, AgentRegistrationComponent, InputAutoCompleteComponent, + DatasetVersionSelectorComponent, FileSelectionComponent, CollabWrapperComponent, AboutComponent, diff --git a/frontend/src/app/common/formly/formly-config.ts b/frontend/src/app/common/formly/formly-config.ts index d950bd3690c..8c814a5993e 100644 --- a/frontend/src/app/common/formly/formly-config.ts +++ b/frontend/src/app/common/formly/formly-config.ts @@ -27,6 +27,7 @@ import { PresetWrapperComponent } from "./preset-wrapper/preset-wrapper.componen import { InputAutoCompleteComponent } from "../../workspace/component/input-autocomplete/input-autocomplete.component"; import { CollabWrapperComponent } from "./collab-wrapper/collab-wrapper/collab-wrapper.component"; import { FormlyRepeatDndComponent } from "./repeat-dnd/repeat-dnd.component"; +import { DatasetVersionSelectorComponent } from "../../workspace/component/dataset-version-selector/dataset-version-selector.component"; /** * Configuration for using Json Schema with Formly. @@ -77,6 +78,7 @@ export const TEXERA_FORMLY_CONFIG = { { name: "multischema", component: MultiSchemaTypeComponent }, { name: "codearea", component: CodeareaCustomTemplateComponent }, { name: "inputautocomplete", component: InputAutoCompleteComponent, wrappers: ["form-field"] }, + { name: "datasetversionselector", component: DatasetVersionSelectorComponent, wrappers: ["form-field"] }, { name: "repeat-section-dnd", component: FormlyRepeatDndComponent }, ], wrappers: [ diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html new file mode 100644 index 00000000000..1a2ff4889b8 --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html @@ -0,0 +1,45 @@ + + + + + + + + + + + diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts new file mode 100644 index 00000000000..eb2b2b4338f --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts @@ -0,0 +1,83 @@ +/** + * 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 {ChangeDetectorRef, Component, OnInit} from "@angular/core"; +import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { DashboardDataset } from "../../../dashboard/type/dashboard-dataset.interface"; +import { DatasetVersion } from "../../../common/type/dataset"; +import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; + +@UntilDestroy() +@Component({ + selector: "texera-dataset-version-selector-template", + templateUrl: "./dataset-version-selector.component.html", +}) +export class DatasetVersionSelectorComponent extends FieldType implements OnInit { + datasets: ReadonlyArray = []; + datasetVersions: ReadonlyArray = []; + selectedDataset?: DashboardDataset; + selectedVersion?: DatasetVersion; + + constructor(private datasetService: DatasetService, private changeDetectorRef: ChangeDetectorRef) { + super(); + } + + ngOnInit(): void { + this.datasetService + .retrieveAccessibleDatasets() + .pipe(untilDestroyed(this)) + .subscribe(datasets => { + this.datasets = datasets; + const [_, ownerEmail, datasetName, versionName] = this.formControl.value.split("/") + if (versionName) { + this.selectedDataset = this.datasets.find( + dataset => + dataset.ownerEmail === ownerEmail && dataset.dataset.name === datasetName + ); + this.onDatasetChange() + } + }); + } + + onDatasetChange(): void { + if (this.selectedDataset) { + this.datasetService + .retrieveDatasetVersionList(this.selectedDataset.dataset.did!) + .pipe(untilDestroyed(this)) + .subscribe(versions => { + this.datasetVersions = versions; + this.selectedVersion = versions[0]; + this.onVersionChange(); + this.changeDetectorRef.detectChanges(); + }); + } else { + this.selectedVersion = undefined; + this.onVersionChange(); + } + } + + onVersionChange(): void { + this.formControl.setValue( + this.selectedDataset && this.selectedVersion + ? `/${this.selectedDataset?.ownerEmail}/${this.selectedDataset?.dataset?.name}/${this.selectedVersion?.name}` + : null + ); + } +} diff --git a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts index 5d457e9050e..a436d111423 100644 --- a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts +++ b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts @@ -453,6 +453,10 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On mappedField.type = "inputautocomplete"; } + if (mappedField.key == "datasetVersionPath") { + mappedField.type = "datasetversionselector"; + } + // if the title is python script (for Python UDF), then make this field a custom template 'codearea' if (mapSource?.description?.toLowerCase() === "input your code here") { if (mappedField.type) { diff --git a/frontend/src/assets/operator_images/DatasetSelector.png b/frontend/src/assets/operator_images/DatasetSelector.png new file mode 100644 index 0000000000000000000000000000000000000000..c570e230ac279de6d519208c9c0b974ef6d6ec4b GIT binary patch literal 22499 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 Cd|v$k literal 0 HcmV?d00001 From dd0b69022287f1a22577f69e4e79d7e7b3dafaeb Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Fri, 10 Apr 2026 17:02:50 -0700 Subject: [PATCH 02/23] update --- .../DatasetSelectorSourceOpDescSpec.scala | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDescSpec.scala diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDescSpec.scala new file mode 100644 index 00000000000..5ef53fb8d6f --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDescSpec.scala @@ -0,0 +1,43 @@ +/* + * 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.amber.operator.source.dataset + +import org.apache.texera.amber.core.tuple.AttributeType +import org.scalatest.flatspec.AnyFlatSpec + +class DatasetSelectorSourceOpDescSpec extends AnyFlatSpec { + + "DatasetSelectorSourceOpDesc" should "expose a filename output column" in { + val opDesc = new DatasetSelectorSourceOpDesc() + + val outputSchema = opDesc.getExternalOutputSchemas(Map.empty).values.head + + assert(outputSchema.getAttributes.length == 1) + assert(outputSchema.getAttribute("filename").getType == AttributeType.STRING) + } + + it should "use the expected operator metadata" in { + val opDesc = new DatasetSelectorSourceOpDesc() + + assert(opDesc.operatorInfo.userFriendlyName == "Dataset Selector") + assert(opDesc.operatorInfo.inputPorts.isEmpty) + assert(opDesc.operatorInfo.outputPorts.length == 1) + } +} From 4b95eaba765698eee011289e9e70565dfdd73ffa Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Fri, 10 Apr 2026 17:12:47 -0700 Subject: [PATCH 03/23] fix fmt --- .../dataset/DatasetSelectorSourceOpDesc.scala | 4 +++- .../dataset/DatasetSelectorSourceOpExec.scala | 16 +++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDesc.scala index c0aa8a1d04a..386d26e1e08 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpDesc.scala @@ -52,7 +52,9 @@ class DatasetSelectorSourceOpDesc extends LogicalOp { .withInputPorts(operatorInfo.inputPorts) .withOutputPorts(operatorInfo.outputPorts) .withPropagateSchema( - SchemaPropagationFunc(_ => Map(operatorInfo.outputPorts.head.id -> Schema().add("filename", AttributeType.STRING))) + SchemaPropagationFunc(_ => + Map(operatorInfo.outputPorts.head.id -> Schema().add("filename", AttributeType.STRING)) + ) ) override def operatorInfo: OperatorInfo = diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpExec.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpExec.scala index eee7bc40511..6a53da47672 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpExec.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/DatasetSelectorSourceOpExec.scala @@ -28,19 +28,25 @@ import org.apache.texera.dao.jooq.generated.tables.Dataset.DATASET import org.apache.texera.dao.jooq.generated.tables.DatasetVersion.DATASET_VERSION import org.apache.texera.dao.jooq.generated.tables.User.USER -class DatasetSelectorSourceOpExec private[dataset] (descString: String) extends SourceOperatorExecutor { - private val desc: DatasetSelectorSourceOpDesc = objectMapper.readValue(descString, classOf[DatasetSelectorSourceOpDesc]) +class DatasetSelectorSourceOpExec private[dataset] (descString: String) + extends SourceOperatorExecutor { + private val desc: DatasetSelectorSourceOpDesc = + objectMapper.readValue(descString, classOf[DatasetSelectorSourceOpDesc]) override def produceTuple(): Iterator[TupleLike] = { val Seq(_, ownerEmail, datasetName, versionName) = desc.datasetVersionPath.split("/").toSeq val (repositoryName, versionHash) = - SqlServer.getInstance().createDSLContext() + SqlServer + .getInstance() + .createDSLContext() .select(DATASET.REPOSITORY_NAME, DATASET_VERSION.VERSION_HASH) .from(DATASET) - .join(USER).on(USER.UID.eq(DATASET.OWNER_UID)) - .join(DATASET_VERSION).on(DATASET_VERSION.DID.eq(DATASET.DID)) + .join(USER) + .on(USER.UID.eq(DATASET.OWNER_UID)) + .join(DATASET_VERSION) + .on(DATASET_VERSION.DID.eq(DATASET.DID)) .where(USER.EMAIL.eq(ownerEmail)) .and(DATASET.NAME.eq(datasetName)) .and(DATASET_VERSION.NAME.eq(versionName)) From 9baf3927ed479f78a284b70b4d7d2a86352f3862 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Fri, 10 Apr 2026 17:14:41 -0700 Subject: [PATCH 04/23] fix fmt --- .../dataset-version-selector.component.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts index eb2b2b4338f..16403806556 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts @@ -17,7 +17,7 @@ * under the License. */ -import {ChangeDetectorRef, Component, OnInit} from "@angular/core"; +import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { DashboardDataset } from "../../../dashboard/type/dashboard-dataset.interface"; @@ -35,7 +35,10 @@ export class DatasetVersionSelectorComponent extends FieldType selectedDataset?: DashboardDataset; selectedVersion?: DatasetVersion; - constructor(private datasetService: DatasetService, private changeDetectorRef: ChangeDetectorRef) { + constructor( + private datasetService: DatasetService, + private changeDetectorRef: ChangeDetectorRef + ) { super(); } @@ -45,16 +48,15 @@ export class DatasetVersionSelectorComponent extends FieldType .pipe(untilDestroyed(this)) .subscribe(datasets => { this.datasets = datasets; - const [_, ownerEmail, datasetName, versionName] = this.formControl.value.split("/") + const [_, ownerEmail, datasetName, versionName] = this.formControl.value.split("/"); if (versionName) { this.selectedDataset = this.datasets.find( - dataset => - dataset.ownerEmail === ownerEmail && dataset.dataset.name === datasetName + dataset => dataset.ownerEmail === ownerEmail && dataset.dataset.name === datasetName ); - this.onDatasetChange() + this.onDatasetChange(); } }); - } + } onDatasetChange(): void { if (this.selectedDataset) { From 1afbddbecdd83a804520ddc074a204e90c0d32fa Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Fri, 10 Apr 2026 17:17:25 -0700 Subject: [PATCH 05/23] fix fmt --- .../operator-property-edit-frame.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts index a436d111423..0e1484f59c1 100644 --- a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts +++ b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts @@ -449,11 +449,11 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On } // if the title is fileName, then change it to custom autocomplete input template - if (mappedField.key == "fileName") { + if (mappedField.key === "fileName") { mappedField.type = "inputautocomplete"; } - if (mappedField.key == "datasetVersionPath") { + if (mappedField.key === "datasetVersionPath") { mappedField.type = "datasetversionselector"; } From 42c401c8364185192c1c81c90860d8f23524388e Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Fri, 10 Apr 2026 17:18:30 -0700 Subject: [PATCH 06/23] fix fmt --- .../dataset-version-selector.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts index 16403806556..8f4dc94e4fc 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts @@ -48,8 +48,9 @@ export class DatasetVersionSelectorComponent extends FieldType .pipe(untilDestroyed(this)) .subscribe(datasets => { this.datasets = datasets; - const [_, ownerEmail, datasetName, versionName] = this.formControl.value.split("/"); - if (versionName) { + const path = this.formControl.value.split("/") + if (path) { + const [_, ownerEmail, datasetName, versionName] = this.formControl.value.split("/"); this.selectedDataset = this.datasets.find( dataset => dataset.ownerEmail === ownerEmail && dataset.dataset.name === datasetName ); From 174c0d1b0df686d4162e7336c9013bfebd6b71e6 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Fri, 10 Apr 2026 17:20:10 -0700 Subject: [PATCH 07/23] fix fmt --- .../dataset-version-selector.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts index 8f4dc94e4fc..8eff5909692 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts @@ -50,7 +50,7 @@ export class DatasetVersionSelectorComponent extends FieldType this.datasets = datasets; const path = this.formControl.value.split("/") if (path) { - const [_, ownerEmail, datasetName, versionName] = this.formControl.value.split("/"); + const [, ownerEmail, datasetName, ] = this.formControl.value.split("/"); this.selectedDataset = this.datasets.find( dataset => dataset.ownerEmail === ownerEmail && dataset.dataset.name === datasetName ); From f35612631d78783fa3c6ba0db9ba4300be439367 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Fri, 10 Apr 2026 17:23:15 -0700 Subject: [PATCH 08/23] fix fmt --- .../dataset-version-selector.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts index 8eff5909692..84ad8f2073b 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts @@ -48,9 +48,9 @@ export class DatasetVersionSelectorComponent extends FieldType .pipe(untilDestroyed(this)) .subscribe(datasets => { this.datasets = datasets; - const path = this.formControl.value.split("/") + const path = this.formControl.value.split("/"); if (path) { - const [, ownerEmail, datasetName, ] = this.formControl.value.split("/"); + const [, ownerEmail, datasetName] = this.formControl.value.split("/"); this.selectedDataset = this.datasets.find( dataset => dataset.ownerEmail === ownerEmail && dataset.dataset.name === datasetName ); From 1daf31276048c74c9a3c57e8986bcf5bdccf0d51 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Sun, 12 Apr 2026 20:37:38 -0700 Subject: [PATCH 09/23] refactor --- frontend/src/app/app.module.ts | 4 +- .../dataset-selection.component.html} | 12 +- .../dataset-selection.component.scss} | 9 +- .../dataset-selection.component.ts} | 105 ++++++++++++++---- .../dataset-version-selector.component.html | 40 +++---- .../dataset-version-selector.component.ts | 82 ++++++-------- .../input-autocomplete.component.ts | 15 +-- 7 files changed, 155 insertions(+), 112 deletions(-) rename frontend/src/app/workspace/component/{file-selection/file-selection.component.html => dataset-selection/dataset-selection.component.html} (89%) rename frontend/src/app/workspace/component/{file-selection/file-selection.component.scss => dataset-selection/dataset-selection.component.scss} (93%) rename frontend/src/app/workspace/component/{file-selection/file-selection.component.ts => dataset-selection/dataset-selection.component.ts} (55%) diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index f1d4ee53a34..3884bc3b9e7 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -105,6 +105,7 @@ import { AgentChatComponent } from "./workspace/component/agent-panel/agent-chat import { AgentRegistrationComponent } from "./workspace/component/agent-panel/agent-registration/agent-registration.component"; import { InputAutoCompleteComponent } from "./workspace/component/input-autocomplete/input-autocomplete.component"; import { DatasetVersionSelectorComponent } from "./workspace/component/dataset-version-selector/dataset-version-selector.component"; +import { DatasetSelectionComponent } from "./workspace/component/dataset-selection/dataset-selection.component"; import { CollabWrapperComponent } from "./common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; import { TexeraCopilot } from "./workspace/service/copilot/texera-copilot"; import { NzSwitchModule } from "ng-zorro-antd/switch"; @@ -153,7 +154,6 @@ import { NzTreeModule } from "ng-zorro-antd/tree"; import { NzTreeViewModule } from "ng-zorro-antd/tree-view"; import { NzNoAnimationModule } from "ng-zorro-antd/core/no-animation"; import { TreeModule } from "@ali-hm/angular-tree-component"; -import { FileSelectionComponent } from "./workspace/component/file-selection/file-selection.component"; import { ResultExportationComponent } from "./workspace/component/result-exportation/result-exportation.component"; import { ReportGenerationService } from "./workspace/service/report-generation/report-generation.service"; import { SearchBarComponent } from "./dashboard/component/user/search-bar/search-bar.component"; @@ -260,7 +260,7 @@ registerLocaleData(en); AgentRegistrationComponent, InputAutoCompleteComponent, DatasetVersionSelectorComponent, - FileSelectionComponent, + DatasetSelectionComponent, CollabWrapperComponent, AboutComponent, UserWorkflowListItemComponent, diff --git a/frontend/src/app/workspace/component/file-selection/file-selection.component.html b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.html similarity index 89% rename from frontend/src/app/workspace/component/file-selection/file-selection.component.html rename to frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.html index f78d46102d1..9a341db431f 100644 --- a/frontend/src/app/workspace/component/file-selection/file-selection.component.html +++ b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.html @@ -67,11 +67,21 @@ + +
+ +
diff --git a/frontend/src/app/workspace/component/file-selection/file-selection.component.scss b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.scss similarity index 93% rename from frontend/src/app/workspace/component/file-selection/file-selection.component.scss rename to frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.scss index 0e7ceadbf9b..66765eaf641 100644 --- a/frontend/src/app/workspace/component/file-selection/file-selection.component.scss +++ b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.scss @@ -38,7 +38,7 @@ } .select-dataset { - transition: width 0.3s; /* Add animation effect */ + transition: width 0.3s; } .select-version { @@ -60,7 +60,7 @@ .dataset-name { display: inline-block; - max-width: calc(100% - 100px); /* Adjust to fit other elements */ + max-width: calc(100% - 100px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -87,3 +87,8 @@ font-size: 12px; color: grey; } + +.action-row { + display: flex; + justify-content: flex-end; +} diff --git a/frontend/src/app/workspace/component/file-selection/file-selection.component.ts b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts similarity index 55% rename from frontend/src/app/workspace/component/file-selection/file-selection.component.ts rename to frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts index dfae2e48350..e4f0ca5c93d 100644 --- a/frontend/src/app/workspace/component/file-selection/file-selection.component.ts +++ b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts @@ -20,30 +20,45 @@ import { Component, inject, OnInit } from "@angular/core"; import { NZ_MODAL_DATA, NzModalRef } from "ng-zorro-antd/modal"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { DatasetFileNode } from "../../../common/type/datasetVersionFileTree"; +import { DatasetFileNode, getFullPathFromDatasetFileNode } from "../../../common/type/datasetVersionFileTree"; import { DatasetVersion } from "../../../common/type/dataset"; import { DashboardDataset } from "../../../dashboard/type/dashboard-dataset.interface"; import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; -import { parseFilePathToDatasetFile } from "../../../common/type/dataset-file"; + +type DatasetSelectionMode = "file" | "version"; + +interface DatasetSelectionModalData { + mode: DatasetSelectionMode; + selectedPath?: string | null; +} + +interface ParsedDatasetVersionPath { + ownerEmail: string; + datasetName: string; + versionName: string; +} @UntilDestroy() @Component({ - selector: "texera-file-selection-model", - templateUrl: "file-selection.component.html", - styleUrls: ["file-selection.component.scss"], + selector: "texera-dataset-selection-modal", + templateUrl: "dataset-selection.component.html", + styleUrls: ["dataset-selection.component.scss"], }) -export class FileSelectionComponent implements OnInit { - readonly selectedFilePath: string = inject(NZ_MODAL_DATA).selectedFilePath; +export class DatasetSelectionComponent implements OnInit { + private readonly data: DatasetSelectionModalData = inject(NZ_MODAL_DATA); private _datasets: ReadonlyArray = []; - // indicate whether the accessible datasets have been loaded from the backend + readonly mode: DatasetSelectionMode = this.data.mode; + readonly selectedPath: string = this.data.selectedPath ?? ""; + isAccessibleDatasetsLoading = true; selectedDataset?: DashboardDataset; selectedVersion?: DatasetVersion; + selectedFileNode?: DatasetFileNode; datasetVersions?: DatasetVersion[]; suggestedFileTreeNodes: DatasetFileNode[] = []; - isDatasetSelected: boolean = false; + isDatasetSelected = false; constructor( private modalRef: NzModalRef, @@ -53,29 +68,34 @@ export class FileSelectionComponent implements OnInit { ngOnInit() { this.isAccessibleDatasetsLoading = true; - // retrieve all the accessible datasets from the backend this.datasetService .retrieveAccessibleDatasets() .pipe(untilDestroyed(this)) .subscribe(datasets => { this._datasets = datasets; this.isAccessibleDatasetsLoading = false; - if (!this.selectedFilePath || this.selectedFilePath == "") { + + if (!this.selectedPath) { return; } - // if users already select some file, then ONLY show that selected dataset & related version - const selectedDatasetFile = parseFilePathToDatasetFile(this.selectedFilePath); + + const parsedPath = this.parseDatasetVersionPath(this.selectedPath); this.selectedDataset = this.datasets.find( - d => d.ownerEmail === selectedDatasetFile.ownerEmail && d.dataset.name === selectedDatasetFile.datasetName + dataset => + dataset.ownerEmail === parsedPath.ownerEmail && + dataset.dataset.name === parsedPath.datasetName ); this.isDatasetSelected = !!this.selectedDataset; - if (this.selectedDataset && this.selectedDataset.dataset.did !== undefined) { + + if (this.selectedDataset?.dataset.did !== undefined) { this.datasetService .retrieveDatasetVersionList(this.selectedDataset.dataset.did) .pipe(untilDestroyed(this)) .subscribe(versions => { this.datasetVersions = versions; - this.selectedVersion = this.datasetVersions.find(v => v.name === selectedDatasetFile.versionName); + this.selectedVersion = + this.datasetVersions.find(version => version.name === parsedPath.versionName) ?? + this.datasetVersions[0]; this.onVersionChange(); }); } @@ -84,15 +104,17 @@ export class FileSelectionComponent implements OnInit { onDatasetChange() { this.selectedVersion = undefined; + this.selectedFileNode = undefined; this.suggestedFileTreeNodes = []; this.isDatasetSelected = !!this.selectedDataset; - if (this.selectedDataset && this.selectedDataset.dataset.did !== undefined) { + + if (this.selectedDataset?.dataset.did !== undefined) { this.datasetService .retrieveDatasetVersionList(this.selectedDataset.dataset.did) .pipe(untilDestroyed(this)) .subscribe(versions => { this.datasetVersions = versions; - if (this.datasetVersions && this.datasetVersions.length > 0) { + if (this.datasetVersions.length > 0) { this.selectedVersion = this.datasetVersions[0]; this.onVersionChange(); } @@ -101,12 +123,13 @@ export class FileSelectionComponent implements OnInit { } onVersionChange() { + this.selectedFileNode = undefined; this.suggestedFileTreeNodes = []; + if ( - this.selectedDataset && - this.selectedDataset.dataset.did !== undefined && - this.selectedVersion && - this.selectedVersion.dvid !== undefined + this.mode === "file" && + this.selectedDataset?.dataset.did !== undefined && + this.selectedVersion?.dvid !== undefined ) { this.datasetService .retrieveDatasetVersionFileTree(this.selectedDataset.dataset.did, this.selectedVersion.dvid) @@ -118,10 +141,46 @@ export class FileSelectionComponent implements OnInit { } onFileTreeNodeSelected(node: DatasetFileNode) { - this.modalRef.close(node); + this.selectedFileNode = node.type === "file" ? node : undefined; + } + + onConfirmSelection(): void { + if (this.mode === "version") { + if (this.selectedDataset && this.selectedVersion) { + this.modalRef.close( + `/${this.selectedDataset.ownerEmail}/${this.selectedDataset.dataset.name}/${this.selectedVersion.name}` + ); + } + return; + } + + if (this.selectedFileNode) { + this.modalRef.close(getFullPathFromDatasetFileNode(this.selectedFileNode)); + } } get datasets(): ReadonlyArray { return this._datasets; } + + get confirmButtonText(): string { + return this.mode === "version" ? "Select Dataset" : "Select File"; + } + + get isConfirmDisabled(): boolean { + return this.mode === "version" + ? !(this.selectedDataset && this.selectedVersion) + : !this.selectedFileNode; + } + + private parseDatasetVersionPath(path: string): ParsedDatasetVersionPath { + const parts = path.split("/").filter(part => part.length > 0); + + if (parts.length < 3) { + throw new Error("Invalid dataset version path format"); + } + + const [ownerEmail, datasetName, versionName] = parts; + return { ownerEmail, datasetName, versionName }; + } } diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html index 1a2ff4889b8..50e4fa684d2 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html @@ -17,29 +17,17 @@ under the License. --> - - - - - - - - - +
+ + +
diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts index 84ad8f2073b..8ad72f20585 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts @@ -17,70 +17,50 @@ * under the License. */ -import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { DashboardDataset } from "../../../dashboard/type/dashboard-dataset.interface"; -import { DatasetVersion } from "../../../common/type/dataset"; -import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; +import { NzModalService } from "ng-zorro-antd/modal"; +import { DatasetSelectionComponent } from "../dataset-selection/dataset-selection.component"; @UntilDestroy() @Component({ selector: "texera-dataset-version-selector-template", templateUrl: "./dataset-version-selector.component.html", }) -export class DatasetVersionSelectorComponent extends FieldType implements OnInit { - datasets: ReadonlyArray = []; - datasetVersions: ReadonlyArray = []; - selectedDataset?: DashboardDataset; - selectedVersion?: DatasetVersion; - - constructor( - private datasetService: DatasetService, - private changeDetectorRef: ChangeDetectorRef - ) { +export class DatasetVersionSelectorComponent extends FieldType { + constructor(private modalService: NzModalService) { super(); } - ngOnInit(): void { - this.datasetService - .retrieveAccessibleDatasets() - .pipe(untilDestroyed(this)) - .subscribe(datasets => { - this.datasets = datasets; - const path = this.formControl.value.split("/"); - if (path) { - const [, ownerEmail, datasetName] = this.formControl.value.split("/"); - this.selectedDataset = this.datasets.find( - dataset => dataset.ownerEmail === ownerEmail && dataset.dataset.name === datasetName - ); - this.onDatasetChange(); - } - }); - } + onClickOpenDatasetSelectionModal(): void { + const modal = this.modalService.create({ + nzTitle: "Please select a dataset version", + nzContent: DatasetSelectionComponent, + nzFooter: null, + nzData: { + mode: "version", + selectedPath: this.formControl.getRawValue(), + }, + nzBodyStyle: { + resize: "both", + overflow: "auto", + minHeight: "200px", + minWidth: "550px", + maxWidth: "90vw", + maxHeight: "80vh", + }, + nzWidth: "fit-content", + }); - onDatasetChange(): void { - if (this.selectedDataset) { - this.datasetService - .retrieveDatasetVersionList(this.selectedDataset.dataset.did!) - .pipe(untilDestroyed(this)) - .subscribe(versions => { - this.datasetVersions = versions; - this.selectedVersion = versions[0]; - this.onVersionChange(); - this.changeDetectorRef.detectChanges(); - }); - } else { - this.selectedVersion = undefined; - this.onVersionChange(); - } + modal.afterClose.pipe(untilDestroyed(this)).subscribe(selectedPath => { + if (selectedPath) { + this.formControl.setValue(selectedPath); + } + }); } - onVersionChange(): void { - this.formControl.setValue( - this.selectedDataset && this.selectedVersion - ? `/${this.selectedDataset?.ownerEmail}/${this.selectedDataset?.dataset?.name}/${this.selectedVersion?.name}` - : null - ); + get selectedDatasetVersionPath(): string | null { + return this.formControl.value; } } diff --git a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts b/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts index 38839f377e5..138ab138932 100644 --- a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts +++ b/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts @@ -22,8 +22,7 @@ import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; import { NzModalService } from "ng-zorro-antd/modal"; -import { FileSelectionComponent } from "../file-selection/file-selection.component"; -import { DatasetFileNode, getFullPathFromDatasetFileNode } from "../../../common/type/datasetVersionFileTree"; +import { DatasetSelectionComponent } from "../dataset-selection/dataset-selection.component"; import { GuiConfigService } from "../../../common/service/gui-config.service"; @UntilDestroy() @@ -44,10 +43,11 @@ export class InputAutoCompleteComponent extends FieldType { onClickOpenFileSelectionModal(): void { const modal = this.modalService.create({ nzTitle: "Please select one file from datasets", - nzContent: FileSelectionComponent, + nzContent: DatasetSelectionComponent, nzFooter: null, nzData: { - selectedFilePath: this.formControl.getRawValue(), + mode: "file", + selectedPath: this.formControl.getRawValue(), }, nzBodyStyle: { // Enables the file selection window to be resizable @@ -61,9 +61,10 @@ export class InputAutoCompleteComponent extends FieldType { nzWidth: "fit-content", }); // Handle the selection from the modal - modal.afterClose.pipe(untilDestroyed(this)).subscribe(fileNode => { - const node: DatasetFileNode = fileNode as DatasetFileNode; - this.formControl.setValue(getFullPathFromDatasetFileNode(node)); + modal.afterClose.pipe(untilDestroyed(this)).subscribe(selectedPath => { + if (selectedPath) { + this.formControl.setValue(selectedPath); + } }); } From ec67fd8b84dabfd626ebe76013484adbf9488a16 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Sun, 12 Apr 2026 20:43:52 -0700 Subject: [PATCH 10/23] fix fmt --- .../dataset-selection/dataset-selection.component.ts | 8 ++------ .../dataset-version-selector.component.html | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts index e4f0ca5c93d..c4de837d219 100644 --- a/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts +++ b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts @@ -81,9 +81,7 @@ export class DatasetSelectionComponent implements OnInit { const parsedPath = this.parseDatasetVersionPath(this.selectedPath); this.selectedDataset = this.datasets.find( - dataset => - dataset.ownerEmail === parsedPath.ownerEmail && - dataset.dataset.name === parsedPath.datasetName + dataset => dataset.ownerEmail === parsedPath.ownerEmail && dataset.dataset.name === parsedPath.datasetName ); this.isDatasetSelected = !!this.selectedDataset; @@ -168,9 +166,7 @@ export class DatasetSelectionComponent implements OnInit { } get isConfirmDisabled(): boolean { - return this.mode === "version" - ? !(this.selectedDataset && this.selectedVersion) - : !this.selectedFileNode; + return this.mode === "version" ? !(this.selectedDataset && this.selectedVersion) : !this.selectedFileNode; } private parseDatasetVersionPath(path: string): ParsedDatasetVersionPath { diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html index 50e4fa684d2..1fa398e2fd6 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html @@ -20,7 +20,7 @@
From fc25f2dbc491099b484b525633f920101aa55b9b Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Mon, 13 Apr 2026 00:01:46 -0700 Subject: [PATCH 11/23] fix fmt --- .../dataset-version-selector.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html index 1fa398e2fd6..f2c03d019d0 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html @@ -19,6 +19,7 @@
Date: Mon, 13 Apr 2026 00:23:18 -0700 Subject: [PATCH 12/23] fix fmt --- frontend/src/app/app.module.ts | 8 +- .../src/app/common/formly/formly-config.ts | 4 +- .../dataset-selection.component.html | 87 --------- .../dataset-selection.component.scss | 94 --------- .../dataset-selection.component.ts | 182 ------------------ .../dataset-version-selector.component.html | 28 ++- .../dataset-version-selector.component.ts | 9 +- .../input-autocomplete.component.html | 42 ---- .../input-autocomplete.component.scss | 50 ----- .../input-autocomplete.component.spec.ts | 49 ----- .../input-autocomplete.component.ts | 82 -------- 11 files changed, 20 insertions(+), 615 deletions(-) delete mode 100644 frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.html delete mode 100644 frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.scss delete mode 100644 frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts delete mode 100644 frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.html delete mode 100644 frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.scss delete mode 100644 frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.spec.ts delete mode 100644 frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 3884bc3b9e7..11ce2f26f89 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -103,9 +103,9 @@ import { CoeditorUserIconComponent } from "./workspace/component/menu/coeditor-u import { AgentPanelComponent } from "./workspace/component/agent-panel/agent-panel.component"; import { AgentChatComponent } from "./workspace/component/agent-panel/agent-chat/agent-chat.component"; import { AgentRegistrationComponent } from "./workspace/component/agent-panel/agent-registration/agent-registration.component"; -import { InputAutoCompleteComponent } from "./workspace/component/input-autocomplete/input-autocomplete.component"; +import { DatasetFileSelectorComponent } from "./workspace/component/dataset-file-selector/dataset-file-selector.component"; import { DatasetVersionSelectorComponent } from "./workspace/component/dataset-version-selector/dataset-version-selector.component"; -import { DatasetSelectionComponent } from "./workspace/component/dataset-selection/dataset-selection.component"; +import { DatasetSelectionModalComponent } from "./workspace/component/dataset-selection-modal/dataset-selection-modal.component"; import { CollabWrapperComponent } from "./common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component"; import { TexeraCopilot } from "./workspace/service/copilot/texera-copilot"; import { NzSwitchModule } from "ng-zorro-antd/switch"; @@ -258,9 +258,9 @@ registerLocaleData(en); AgentPanelComponent, AgentChatComponent, AgentRegistrationComponent, - InputAutoCompleteComponent, + DatasetFileSelectorComponent, DatasetVersionSelectorComponent, - DatasetSelectionComponent, + DatasetSelectionModalComponent, CollabWrapperComponent, AboutComponent, UserWorkflowListItemComponent, diff --git a/frontend/src/app/common/formly/formly-config.ts b/frontend/src/app/common/formly/formly-config.ts index 8c814a5993e..c3995abb544 100644 --- a/frontend/src/app/common/formly/formly-config.ts +++ b/frontend/src/app/common/formly/formly-config.ts @@ -24,7 +24,7 @@ import { MultiSchemaTypeComponent } from "./multischema.type"; import { FormlyFieldConfig } from "@ngx-formly/core"; import { CodeareaCustomTemplateComponent } from "../../workspace/component/codearea-custom-template/codearea-custom-template.component"; import { PresetWrapperComponent } from "./preset-wrapper/preset-wrapper.component"; -import { InputAutoCompleteComponent } from "../../workspace/component/input-autocomplete/input-autocomplete.component"; +import { DatasetFileSelectorComponent } from "../../workspace/component/dataset-file-selector/dataset-file-selector.component"; import { CollabWrapperComponent } from "./collab-wrapper/collab-wrapper/collab-wrapper.component"; import { FormlyRepeatDndComponent } from "./repeat-dnd/repeat-dnd.component"; import { DatasetVersionSelectorComponent } from "../../workspace/component/dataset-version-selector/dataset-version-selector.component"; @@ -77,7 +77,7 @@ export const TEXERA_FORMLY_CONFIG = { { name: "object", component: ObjectTypeComponent }, { name: "multischema", component: MultiSchemaTypeComponent }, { name: "codearea", component: CodeareaCustomTemplateComponent }, - { name: "inputautocomplete", component: InputAutoCompleteComponent, wrappers: ["form-field"] }, + { name: "inputautocomplete", component: DatasetFileSelectorComponent, wrappers: ["form-field"] }, { name: "datasetversionselector", component: DatasetVersionSelectorComponent, wrappers: ["form-field"] }, { name: "repeat-section-dnd", component: FormlyRepeatDndComponent }, ], diff --git a/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.html b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.html deleted file mode 100644 index 9a341db431f..00000000000 --- a/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.html +++ /dev/null @@ -1,87 +0,0 @@ - - - -
-
- - -
-
{{ dataset.dataset.did }}
- {{ dataset.dataset.name }} - - OWNER - - - {{ dataset.accessPrivilege }} - -
-
-
- - - - - -
- - - - -
- -
-
-
diff --git a/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.scss b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.scss deleted file mode 100644 index 66765eaf641..00000000000 --- a/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.scss +++ /dev/null @@ -1,94 +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. - */ - -:host { - display: block; - padding: 16px; -} - -.container { - display: flex; - flex-direction: column; - gap: 16px; -} - -.selection-row { - display: flex; - gap: 16px; -} - -.nz-select { - width: 100%; -} - -.select-dataset { - transition: width 0.3s; -} - -.select-version { - flex: 1; - min-width: 0; -} - -.texera-user-dataset-version-filetree { - margin-top: 16px; -} - -.dataset-option { - display: flex; - align-items: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.dataset-name { - display: inline-block; - max-width: calc(100% - 100px); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.dataset-id-container { - background-color: grey; - color: white; - width: 23px; - height: 23px; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - font-size: 12px; - margin-right: 5px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - overflow: hidden; -} - -.dataset-owner, -.dataset-access-privilege { - margin-left: 10px; - font-size: 12px; - color: grey; -} - -.action-row { - display: flex; - justify-content: flex-end; -} diff --git a/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts b/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts deleted file mode 100644 index c4de837d219..00000000000 --- a/frontend/src/app/workspace/component/dataset-selection/dataset-selection.component.ts +++ /dev/null @@ -1,182 +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, inject, OnInit } from "@angular/core"; -import { NZ_MODAL_DATA, NzModalRef } from "ng-zorro-antd/modal"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { DatasetFileNode, getFullPathFromDatasetFileNode } from "../../../common/type/datasetVersionFileTree"; -import { DatasetVersion } from "../../../common/type/dataset"; -import { DashboardDataset } from "../../../dashboard/type/dashboard-dataset.interface"; -import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; - -type DatasetSelectionMode = "file" | "version"; - -interface DatasetSelectionModalData { - mode: DatasetSelectionMode; - selectedPath?: string | null; -} - -interface ParsedDatasetVersionPath { - ownerEmail: string; - datasetName: string; - versionName: string; -} - -@UntilDestroy() -@Component({ - selector: "texera-dataset-selection-modal", - templateUrl: "dataset-selection.component.html", - styleUrls: ["dataset-selection.component.scss"], -}) -export class DatasetSelectionComponent implements OnInit { - private readonly data: DatasetSelectionModalData = inject(NZ_MODAL_DATA); - private _datasets: ReadonlyArray = []; - - readonly mode: DatasetSelectionMode = this.data.mode; - readonly selectedPath: string = this.data.selectedPath ?? ""; - - isAccessibleDatasetsLoading = true; - - selectedDataset?: DashboardDataset; - selectedVersion?: DatasetVersion; - selectedFileNode?: DatasetFileNode; - datasetVersions?: DatasetVersion[]; - suggestedFileTreeNodes: DatasetFileNode[] = []; - isDatasetSelected = false; - - constructor( - private modalRef: NzModalRef, - private datasetService: DatasetService - ) {} - - ngOnInit() { - this.isAccessibleDatasetsLoading = true; - - this.datasetService - .retrieveAccessibleDatasets() - .pipe(untilDestroyed(this)) - .subscribe(datasets => { - this._datasets = datasets; - this.isAccessibleDatasetsLoading = false; - - if (!this.selectedPath) { - return; - } - - const parsedPath = this.parseDatasetVersionPath(this.selectedPath); - this.selectedDataset = this.datasets.find( - dataset => dataset.ownerEmail === parsedPath.ownerEmail && dataset.dataset.name === parsedPath.datasetName - ); - this.isDatasetSelected = !!this.selectedDataset; - - if (this.selectedDataset?.dataset.did !== undefined) { - this.datasetService - .retrieveDatasetVersionList(this.selectedDataset.dataset.did) - .pipe(untilDestroyed(this)) - .subscribe(versions => { - this.datasetVersions = versions; - this.selectedVersion = - this.datasetVersions.find(version => version.name === parsedPath.versionName) ?? - this.datasetVersions[0]; - this.onVersionChange(); - }); - } - }); - } - - onDatasetChange() { - this.selectedVersion = undefined; - this.selectedFileNode = undefined; - this.suggestedFileTreeNodes = []; - this.isDatasetSelected = !!this.selectedDataset; - - if (this.selectedDataset?.dataset.did !== undefined) { - this.datasetService - .retrieveDatasetVersionList(this.selectedDataset.dataset.did) - .pipe(untilDestroyed(this)) - .subscribe(versions => { - this.datasetVersions = versions; - if (this.datasetVersions.length > 0) { - this.selectedVersion = this.datasetVersions[0]; - this.onVersionChange(); - } - }); - } - } - - onVersionChange() { - this.selectedFileNode = undefined; - this.suggestedFileTreeNodes = []; - - if ( - this.mode === "file" && - this.selectedDataset?.dataset.did !== undefined && - this.selectedVersion?.dvid !== undefined - ) { - this.datasetService - .retrieveDatasetVersionFileTree(this.selectedDataset.dataset.did, this.selectedVersion.dvid) - .pipe(untilDestroyed(this)) - .subscribe(data => { - this.suggestedFileTreeNodes = data.fileNodes; - }); - } - } - - onFileTreeNodeSelected(node: DatasetFileNode) { - this.selectedFileNode = node.type === "file" ? node : undefined; - } - - onConfirmSelection(): void { - if (this.mode === "version") { - if (this.selectedDataset && this.selectedVersion) { - this.modalRef.close( - `/${this.selectedDataset.ownerEmail}/${this.selectedDataset.dataset.name}/${this.selectedVersion.name}` - ); - } - return; - } - - if (this.selectedFileNode) { - this.modalRef.close(getFullPathFromDatasetFileNode(this.selectedFileNode)); - } - } - - get datasets(): ReadonlyArray { - return this._datasets; - } - - get confirmButtonText(): string { - return this.mode === "version" ? "Select Dataset" : "Select File"; - } - - get isConfirmDisabled(): boolean { - return this.mode === "version" ? !(this.selectedDataset && this.selectedVersion) : !this.selectedFileNode; - } - - private parseDatasetVersionPath(path: string): ParsedDatasetVersionPath { - const parts = path.split("/").filter(part => part.length > 0); - - if (parts.length < 3) { - throw new Error("Invalid dataset version path format"); - } - - const [ownerEmail, datasetName, versionName] = parts; - return { ownerEmail, datasetName, versionName }; - } -} diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html index f2c03d019d0..24bc017b0ba 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html @@ -16,19 +16,15 @@ specific language governing permissions and limitations under the License. --> - -
- - -
+ + diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts index 8ad72f20585..b782488dbce 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts @@ -21,11 +21,10 @@ import { Component } from "@angular/core"; import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { NzModalService } from "ng-zorro-antd/modal"; -import { DatasetSelectionComponent } from "../dataset-selection/dataset-selection.component"; +import { DatasetSelectionModalComponent } from "../dataset-selection-modal/dataset-selection-modal.component"; @UntilDestroy() @Component({ - selector: "texera-dataset-version-selector-template", templateUrl: "./dataset-version-selector.component.html", }) export class DatasetVersionSelectorComponent extends FieldType { @@ -36,7 +35,7 @@ export class DatasetVersionSelectorComponent extends FieldType onClickOpenDatasetSelectionModal(): void { const modal = this.modalService.create({ nzTitle: "Please select a dataset version", - nzContent: DatasetSelectionComponent, + nzContent: DatasetSelectionModalComponent, nzFooter: null, nzData: { mode: "version", @@ -59,8 +58,4 @@ export class DatasetVersionSelectorComponent extends FieldType } }); } - - get selectedDatasetVersionPath(): string | null { - return this.formControl.value; - } } diff --git a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.html b/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.html deleted file mode 100644 index 53ee531fa6e..00000000000 --- a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.html +++ /dev/null @@ -1,42 +0,0 @@ - - -
- - -
- diff --git a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.scss b/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.scss deleted file mode 100644 index cc76590399a..00000000000 --- a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.scss +++ /dev/null @@ -1,50 +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. - */ - -mat-form-field { - width: 100%; -} -.input-autocomplete-container { - display: flex; - align-items: center; - width: 100%; - - input { - flex: 1; - margin-right: 10px; - } - - button { - white-space: nowrap; - } - - .file-select-button { - border: 2px solid #1890ff; - color: #1890ff; - - &:hover { - background-color: #e6f7ff; - border-color: #1890ff; - } - - &:focus { - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); - } - } -} diff --git a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.spec.ts b/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.spec.ts deleted file mode 100644 index 690ad849990..00000000000 --- a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.spec.ts +++ /dev/null @@ -1,49 +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 { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; -import { FormControl, ReactiveFormsModule } from "@angular/forms"; -import { InputAutoCompleteComponent } from "./input-autocomplete.component"; -import { HttpClientTestingModule } from "@angular/common/http/testing"; -import { NzModalService } from "ng-zorro-antd/modal"; -import { commonTestProviders } from "../../../common/testing/test-utils"; - -describe("InputAutoCompleteComponent", () => { - let component: InputAutoCompleteComponent; - let fixture: ComponentFixture; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [InputAutoCompleteComponent], - imports: [ReactiveFormsModule, HttpClientTestingModule], - providers: [NzModalService, ...commonTestProviders], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(InputAutoCompleteComponent); - component = fixture.componentInstance; - component.field = { props: {}, formControl: new FormControl() }; - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts b/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts deleted file mode 100644 index 138ab138932..00000000000 --- a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts +++ /dev/null @@ -1,82 +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 } from "@angular/core"; -import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; -import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; -import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; -import { NzModalService } from "ng-zorro-antd/modal"; -import { DatasetSelectionComponent } from "../dataset-selection/dataset-selection.component"; -import { GuiConfigService } from "../../../common/service/gui-config.service"; - -@UntilDestroy() -@Component({ - selector: "texera-input-autocomplete-template", - templateUrl: "./input-autocomplete.component.html", - styleUrls: ["input-autocomplete.component.scss"], -}) -export class InputAutoCompleteComponent extends FieldType { - constructor( - private modalService: NzModalService, - public workflowActionService: WorkflowActionService, - private config: GuiConfigService - ) { - super(); - } - - onClickOpenFileSelectionModal(): void { - const modal = this.modalService.create({ - nzTitle: "Please select one file from datasets", - nzContent: DatasetSelectionComponent, - nzFooter: null, - nzData: { - mode: "file", - selectedPath: this.formControl.getRawValue(), - }, - nzBodyStyle: { - // Enables the file selection window to be resizable - resize: "both", - overflow: "auto", - minHeight: "200px", - minWidth: "550px", - maxWidth: "90vw", - maxHeight: "80vh", - }, - nzWidth: "fit-content", - }); - // Handle the selection from the modal - modal.afterClose.pipe(untilDestroyed(this)).subscribe(selectedPath => { - if (selectedPath) { - this.formControl.setValue(selectedPath); - } - }); - } - - get enableDatasetSource(): boolean { - return this.config.env.selectingFilesFromDatasetsEnabled; - } - - get isFileSelectionEnabled(): boolean { - return this.enableDatasetSource; - } - - get selectedFilePath(): string | null { - return this.formControl.value; - } -} From 5ca84f00f6b00fa8c753d0be3584d678efe825d2 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Mon, 13 Apr 2026 00:23:26 -0700 Subject: [PATCH 13/23] fix fmt --- .../dataset-file-selector.component.html | 42 ++++ .../dataset-file-selector.component.scss | 50 +++++ .../dataset-file-selector.component.ts | 82 ++++++++ .../dataset-selection-modal.component.html | 87 +++++++++ .../dataset-selection-modal.component.scss | 94 +++++++++ .../dataset-selection-modal.component.ts | 182 ++++++++++++++++++ 6 files changed, 537 insertions(+) create mode 100644 frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html create mode 100644 frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.scss create mode 100644 frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts create mode 100644 frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html create mode 100644 frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss create mode 100644 frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts diff --git a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html new file mode 100644 index 00000000000..8b67637ca19 --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html @@ -0,0 +1,42 @@ + + +
+ + +
+ diff --git a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.scss b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.scss new file mode 100644 index 00000000000..02aff2b3cf4 --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.scss @@ -0,0 +1,50 @@ +/** + * 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. + */ + +mat-form-field { + width: 100%; +} +.dataset-file-selector-container { + display: flex; + align-items: center; + width: 100%; + + input { + flex: 1; + margin-right: 10px; + } + + button { + white-space: nowrap; + } + + .file-select-button { + border: 2px solid #1890ff; + color: #1890ff; + + &:hover { + background-color: #e6f7ff; + border-color: #1890ff; + } + + &:focus { + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + } +} diff --git a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts new file mode 100644 index 00000000000..c29f0c22578 --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts @@ -0,0 +1,82 @@ +/** + * 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 } from "@angular/core"; +import { FieldType, FieldTypeConfig } from "@ngx-formly/core"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; +import { NzModalService } from "ng-zorro-antd/modal"; +import { DatasetSelectionModalComponent } from "../dataset-selection-modal/dataset-selection-modal.component"; +import { GuiConfigService } from "../../../common/service/gui-config.service"; + +@UntilDestroy() +@Component({ + selector: "texera-dataset-file-selector-template", + templateUrl: "./dataset-file-selector.component.html", + styleUrls: ["dataset-file-selector.component.scss"], +}) +export class DatasetFileSelectorComponent extends FieldType { + constructor( + private modalService: NzModalService, + public workflowActionService: WorkflowActionService, + private config: GuiConfigService + ) { + super(); + } + + onClickOpenFileSelectionModal(): void { + const modal = this.modalService.create({ + nzTitle: "Please select one file from datasets", + nzContent: DatasetSelectionModalComponent, + nzFooter: null, + nzData: { + mode: "file", + selectedPath: this.formControl.getRawValue(), + }, + nzBodyStyle: { + // Enables the file selection window to be resizable + resize: "both", + overflow: "auto", + minHeight: "200px", + minWidth: "550px", + maxWidth: "90vw", + maxHeight: "80vh", + }, + nzWidth: "fit-content", + }); + // Handle the selection from the modal + modal.afterClose.pipe(untilDestroyed(this)).subscribe(selectedPath => { + if (selectedPath) { + this.formControl.setValue(selectedPath); + } + }); + } + + get enableDatasetSource(): boolean { + return this.config.env.selectingFilesFromDatasetsEnabled; + } + + get isFileSelectionEnabled(): boolean { + return this.enableDatasetSource; + } + + get selectedFilePath(): string | null { + return this.formControl.value; + } +} diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html new file mode 100644 index 00000000000..9a341db431f --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html @@ -0,0 +1,87 @@ + + + +
+
+ + +
+
{{ dataset.dataset.did }}
+ {{ dataset.dataset.name }} + + OWNER + + + {{ dataset.accessPrivilege }} + +
+
+
+ + + + + +
+ + + + +
+ +
+
+
diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss new file mode 100644 index 00000000000..66765eaf641 --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss @@ -0,0 +1,94 @@ +/** + * 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. + */ + +:host { + display: block; + padding: 16px; +} + +.container { + display: flex; + flex-direction: column; + gap: 16px; +} + +.selection-row { + display: flex; + gap: 16px; +} + +.nz-select { + width: 100%; +} + +.select-dataset { + transition: width 0.3s; +} + +.select-version { + flex: 1; + min-width: 0; +} + +.texera-user-dataset-version-filetree { + margin-top: 16px; +} + +.dataset-option { + display: flex; + align-items: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.dataset-name { + display: inline-block; + max-width: calc(100% - 100px); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.dataset-id-container { + background-color: grey; + color: white; + width: 23px; + height: 23px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + font-size: 12px; + margin-right: 5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.dataset-owner, +.dataset-access-privilege { + margin-left: 10px; + font-size: 12px; + color: grey; +} + +.action-row { + display: flex; + justify-content: flex-end; +} diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts new file mode 100644 index 00000000000..ee780d8b96b --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts @@ -0,0 +1,182 @@ +/** + * 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, inject, OnInit } from "@angular/core"; +import { NZ_MODAL_DATA, NzModalRef } from "ng-zorro-antd/modal"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { DatasetFileNode, getFullPathFromDatasetFileNode } from "../../../common/type/datasetVersionFileTree"; +import { DatasetVersion } from "../../../common/type/dataset"; +import { DashboardDataset } from "../../../dashboard/type/dashboard-dataset.interface"; +import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; + +type DatasetSelectionMode = "file" | "version"; + +interface DatasetSelectionModalData { + mode: DatasetSelectionMode; + selectedPath?: string | null; +} + +interface ParsedDatasetVersionPath { + ownerEmail: string; + datasetName: string; + versionName: string; +} + +@UntilDestroy() +@Component({ + selector: "texera-dataset-selection-modal", + templateUrl: "dataset-selection-modal.component.html", + styleUrls: ["dataset-selection-modal.component.scss"], +}) +export class DatasetSelectionModalComponent implements OnInit { + private readonly data: DatasetSelectionModalData = inject(NZ_MODAL_DATA); + private _datasets: ReadonlyArray = []; + + readonly mode: DatasetSelectionMode = this.data.mode; + readonly selectedPath: string = this.data.selectedPath ?? ""; + + isAccessibleDatasetsLoading = true; + + selectedDataset?: DashboardDataset; + selectedVersion?: DatasetVersion; + selectedFileNode?: DatasetFileNode; + datasetVersions?: DatasetVersion[]; + suggestedFileTreeNodes: DatasetFileNode[] = []; + isDatasetSelected = false; + + constructor( + private modalRef: NzModalRef, + private datasetService: DatasetService + ) {} + + ngOnInit() { + this.isAccessibleDatasetsLoading = true; + + this.datasetService + .retrieveAccessibleDatasets() + .pipe(untilDestroyed(this)) + .subscribe(datasets => { + this._datasets = datasets; + this.isAccessibleDatasetsLoading = false; + + if (!this.selectedPath) { + return; + } + + const parsedPath = this.parseDatasetVersionPath(this.selectedPath); + this.selectedDataset = this.datasets.find( + dataset => dataset.ownerEmail === parsedPath.ownerEmail && dataset.dataset.name === parsedPath.datasetName + ); + this.isDatasetSelected = !!this.selectedDataset; + + if (this.selectedDataset?.dataset.did !== undefined) { + this.datasetService + .retrieveDatasetVersionList(this.selectedDataset.dataset.did) + .pipe(untilDestroyed(this)) + .subscribe(versions => { + this.datasetVersions = versions; + this.selectedVersion = + this.datasetVersions.find(version => version.name === parsedPath.versionName) ?? + this.datasetVersions[0]; + this.onVersionChange(); + }); + } + }); + } + + onDatasetChange() { + this.selectedVersion = undefined; + this.selectedFileNode = undefined; + this.suggestedFileTreeNodes = []; + this.isDatasetSelected = !!this.selectedDataset; + + if (this.selectedDataset?.dataset.did !== undefined) { + this.datasetService + .retrieveDatasetVersionList(this.selectedDataset.dataset.did) + .pipe(untilDestroyed(this)) + .subscribe(versions => { + this.datasetVersions = versions; + if (this.datasetVersions.length > 0) { + this.selectedVersion = this.datasetVersions[0]; + this.onVersionChange(); + } + }); + } + } + + onVersionChange() { + this.selectedFileNode = undefined; + this.suggestedFileTreeNodes = []; + + if ( + this.mode === "file" && + this.selectedDataset?.dataset.did !== undefined && + this.selectedVersion?.dvid !== undefined + ) { + this.datasetService + .retrieveDatasetVersionFileTree(this.selectedDataset.dataset.did, this.selectedVersion.dvid) + .pipe(untilDestroyed(this)) + .subscribe(data => { + this.suggestedFileTreeNodes = data.fileNodes; + }); + } + } + + onFileTreeNodeSelected(node: DatasetFileNode) { + this.selectedFileNode = node.type === "file" ? node : undefined; + } + + onConfirmSelection(): void { + if (this.mode === "version") { + if (this.selectedDataset && this.selectedVersion) { + this.modalRef.close( + `/${this.selectedDataset.ownerEmail}/${this.selectedDataset.dataset.name}/${this.selectedVersion.name}` + ); + } + return; + } + + if (this.selectedFileNode) { + this.modalRef.close(getFullPathFromDatasetFileNode(this.selectedFileNode)); + } + } + + get datasets(): ReadonlyArray { + return this._datasets; + } + + get confirmButtonText(): string { + return this.mode === "version" ? "Select Dataset" : "Select File"; + } + + get isConfirmDisabled(): boolean { + return this.mode === "version" ? !(this.selectedDataset && this.selectedVersion) : !this.selectedFileNode; + } + + private parseDatasetVersionPath(path: string): ParsedDatasetVersionPath { + const parts = path.split("/").filter(part => part.length > 0); + + if (parts.length < 3) { + throw new Error("Invalid dataset version path format"); + } + + const [ownerEmail, datasetName, versionName] = parts; + return { ownerEmail, datasetName, versionName }; + } +} From 78a7b972890c4e2b8ca4794bc48eb1ca4d4e30db Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Mon, 13 Apr 2026 00:38:06 -0700 Subject: [PATCH 14/23] fix fmt --- .../dataset-file-selector.component.html | 6 --- .../dataset-file-selector.component.scss | 50 ------------------- .../dataset-file-selector.component.ts | 4 +- .../dataset-version-selector.component.ts | 2 +- 4 files changed, 2 insertions(+), 60 deletions(-) delete mode 100644 frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.scss diff --git a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html index 8b67637ca19..5147782c9ce 100644 --- a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html @@ -34,9 +34,3 @@ {{ selectedFilePath ? 'Reselect File' : 'Select File' }}
- diff --git a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.scss b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.scss deleted file mode 100644 index 02aff2b3cf4..00000000000 --- a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.scss +++ /dev/null @@ -1,50 +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. - */ - -mat-form-field { - width: 100%; -} -.dataset-file-selector-container { - display: flex; - align-items: center; - width: 100%; - - input { - flex: 1; - margin-right: 10px; - } - - button { - white-space: nowrap; - } - - .file-select-button { - border: 2px solid #1890ff; - color: #1890ff; - - &:hover { - background-color: #e6f7ff; - border-color: #1890ff; - } - - &:focus { - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); - } - } -} diff --git a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts index c29f0c22578..6a4848f411e 100644 --- a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts @@ -27,9 +27,7 @@ import { GuiConfigService } from "../../../common/service/gui-config.service"; @UntilDestroy() @Component({ - selector: "texera-dataset-file-selector-template", - templateUrl: "./dataset-file-selector.component.html", - styleUrls: ["dataset-file-selector.component.scss"], + templateUrl: "dataset-file-selector.component.html", }) export class DatasetFileSelectorComponent extends FieldType { constructor( diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts index b782488dbce..2e754131155 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts @@ -25,7 +25,7 @@ import { DatasetSelectionModalComponent } from "../dataset-selection-modal/datas @UntilDestroy() @Component({ - templateUrl: "./dataset-version-selector.component.html", + templateUrl: "dataset-version-selector.component.html", }) export class DatasetVersionSelectorComponent extends FieldType { constructor(private modalService: NzModalService) { From d653941485e017cb2271e456b7376e02e904ecc8 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Mon, 13 Apr 2026 00:41:45 -0700 Subject: [PATCH 15/23] fix fmt --- .../org/apache/texera/config/GuiConfig.scala | 2 -- .../dataset-file-selector.component.html | 31 ++++++++----------- .../dataset-file-selector.component.ts | 6 +--- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala b/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala index adc789c9843..232b6c54729 100644 --- a/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala +++ b/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala @@ -49,8 +49,6 @@ object GuiConfig { conf.getInt("gui.workflow-workspace.default-data-transfer-batch-size") val guiWorkflowWorkspaceDefaultExecutionMode: String = conf.getString("gui.workflow-workspace.default-execution-mode") - val guiWorkflowWorkspaceSelectingFilesFromDatasetsEnabled: Boolean = - conf.getBoolean("gui.workflow-workspace.selecting-files-from-datasets-enabled") val guiWorkflowWorkspaceWorkflowExecutionsTrackingEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.workflow-executions-tracking-enabled") val guiWorkflowWorkspaceLinkBreakpointEnabled: Boolean = diff --git a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html index 5147782c9ce..75c64fc68c2 100644 --- a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html @@ -16,21 +16,16 @@ specific language governing permissions and limitations under the License. --> - -
- - -
+ + diff --git a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts index 6a4848f411e..2720e2815bd 100644 --- a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts @@ -66,12 +66,8 @@ export class DatasetFileSelectorComponent extends FieldType { }); } - get enableDatasetSource(): boolean { - return this.config.env.selectingFilesFromDatasetsEnabled; - } - get isFileSelectionEnabled(): boolean { - return this.enableDatasetSource; + return this.config.env.selectingFilesFromDatasetsEnabled; } get selectedFilePath(): string | null { From ed2b3ec2fd242d8cd9a77cb1a2ff1745efb7c2a8 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Mon, 13 Apr 2026 00:47:07 -0700 Subject: [PATCH 16/23] fix fmt --- .../dataset-file-selector.component.html | 2 +- .../dataset-file-selector.component.ts | 6 ------ .../dataset-selection-modal.component.html | 2 +- .../dataset-selection-modal.component.scss | 1 - .../dataset-selection-modal.component.ts | 9 --------- .../dataset-version-selector.component.ts | 1 - 6 files changed, 2 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html index 75c64fc68c2..34b3f8909be 100644 --- a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html @@ -17,7 +17,7 @@ under the License. --> { onClickOpenFileSelectionModal(): void { const modal = this.modalService.create({ - nzTitle: "Please select one file from datasets", nzContent: DatasetSelectionModalComponent, nzFooter: null, nzData: { @@ -58,7 +57,6 @@ export class DatasetFileSelectorComponent extends FieldType { }, nzWidth: "fit-content", }); - // Handle the selection from the modal modal.afterClose.pipe(untilDestroyed(this)).subscribe(selectedPath => { if (selectedPath) { this.formControl.setValue(selectedPath); @@ -69,8 +67,4 @@ export class DatasetFileSelectorComponent extends FieldType { get isFileSelectionEnabled(): boolean { return this.config.env.selectingFilesFromDatasetsEnabled; } - - get selectedFilePath(): string | null { - return this.formControl.value; - } } diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html index 9a341db431f..ef063708d07 100644 --- a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html @@ -80,7 +80,7 @@ nzType="primary" [disabled]="isConfirmDisabled" (click)="onConfirmSelection()"> - {{ confirmButtonText }} + Select
diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss index 66765eaf641..5ce90e98ccf 100644 --- a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss @@ -18,7 +18,6 @@ */ :host { - display: block; padding: 16px; } diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts index ee780d8b96b..a1a6880b733 100644 --- a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts @@ -40,7 +40,6 @@ interface ParsedDatasetVersionPath { @UntilDestroy() @Component({ - selector: "texera-dataset-selection-modal", templateUrl: "dataset-selection-modal.component.html", styleUrls: ["dataset-selection-modal.component.scss"], }) @@ -161,10 +160,6 @@ export class DatasetSelectionModalComponent implements OnInit { return this._datasets; } - get confirmButtonText(): string { - return this.mode === "version" ? "Select Dataset" : "Select File"; - } - get isConfirmDisabled(): boolean { return this.mode === "version" ? !(this.selectedDataset && this.selectedVersion) : !this.selectedFileNode; } @@ -172,10 +167,6 @@ export class DatasetSelectionModalComponent implements OnInit { private parseDatasetVersionPath(path: string): ParsedDatasetVersionPath { const parts = path.split("/").filter(part => part.length > 0); - if (parts.length < 3) { - throw new Error("Invalid dataset version path format"); - } - const [ownerEmail, datasetName, versionName] = parts; return { ownerEmail, datasetName, versionName }; } diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts index 2e754131155..89b0a6c677d 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts @@ -34,7 +34,6 @@ export class DatasetVersionSelectorComponent extends FieldType onClickOpenDatasetSelectionModal(): void { const modal = this.modalService.create({ - nzTitle: "Please select a dataset version", nzContent: DatasetSelectionModalComponent, nzFooter: null, nzData: { From 8eecefa73b83119273d51cf57922ceb4e7160ea6 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Mon, 13 Apr 2026 01:04:36 -0700 Subject: [PATCH 17/23] fix fmt --- .../dataset-selection-modal.component.html | 7 +------ .../dataset-selection-modal.component.scss | 4 ---- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html index ef063708d07..247dc14ee48 100644 --- a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html @@ -21,8 +21,6 @@
+ (selectedTreeNode)="onFileTreeNodeSelected($event)">
diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss index 5ce90e98ccf..9762d18602c 100644 --- a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss @@ -45,10 +45,6 @@ min-width: 0; } -.texera-user-dataset-version-filetree { - margin-top: 16px; -} - .dataset-option { display: flex; align-items: center; From 27c53ee533d7c31054a1be6bb7cb5b8fc1f9448a Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Mon, 13 Apr 2026 11:19:53 -0700 Subject: [PATCH 18/23] fix fmt --- .../dataset-file-selector.component.html | 2 +- .../dataset-file-selector.component.ts | 3 +- .../dataset-selection-modal.component.html | 44 ++---- .../dataset-selection-modal.component.scss | 40 ++---- .../dataset-selection-modal.component.ts | 132 ++++++------------ .../dataset-version-selector.component.html | 2 +- .../dataset-version-selector.component.ts | 2 +- 7 files changed, 70 insertions(+), 155 deletions(-) diff --git a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html index 34b3f8909be..b4f0b58d5e4 100644 --- a/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html @@ -17,7 +17,7 @@ under the License. --> { nzContent: DatasetSelectionModalComponent, nzFooter: null, nzData: { - mode: "file", + selectFile: true, selectedPath: this.formControl.getRawValue(), }, nzBodyStyle: { - // Enables the file selection window to be resizable resize: "both", overflow: "auto", minHeight: "200px", diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html index 247dc14ee48..31f62503012 100644 --- a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html @@ -17,33 +17,23 @@ under the License. --> - -
-
+ +
+
+ class="dataset-select">
-
{{ dataset.dataset.did }}
+ #{{ dataset.dataset.did }}   {{ dataset.dataset.name }} - - OWNER - - - {{ dataset.accessPrivilege }} - + {{ dataset.isOwner ? 'OWNER' : dataset.accessPrivilege }}
@@ -53,7 +43,7 @@ nzPlaceHolder="Select version" [(ngModel)]="selectedVersion" (ngModelChange)="onVersionChange()" - class="select-version"> + class="version-select">
- - -
- -
+
diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss index 9762d18602c..3ae6c146e00 100644 --- a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss @@ -17,30 +17,23 @@ * under the License. */ -:host { - padding: 16px; -} - -.container { +.content { display: flex; flex-direction: column; gap: 16px; } -.selection-row { +.selectors { display: flex; gap: 16px; } -.nz-select { - width: 100%; -} - -.select-dataset { - transition: width 0.3s; +.dataset-select { + flex: 1; + min-width: 0; } -.select-version { +.version-select { flex: 1; min-width: 0; } @@ -54,36 +47,19 @@ } .dataset-name { - display: inline-block; max-width: calc(100% - 100px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.dataset-id-container { - background-color: grey; - color: white; - width: 23px; - height: 23px; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - font-size: 12px; - margin-right: 5px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - overflow: hidden; -} - -.dataset-owner, -.dataset-access-privilege { +.access-level{ margin-left: 10px; font-size: 12px; color: grey; } -.action-row { +.actions { display: flex; justify-content: flex-end; } diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts index a1a6880b733..16a457b1a50 100644 --- a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts @@ -25,39 +25,27 @@ import { DatasetVersion } from "../../../common/type/dataset"; import { DashboardDataset } from "../../../dashboard/type/dashboard-dataset.interface"; import { DatasetService } from "../../../dashboard/service/user/dataset/dataset.service"; -type DatasetSelectionMode = "file" | "version"; - -interface DatasetSelectionModalData { - mode: DatasetSelectionMode; - selectedPath?: string | null; -} - -interface ParsedDatasetVersionPath { - ownerEmail: string; - datasetName: string; - versionName: string; -} - @UntilDestroy() @Component({ templateUrl: "dataset-selection-modal.component.html", styleUrls: ["dataset-selection-modal.component.scss"], }) export class DatasetSelectionModalComponent implements OnInit { - private readonly data: DatasetSelectionModalData = inject(NZ_MODAL_DATA); - private _datasets: ReadonlyArray = []; + private readonly data = inject(NZ_MODAL_DATA) as { + selectFile: boolean; + selectedPath?: string | null; + }; - readonly mode: DatasetSelectionMode = this.data.mode; - readonly selectedPath: string = this.data.selectedPath ?? ""; + readonly selectFile: boolean = this.data.selectFile; - isAccessibleDatasetsLoading = true; + loading = true; + datasets: ReadonlyArray = []; + datasetVersions: ReadonlyArray = []; selectedDataset?: DashboardDataset; selectedVersion?: DatasetVersion; - selectedFileNode?: DatasetFileNode; - datasetVersions?: DatasetVersion[]; suggestedFileTreeNodes: DatasetFileNode[] = []; - isDatasetSelected = false; + selectedFilePath?: string; constructor( private modalRef: NzModalRef, @@ -65,66 +53,41 @@ export class DatasetSelectionModalComponent implements OnInit { ) {} ngOnInit() { - this.isAccessibleDatasetsLoading = true; - this.datasetService .retrieveAccessibleDatasets() .pipe(untilDestroyed(this)) .subscribe(datasets => { - this._datasets = datasets; - this.isAccessibleDatasetsLoading = false; - - if (!this.selectedPath) { - return; - } - - const parsedPath = this.parseDatasetVersionPath(this.selectedPath); - this.selectedDataset = this.datasets.find( - dataset => dataset.ownerEmail === parsedPath.ownerEmail && dataset.dataset.name === parsedPath.datasetName - ); - this.isDatasetSelected = !!this.selectedDataset; - - if (this.selectedDataset?.dataset.did !== undefined) { - this.datasetService - .retrieveDatasetVersionList(this.selectedDataset.dataset.did) - .pipe(untilDestroyed(this)) - .subscribe(versions => { - this.datasetVersions = versions; - this.selectedVersion = - this.datasetVersions.find(version => version.name === parsedPath.versionName) ?? - this.datasetVersions[0]; - this.onVersionChange(); - }); + this.datasets = datasets; + const selectedPath = this.data.selectedPath; + if (selectedPath) { + const [ownerEmail, datasetName, versionName] = selectedPath.split("/").filter(part => part.length > 0); + this.selectedDataset = this.datasets.find( + dataset => dataset.ownerEmail === ownerEmail && dataset.dataset.name === datasetName + ); + this.loadDatasetVersions(versionName); } + this.loading = false; }); } onDatasetChange() { this.selectedVersion = undefined; - this.selectedFileNode = undefined; + this.selectedFilePath = undefined; this.suggestedFileTreeNodes = []; - this.isDatasetSelected = !!this.selectedDataset; - - if (this.selectedDataset?.dataset.did !== undefined) { - this.datasetService - .retrieveDatasetVersionList(this.selectedDataset.dataset.did) - .pipe(untilDestroyed(this)) - .subscribe(versions => { - this.datasetVersions = versions; - if (this.datasetVersions.length > 0) { - this.selectedVersion = this.datasetVersions[0]; - this.onVersionChange(); - } - }); - } + this.loadDatasetVersions(); } onVersionChange() { - this.selectedFileNode = undefined; + this.selectedFilePath = undefined; this.suggestedFileTreeNodes = []; + if (!this.selectFile && this.selectedDataset && this.selectedVersion) { + this.selectedFilePath = + `/${this.selectedDataset.ownerEmail}/${this.selectedDataset.dataset.name}/${this.selectedVersion.name}`; + } + if ( - this.mode === "file" && + this.selectFile && this.selectedDataset?.dataset.did !== undefined && this.selectedVersion?.dvid !== undefined ) { @@ -138,36 +101,27 @@ export class DatasetSelectionModalComponent implements OnInit { } onFileTreeNodeSelected(node: DatasetFileNode) { - this.selectedFileNode = node.type === "file" ? node : undefined; + this.selectedFilePath = getFullPathFromDatasetFileNode(node) } onConfirmSelection(): void { - if (this.mode === "version") { - if (this.selectedDataset && this.selectedVersion) { - this.modalRef.close( - `/${this.selectedDataset.ownerEmail}/${this.selectedDataset.dataset.name}/${this.selectedVersion.name}` - ); - } - return; - } - - if (this.selectedFileNode) { - this.modalRef.close(getFullPathFromDatasetFileNode(this.selectedFileNode)); - } - } - - get datasets(): ReadonlyArray { - return this._datasets; + this.modalRef.close(this.selectedFilePath); } - get isConfirmDisabled(): boolean { - return this.mode === "version" ? !(this.selectedDataset && this.selectedVersion) : !this.selectedFileNode; - } - - private parseDatasetVersionPath(path: string): ParsedDatasetVersionPath { - const parts = path.split("/").filter(part => part.length > 0); + private loadDatasetVersions(preferredVersionName?: string): void { + if (this.selectedDataset?.dataset.did === undefined) { + this.datasetVersions = []; + return; + } - const [ownerEmail, datasetName, versionName] = parts; - return { ownerEmail, datasetName, versionName }; + this.datasetService + .retrieveDatasetVersionList(this.selectedDataset.dataset.did) + .pipe(untilDestroyed(this)) + .subscribe(versions => { + this.datasetVersions = versions; + this.selectedVersion = + versions.find(version => version.name === preferredVersionName) ?? versions[0]; + this.onVersionChange(); + }); } } diff --git a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html index 24bc017b0ba..f7fcdf78d40 100644 --- a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html @@ -17,7 +17,7 @@ under the License. --> nzContent: DatasetSelectionModalComponent, nzFooter: null, nzData: { - mode: "version", + selectFile: false, selectedPath: this.formControl.getRawValue(), }, nzBodyStyle: { From efd22209e5e9627683c767318dfdce770b3cd2b2 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Mon, 13 Apr 2026 12:24:30 -0700 Subject: [PATCH 19/23] revert change --- .../src/main/scala/org/apache/texera/config/GuiConfig.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala b/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala index 232b6c54729..adc789c9843 100644 --- a/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala +++ b/common/config/src/main/scala/org/apache/texera/config/GuiConfig.scala @@ -49,6 +49,8 @@ object GuiConfig { conf.getInt("gui.workflow-workspace.default-data-transfer-batch-size") val guiWorkflowWorkspaceDefaultExecutionMode: String = conf.getString("gui.workflow-workspace.default-execution-mode") + val guiWorkflowWorkspaceSelectingFilesFromDatasetsEnabled: Boolean = + conf.getBoolean("gui.workflow-workspace.selecting-files-from-datasets-enabled") val guiWorkflowWorkspaceWorkflowExecutionsTrackingEnabled: Boolean = conf.getBoolean("gui.workflow-workspace.workflow-executions-tracking-enabled") val guiWorkflowWorkspaceLinkBreakpointEnabled: Boolean = From ae027e041ee33c16d25e036bc6718e1c492281ca Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Mon, 13 Apr 2026 13:02:39 -0700 Subject: [PATCH 20/23] fix fmt --- .../dataset-selection-modal.component.html | 3 ++- .../dataset-selection-modal.component.scss | 10 +--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html index 31f62503012..bd316dbdba4 100644 --- a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html @@ -32,7 +32,7 @@ [nzCustomContent]="true">
#{{ dataset.dataset.did }}   - {{ dataset.dataset.name }} + {{ dataset.dataset.name }}  {{ dataset.isOwner ? 'OWNER' : dataset.accessPrivilege }}
@@ -60,6 +60,7 @@