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..3a0f0a5c4b1 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.FileListerSourceOpDesc 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[FileListerSourceOpDesc], name = "FileLister"), 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/FileListerSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpDesc.scala new file mode 100644 index 00000000000..1101dce7e00 --- /dev/null +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpDesc.scala @@ -0,0 +1,68 @@ +/* + * 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 FileListerSourceOpDesc 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.FileListerSourceOpExec", + 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 = "File Lister", + 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/FileListerSourceOpExec.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExec.scala new file mode 100644 index 00000000000..f715a0943fa --- /dev/null +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExec.scala @@ -0,0 +1,60 @@ +/* + * 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 FileListerSourceOpExec private[dataset] (descString: String) + extends SourceOperatorExecutor { + private val desc: FileListerSourceOpDesc = + objectMapper.readValue(descString, classOf[FileListerSourceOpDesc]) + + 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/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpDescSpec.scala new file mode 100644 index 00000000000..a5aa744ff5a --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpDescSpec.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 FileListerSourceOpDescSpec extends AnyFlatSpec { + + "FileListerSourceOpDesc" should "expose a filename output column" in { + val opDesc = new FileListerSourceOpDesc() + + 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 FileListerSourceOpDesc() + + assert(opDesc.operatorInfo.userFriendlyName == "File Lister") + assert(opDesc.operatorInfo.inputPorts.isEmpty) + assert(opDesc.operatorInfo.outputPorts.length == 1) + } +} diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index b6364e781f9..11ce2f26f89 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -103,7 +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 { 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"; @@ -152,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"; @@ -257,8 +258,9 @@ registerLocaleData(en); AgentPanelComponent, AgentChatComponent, AgentRegistrationComponent, - InputAutoCompleteComponent, - FileSelectionComponent, + DatasetFileSelectorComponent, + DatasetVersionSelectorComponent, + 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 d950bd3690c..c3995abb544 100644 --- a/frontend/src/app/common/formly/formly-config.ts +++ b/frontend/src/app/common/formly/formly-config.ts @@ -24,9 +24,10 @@ 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"; /** * Configuration for using Json Schema with Formly. @@ -76,7 +77,8 @@ 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 }, ], wrappers: [ diff --git a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.html b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html similarity index 52% rename from frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.html rename to frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html index 53ee531fa6e..b4f0b58d5e4 100644 --- a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.html +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.html @@ -16,27 +16,16 @@ specific language governing permissions and limitations under the License. --> - -
- - -
- + + diff --git a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts similarity index 64% rename from frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts rename to frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts index 38839f377e5..a1fba605b3e 100644 --- a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.ts +++ b/frontend/src/app/workspace/component/dataset-file-selector/dataset-file-selector.component.ts @@ -22,17 +22,14 @@ 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 { DatasetSelectionModalComponent } from "../dataset-selection-modal/dataset-selection-modal.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"], + templateUrl: "dataset-file-selector.component.html", }) -export class InputAutoCompleteComponent extends FieldType { +export class DatasetFileSelectorComponent extends FieldType { constructor( private modalService: NzModalService, public workflowActionService: WorkflowActionService, @@ -43,14 +40,13 @@ export class InputAutoCompleteComponent extends FieldType { onClickOpenFileSelectionModal(): void { const modal = this.modalService.create({ - nzTitle: "Please select one file from datasets", - nzContent: FileSelectionComponent, + nzContent: DatasetSelectionModalComponent, nzFooter: null, nzData: { - selectedFilePath: this.formControl.getRawValue(), + selectFile: true, + selectedPath: this.formControl.getRawValue(), }, nzBodyStyle: { - // Enables the file selection window to be resizable resize: "both", overflow: "auto", minHeight: "200px", @@ -60,22 +56,14 @@ 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); + } }); } - get enableDatasetSource(): boolean { - return this.config.env.selectingFilesFromDatasetsEnabled; - } - get isFileSelectionEnabled(): boolean { - return this.enableDatasetSource; - } - - get selectedFilePath(): string | null { - return this.formControl.value; + return this.config.env.selectingFilesFromDatasetsEnabled; } } 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..11cd7b1c3de --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.html @@ -0,0 +1,69 @@ + + + +
+ + +
+ #{{ dataset.dataset.did }}   + {{ dataset.dataset.name }}  + {{ dataset.isOwner ? 'OWNER' : dataset.accessPrivilege }} +
+
+
+ + + + + +
+ + + + + +
diff --git a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.scss b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss similarity index 65% rename from frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.scss rename to frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss index cc76590399a..7016c761cd3 100644 --- a/frontend/src/app/workspace/component/input-autocomplete/input-autocomplete.component.scss +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.scss @@ -17,34 +17,15 @@ * under the License. */ -mat-form-field { - width: 100%; -} -.input-autocomplete-container { +.dataset-dropdown { 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; - } +.dataset-name { + overflow: hidden; + text-overflow: ellipsis; +} - &:focus { - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); - } - } +.access-level{ + color: grey; } 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..16a457b1a50 --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts @@ -0,0 +1,127 @@ +/** + * 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"; + +@UntilDestroy() +@Component({ + templateUrl: "dataset-selection-modal.component.html", + styleUrls: ["dataset-selection-modal.component.scss"], +}) +export class DatasetSelectionModalComponent implements OnInit { + private readonly data = inject(NZ_MODAL_DATA) as { + selectFile: boolean; + selectedPath?: string | null; + }; + + readonly selectFile: boolean = this.data.selectFile; + + loading = true; + + datasets: ReadonlyArray = []; + datasetVersions: ReadonlyArray = []; + selectedDataset?: DashboardDataset; + selectedVersion?: DatasetVersion; + suggestedFileTreeNodes: DatasetFileNode[] = []; + selectedFilePath?: string; + + constructor( + private modalRef: NzModalRef, + private datasetService: DatasetService + ) {} + + ngOnInit() { + this.datasetService + .retrieveAccessibleDatasets() + .pipe(untilDestroyed(this)) + .subscribe(datasets => { + 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.selectedFilePath = undefined; + this.suggestedFileTreeNodes = []; + this.loadDatasetVersions(); + } + + onVersionChange() { + 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.selectFile && + 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.selectedFilePath = getFullPathFromDatasetFileNode(node) + } + + onConfirmSelection(): void { + this.modalRef.close(this.selectedFilePath); + } + + private loadDatasetVersions(preferredVersionName?: string): void { + if (this.selectedDataset?.dataset.did === undefined) { + this.datasetVersions = []; + return; + } + + 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 new file mode 100644 index 00000000000..f7fcdf78d40 --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.html @@ -0,0 +1,30 @@ + + + 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..c2cdc3a91c4 --- /dev/null +++ b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.ts @@ -0,0 +1,60 @@ +/** + * 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 { NzModalService } from "ng-zorro-antd/modal"; +import { DatasetSelectionModalComponent } from "../dataset-selection-modal/dataset-selection-modal.component"; + +@UntilDestroy() +@Component({ + templateUrl: "dataset-version-selector.component.html", +}) +export class DatasetVersionSelectorComponent extends FieldType { + constructor(private modalService: NzModalService) { + super(); + } + + onClickOpenDatasetSelectionModal(): void { + const modal = this.modalService.create({ + nzContent: DatasetSelectionModalComponent, + nzFooter: null, + nzData: { + selectFile: false, + selectedPath: this.formControl.getRawValue(), + }, + nzBodyStyle: { + resize: "both", + overflow: "auto", + minHeight: "200px", + minWidth: "550px", + maxWidth: "90vw", + maxHeight: "80vh", + }, + nzWidth: "fit-content", + }); + + modal.afterClose.pipe(untilDestroyed(this)).subscribe(selectedPath => { + if (selectedPath) { + this.formControl.setValue(selectedPath); + } + }); + } +} diff --git a/frontend/src/app/workspace/component/file-selection/file-selection.component.html b/frontend/src/app/workspace/component/file-selection/file-selection.component.html deleted file mode 100644 index f78d46102d1..00000000000 --- a/frontend/src/app/workspace/component/file-selection/file-selection.component.html +++ /dev/null @@ -1,77 +0,0 @@ - - - -
-
- - -
-
{{ dataset.dataset.did }}
- {{ dataset.dataset.name }} - - OWNER - - - {{ dataset.accessPrivilege }} - -
-
-
- - - - - -
- - - -
-
diff --git a/frontend/src/app/workspace/component/file-selection/file-selection.component.scss b/frontend/src/app/workspace/component/file-selection/file-selection.component.scss deleted file mode 100644 index 0e7ceadbf9b..00000000000 --- a/frontend/src/app/workspace/component/file-selection/file-selection.component.scss +++ /dev/null @@ -1,89 +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; /* Add animation effect */ -} - -.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); /* Adjust to fit other elements */ - 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; -} diff --git a/frontend/src/app/workspace/component/file-selection/file-selection.component.ts b/frontend/src/app/workspace/component/file-selection/file-selection.component.ts deleted file mode 100644 index dfae2e48350..00000000000 --- a/frontend/src/app/workspace/component/file-selection/file-selection.component.ts +++ /dev/null @@ -1,127 +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 } 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"; - -@UntilDestroy() -@Component({ - selector: "texera-file-selection-model", - templateUrl: "file-selection.component.html", - styleUrls: ["file-selection.component.scss"], -}) -export class FileSelectionComponent implements OnInit { - readonly selectedFilePath: string = inject(NZ_MODAL_DATA).selectedFilePath; - private _datasets: ReadonlyArray = []; - - // indicate whether the accessible datasets have been loaded from the backend - isAccessibleDatasetsLoading = true; - - selectedDataset?: DashboardDataset; - selectedVersion?: DatasetVersion; - datasetVersions?: DatasetVersion[]; - suggestedFileTreeNodes: DatasetFileNode[] = []; - isDatasetSelected: boolean = false; - - constructor( - private modalRef: NzModalRef, - private datasetService: DatasetService - ) {} - - 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 == "") { - return; - } - // if users already select some file, then ONLY show that selected dataset & related version - const selectedDatasetFile = parseFilePathToDatasetFile(this.selectedFilePath); - this.selectedDataset = this.datasets.find( - d => d.ownerEmail === selectedDatasetFile.ownerEmail && d.dataset.name === selectedDatasetFile.datasetName - ); - this.isDatasetSelected = !!this.selectedDataset; - if (this.selectedDataset && 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.onVersionChange(); - }); - } - }); - } - - onDatasetChange() { - this.selectedVersion = undefined; - this.suggestedFileTreeNodes = []; - this.isDatasetSelected = !!this.selectedDataset; - if (this.selectedDataset && 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) { - this.selectedVersion = this.datasetVersions[0]; - this.onVersionChange(); - } - }); - } - } - - onVersionChange() { - this.suggestedFileTreeNodes = []; - if ( - this.selectedDataset && - this.selectedDataset.dataset.did !== undefined && - this.selectedVersion && - 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.modalRef.close(node); - } - - get datasets(): ReadonlyArray { - return this._datasets; - } -} 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/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..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,10 +449,14 @@ 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") { + 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/FileLister.png b/frontend/src/assets/operator_images/FileLister.png new file mode 100644 index 00000000000..c570e230ac2 Binary files /dev/null and b/frontend/src/assets/operator_images/FileLister.png differ