diff --git a/core/gui/src/app/dashboard/user/component/user-dataset/user-dataset-explorer/user-dataset-explorer.component.html b/core/gui/src/app/dashboard/user/component/user-dataset/user-dataset-explorer/user-dataset-explorer.component.html index 1feaad53995..d491bc98cc8 100644 --- a/core/gui/src/app/dashboard/user/component/user-dataset/user-dataset-explorer/user-dataset-explorer.component.html +++ b/core/gui/src/app/dashboard/user/component/user-dataset/user-dataset-explorer/user-dataset-explorer.component.html @@ -157,6 +157,31 @@
Choose a Version:
(click)="onClickOpenVersionCreator()"> + Version + +
+
+ + +
diff --git a/core/gui/src/app/dashboard/user/component/user-dataset/user-dataset-explorer/user-dataset-explorer.component.ts b/core/gui/src/app/dashboard/user/component/user-dataset/user-dataset-explorer/user-dataset-explorer.component.ts index 6df80a5a649..885ddd87d31 100644 --- a/core/gui/src/app/dashboard/user/component/user-dataset/user-dataset-explorer/user-dataset-explorer.component.ts +++ b/core/gui/src/app/dashboard/user/component/user-dataset/user-dataset-explorer/user-dataset-explorer.component.ts @@ -10,13 +10,19 @@ import { import { DatasetVersion } from "../../../../../common/type/dataset"; import { switchMap } from "rxjs/operators"; import { NotificationService } from "../../../../../common/service/notification/notification.service"; - +import { Injectable } from "@angular/core"; +import { UserWorkflowService } from "../../../service/user-workflow/user-workflow.service"; +import { EnvironmentService } from "../../../service/user-environment/environment.service"; +import { WorkflowPersistService } from "src/app/common/service/workflow-persist/workflow-persist.service"; +import { WorkflowActionService } from "src/app/workspace/service/workflow-graph/model/workflow-action.service"; +import { Point } from "plotly.js-basic-dist-min"; @UntilDestroy() @Component({ templateUrl: "./user-dataset-explorer.component.html", styleUrls: ["./user-dataset-explorer.component.scss"], }) export class UserDatasetExplorerComponent implements OnInit { + public scanOption: string = ""; public did: number | undefined; public datasetName: string = ""; public datasetDescription: string = ""; @@ -41,7 +47,10 @@ export class UserDatasetExplorerComponent implements OnInit { private route: ActivatedRoute, private router: Router, private datasetService: DatasetService, - private notificationService: NotificationService + private notificationService: NotificationService, + private userWorkflowService: UserWorkflowService, + private environmentService: EnvironmentService, + private workflowPersistService: WorkflowPersistService ) {} // item for control the resizeable sider @@ -224,6 +233,51 @@ export class UserDatasetExplorerComponent implements OnInit { this.isRightBarCollapsed = !this.isRightBarCollapsed; } + onClickCreateWorkflowFromDataset(): void { + const datasetFile: string = "/" + this.datasetName + this.currentDisplayedFileName; + this.userWorkflowService + .onClickCreateNewWorkflowFromDatasetDashboard(datasetFile, this.scanOption) // initializes a scan operator in workflow + .pipe(untilDestroyed(this)) + .subscribe({ + next: wid => { + if (wid && this.did) { + // initialize workflow action service + this.retrieveEnvironmentAndAddDataset(wid); + } + }, + }); + } + + private retrieveEnvironmentAndAddDataset(wid: number): void { + this.workflowPersistService + .retrieveWorkflowEnvironment(wid) + .pipe(untilDestroyed(this)) + .subscribe({ + next: env => { + if (env.eid && this.did) { + this.addDatasetToEnvironmentAndNavigate(env.eid, wid); + } + }, + }); + } + + private addDatasetToEnvironmentAndNavigate(eid: number, wid: number): void { + if (this.did != null && this.did != undefined) { + this.environmentService + .addDatasetToEnvironment(eid, this.did) + .pipe(untilDestroyed(this)) + .subscribe({ + next: response => { + this.navigateToWorkflowPage(wid); + }, + }); + } + } + + private navigateToWorkflowPage(wid: number): void { + this.router.navigate([`/workflow/${wid}`]); + } + onVersionSelected(version: DatasetVersion): void { this.selectedVersion = version; if (this.did && this.selectedVersion.dvid) diff --git a/core/gui/src/app/dashboard/user/service/user-workflow/user-workflow.service.ts b/core/gui/src/app/dashboard/user/service/user-workflow/user-workflow.service.ts new file mode 100644 index 00000000000..f4fa03dc6b3 --- /dev/null +++ b/core/gui/src/app/dashboard/user/service/user-workflow/user-workflow.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from "@angular/core"; +import { Input, ViewChild } from "@angular/core"; +import { Router } from "@angular/router"; +import { NzModalService } from "ng-zorro-antd/modal"; +import { firstValueFrom, observable, of } from "rxjs"; +import { + DEFAULT_WORKFLOW_NAME, + WorkflowPersistService, +} from "../../../../common/service/workflow-persist/workflow-persist.service"; +import { DashboardEntry } from "../../type/dashboard-entry"; +import { UserService } from "../../../../common/service/user/user.service"; +import { untilDestroyed } from "@ngneat/until-destroy"; +import { NotificationService } from "../../../../common/service/notification/notification.service"; +import { WorkflowContent } from "../../../../common/type/workflow"; +import { FileSaverService } from "../../service/user-file/file-saver.service"; +import { FiltersComponent } from "../../component/filters/filters.component"; +import { SearchResultsComponent } from "../../component/search-results/search-results.component"; +import { SearchService } from "../../service/search.service"; +import { SortMethod } from "../../type/sort-method"; +import { isDefined } from "../../../../common/util/predicate"; +import { UserProjectService } from "../../service/user-project/user-project.service"; +import { map, mergeMap, tap } from "rxjs/operators"; +import { Observable } from "rxjs"; +import { WorkflowActionService } from "../../../../workspace/service/workflow-graph/model/workflow-action.service"; +import { WorkflowUtilService } from "../../../../workspace/service/workflow-graph/util/workflow-util.service"; + +import { observe } from "@ngx-formly/core/lib/utils"; +import { Point } from "../../../../workspace/types/workflow-common.interface"; + +/** + * UserWorkflowService facilitates creating a new workflow from the dataset dashboard + */ + +@Injectable({ + providedIn: "root", +}) +export class UserWorkflowService { + public ROUTER_WORKFLOW_BASE_URL = "workflow"; + private _searchResultsComponent?: SearchResultsComponent; + @ViewChild(SearchResultsComponent) get searchResultsComponent(): SearchResultsComponent { + if (this._searchResultsComponent) { + return this._searchResultsComponent; + } + throw new Error("Property cannot be accessed before it is initialized."); + } + set searchResultsComponent(value: SearchResultsComponent) { + this._searchResultsComponent = value; + } + private _filters?: FiltersComponent; + @ViewChild(FiltersComponent) get filters(): FiltersComponent { + if (this._filters) { + return this._filters; + } + throw new Error("Property cannot be accessed before it is initialized."); + } + set filters(value: FiltersComponent) { + value.masterFilterListChange.pipe(untilDestroyed(this)).subscribe({ next: () => this.search() }); + this._filters = value; + } + private masterFilterList: ReadonlyArray | null = null; + + // receive input from parent components (UserProjectSection), if any + @Input() public pid?: number = undefined; + @Input() public accessLevel?: string = undefined; + public sortMethod = SortMethod.EditTimeDesc; + lastSortMethod: SortMethod | null = null; + + constructor( + private workflowPersistService: WorkflowPersistService, + private userProjectService: UserProjectService, + private searchService: SearchService, + private workflowUtilService: WorkflowUtilService + ) {} + + /* Creates a workflow from the dataset dashboard with a pre-initialized scan operator*/ + public onClickCreateNewWorkflowFromDatasetDashboard( + datasetFile: string, + scanOption: string + ): Observable { + let operatorPredicate = this.workflowUtilService.getNewOperatorPredicate(scanOption); + operatorPredicate = this.workflowUtilService.addFileName(operatorPredicate, datasetFile); // add the filename + + let localPid = this.pid; + let point: Point = { x: 474, y: 235 }; + let emptyWorkflowContent: WorkflowContent = { + operators: [operatorPredicate], + commentBoxes: [], + groups: [], + links: [], + operatorPositions: { + [operatorPredicate.operatorID]: { x: 474, y: 235 }, + }, + }; + + return this.workflowPersistService.createWorkflow(emptyWorkflowContent, DEFAULT_WORKFLOW_NAME).pipe( + tap(createdWorkflow => { + if (!createdWorkflow.workflow.wid) { + throw new Error("Workflow creation failed."); + } + }), + mergeMap(createdWorkflow => { + // Check if localPid is defined; if so, add the workflow to the project + if (localPid) { + return this.userProjectService.addWorkflowToProject(localPid, createdWorkflow.workflow.wid!).pipe( + // Regardless of the project addition outcome, pass the wid downstream + map(() => createdWorkflow.workflow.wid) + ); + } else { + // If there's no localPid, skip adding to the project and directly pass the wid downstream + return of(createdWorkflow.workflow.wid); + } + }) + //untilDestroyed(this) + ); + } + + /** + * Searches workflows with keywords and filters given in the masterFilterList. + * @returns + */ + async search(forced: Boolean = false): Promise { + const sameList = + this.masterFilterList !== null && + this.filters.masterFilterList.length === this.masterFilterList.length && + this.filters.masterFilterList.every((v, i) => v === this.masterFilterList![i]); + if (!forced && sameList && this.sortMethod === this.lastSortMethod) { + // If the filter lists are the same, do no make the same request again. + return; + } + this.lastSortMethod = this.sortMethod; + this.masterFilterList = this.filters.masterFilterList; + let filterParams = this.filters.getSearchFilterParameters(); + if (isDefined(this.pid)) { + // force the project id in the search query to be the current pid. + filterParams.projectIds = [this.pid]; + } + this.searchResultsComponent.reset(async (start, count) => { + const results = await firstValueFrom( + this.searchService.search( + this.filters.getSearchKeywords(), + filterParams, + start, + count, + "workflow", + this.sortMethod + ) + ); + return { + entries: results.results.map(i => { + if (i.workflow) { + return new DashboardEntry(i.workflow); + } else { + throw new Error("Unexpected type in SearchResult."); + } + }), + more: results.more, + }; + }); + await this.searchResultsComponent.loadMore(); + } +} diff --git a/core/gui/src/app/workspace/service/workflow-graph/util/workflow-util.service.ts b/core/gui/src/app/workspace/service/workflow-graph/util/workflow-util.service.ts index e570ec297f8..52cd13234c2 100644 --- a/core/gui/src/app/workspace/service/workflow-graph/util/workflow-util.service.ts +++ b/core/gui/src/app/workspace/service/workflow-graph/util/workflow-util.service.ts @@ -216,4 +216,17 @@ export class WorkflowUtilService { outputPorts, }; } + + /* Helper function for initializing scan operators with file to scan included.*/ + public addFileName(op: OperatorPredicate, fileName: string): OperatorPredicate { + const updatedPredicate: OperatorPredicate = { ...op }; + const updatedProperties = { ...updatedPredicate.operatorProperties, fileName }; + + const updatedOperatorPredicate: OperatorPredicate = { + ...updatedPredicate, + operatorProperties: updatedProperties, + }; + + return updatedOperatorPredicate; + } }