diff --git a/src/app/business-logic/api-services/pipelines.service.ts b/src/app/business-logic/api-services/pipelines.service.ts index c4e20af9..e30c9ba2 100644 --- a/src/app/business-logic/api-services/pipelines.service.ts +++ b/src/app/business-logic/api-services/pipelines.service.ts @@ -34,7 +34,7 @@ import { BASE_PATH, COLLECTION_FORMATS } from "../variables"; import { Configuration } from "../configuration"; import { PipelinesDeleteRunsRequest } from "~/business-logic/model/pipelines/pipelinesDeleteRunsRequest"; import { PipelinesDeleteRunsResponse } from "~/business-logic/model/pipelines/pipelinesDeleteRunsResponse"; -import { PipelinesCreateRequest, PipelinesCreateResponse } from "../model/pipelines/models"; +import { PipelinesCreateRequest, PipelinesCreateResponse, PipelinesCreateStepsRequest, PipelinesCreateStepsResponse } from "../model/pipelines/models"; @Injectable() export class ApiPipelinesService { @@ -225,4 +225,60 @@ export class ApiPipelinesService { } ); } + + + /** + * + * Create a new pipeline step + * @param request request body + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + + public pipelinesCreateStep( + request: PipelinesCreateStepsRequest, + options?: any, + observe: any = "body", + reportProgress: boolean = false + ): Observable { + if (request === null || request === undefined) { + throw new Error( + "Required parameter request was null or undefined when calling pipelinesCreate." + ); + } + + let headers = this.defaultHeaders; + if (options && options.async_enable) { + headers = headers.set(this.configuration.asyncHeader, "1"); + } + + // to determine the Accept header + const httpHeaderAccepts: string[] = ["application/json"]; + const httpHeaderAcceptSelected: string | undefined = + this.configuration.selectHeaderAccept(httpHeaderAccepts); + if (httpHeaderAcceptSelected != undefined) { + headers = headers.set("Accept", httpHeaderAcceptSelected); + } + + // to determine the Content-Type header + const consumes: string[] = []; + const httpContentTypeSelected: string | undefined = + this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected != undefined) { + headers = headers.set("Content-Type", httpContentTypeSelected); + } + + return this.apiRequest.post( + `${this.basePath}/pipelines.create.step`, + request, + { + withCredentials: this.configuration.withCredentials, + headers: headers, + observe: observe, + reportProgress: reportProgress, + } + ); + } + + } diff --git a/src/app/business-logic/model/pipelines/models.ts b/src/app/business-logic/model/pipelines/models.ts index ef315433..070a4379 100644 --- a/src/app/business-logic/model/pipelines/models.ts +++ b/src/app/business-logic/model/pipelines/models.ts @@ -2,4 +2,7 @@ export * from '././pipelinesStartPipelineRequest'; export * from '././pipelinesStartPipelineResponse'; export * from '././pipelinesCreateRequest'; export * from '././pipelinesCreateResponse'; +export * from '././pipelinesCreateStepsRequest'; +export * from '././pipelinesCreateStepsResponse'; + export * from "././pipeline"; \ No newline at end of file diff --git a/src/app/business-logic/model/pipelines/pipelinesCreateStepsRequest.ts b/src/app/business-logic/model/pipelines/pipelinesCreateStepsRequest.ts new file mode 100644 index 00000000..b1e213f0 --- /dev/null +++ b/src/app/business-logic/model/pipelines/pipelinesCreateStepsRequest.ts @@ -0,0 +1,14 @@ +export interface PipelinesCreateStepsRequest { + /** + * Pipeline name. Unique within the company. + */ + name: string; + /** + * Free text comment + */ + description?: string; + + experiment?: string; + + parameters?: Array +} diff --git a/src/app/business-logic/model/pipelines/pipelinesCreateStepsResponse.ts b/src/app/business-logic/model/pipelines/pipelinesCreateStepsResponse.ts new file mode 100644 index 00000000..37d6aa21 --- /dev/null +++ b/src/app/business-logic/model/pipelines/pipelinesCreateStepsResponse.ts @@ -0,0 +1,7 @@ + +export interface PipelinesCreateStepsResponse { + /** + * Pipeline id + */ + id?: string; +} diff --git a/src/app/webapp-common/pipelines/edit-pipeline-header/edit-pipeline-header.component.html b/src/app/webapp-common/pipelines/edit-pipeline-header/edit-pipeline-header.component.html index 0b0d9613..2f6cf56b 100644 --- a/src/app/webapp-common/pipelines/edit-pipeline-header/edit-pipeline-header.component.html +++ b/src/app/webapp-common/pipelines/edit-pipeline-header/edit-pipeline-header.component.html @@ -39,7 +39,7 @@ - + + \ No newline at end of file diff --git a/src/app/webapp-common/pipelines/pipeline-add-step-dialog/pipeline-add-step-form/pipeline-add-step-form.component.scss b/src/app/webapp-common/pipelines/pipeline-add-step-dialog/pipeline-add-step-form/pipeline-add-step-form.component.scss new file mode 100644 index 00000000..8bb42ffb --- /dev/null +++ b/src/app/webapp-common/pipelines/pipeline-add-step-dialog/pipeline-add-step-form/pipeline-add-step-form.component.scss @@ -0,0 +1,12 @@ +:host { + .create-report-button { + padding: 32px 12px 0; + } + mat-form-field { + width: 100%; + + .report-description { + min-height: 68px; + } + } +} diff --git a/src/app/webapp-common/pipelines/pipeline-add-step-dialog/pipeline-add-step-form/pipeline-add-step-form.component.ts b/src/app/webapp-common/pipelines/pipeline-add-step-dialog/pipeline-add-step-form/pipeline-add-step-form.component.ts new file mode 100644 index 00000000..f2be7891 --- /dev/null +++ b/src/app/webapp-common/pipelines/pipeline-add-step-dialog/pipeline-add-step-form/pipeline-add-step-form.component.ts @@ -0,0 +1,137 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, OnDestroy, + Output, + ViewChild +} from '@angular/core'; +import {NgModel} from '@angular/forms'; +import {Observable, Subscription} from 'rxjs'; +import {trackByValue} from '@common/shared/utils/forms-track-by'; +import {MatOptionSelectionChange} from '@angular/material/core'; +import {rootProjectsPageSize} from '@common/constants'; +import { + IOption +} from '@common/shared/ui-components/inputs/select-autocomplete-with-chips/select-autocomplete-with-chips.component'; +import { Task } from '~/business-logic/model/tasks/task'; + + +@Component({ + selector: 'sm-pipeline-add-step-form', + templateUrl: './pipeline-add-step-form.component.html', + styleUrls: ['./pipeline-add-step-form.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PipelineAddStepFormComponent implements OnChanges, OnDestroy { + public filteredExperiments$: Observable<{ label: string; value: string }[]>; + private _experiments: Task[]; + public experimentsOptions: { label: string; value: string }[]; + public trackByValue = trackByValue; + public panelHeight: number; + private subs = new Subscription(); + private rootFiltered: boolean; + public readonly experimentsRoot = {label: 'My experiment', value: null}; + @ViewChild('experimentInput') experimentInput: NgModel; + + public pipelinesNames: Array; + public experimentsNames: Array; + public step: { name: string; description: string; experiment: { label: string; value: string }, parameters: Array } = { + name: null, + description: '', + experiment: null, + parameters: [], + }; + filterText: string = ''; + isAutoCompleteOpen: boolean; + + @Input() readOnlyExperimentsNames: string[]; + @Input() defaultExperimentId: string; + public loading: boolean; + public noMoreOptions: boolean; + private previousLength: number | undefined; + + @Input() set experiments(experiments: Task[]) { + + this.loading = false; + this.noMoreOptions = experiments?.length === this.previousLength || experiments?.length < rootProjectsPageSize; + this.previousLength = experiments?.length; + this._experiments = experiments; + this.experimentsOptions = [ + ...((this.rootFiltered || experiments === null) ? [] : [this.experimentsRoot]), + ...(experiments ? experiments.map(experiment => ({label: experiment.name, value: experiment.id})) : []) + ]; + this.experimentsNames = this.experimentsOptions.map(experiment => experiment.label); + } + + get experiments() { + return this._experiments; + } + + @Output() stepCreated = new EventEmitter(); + @Output() filterSearchChanged = new EventEmitter<{value: string; loadMore?: boolean}>(); + + ngOnInit(): void { + this.searchChanged(['*', null].includes(this.defaultExperimentId) ? '' : this.defaultExperimentId); + setTimeout(() => { + this.subs.add(this.experimentInput.valueChanges.subscribe(searchString => { + if (searchString !== this.step.experiment) { + this.searchChanged(searchString?.label || searchString || ''); + } + }) + ); + }); + } + + ngOnDestroy(): void { + this.subs.unsubscribe(); + } + + ngOnChanges(): void { + if (this.experiments?.length > 0 && this.step.experiment === null) { + this.step.experiment = this.experimentsOptions.find(p => p.value === this.defaultExperimentId) || {label: this.experimentsRoot.label, value: null}; + this.experimentInput.control.updateValueAndValidity(); + } + } + + createNewSelected($event: MatOptionSelectionChange) { + this.step.experiment = {label: $event.source.value, value: null}; + } + + experimentSelected($event: MatOptionSelectionChange) { + this.step.experiment = {label: $event.source.value.label, value: $event.source.value.value}; + } + setIsAutoCompleteOpen(focus: boolean) { + this.isAutoCompleteOpen = focus; + } + + displayFn(experiment: IOption | string) { + return typeof experiment === 'string' ? experiment : experiment?.label; + } + + clear() { + this.experimentInput.control.setValue(''); + } + + send() { + this.stepCreated.emit(this.step); + } + + searchChanged(searchString: string) { + this.experimentsOptions = null; + this.experimentsNames = null; + this.rootFiltered = searchString && !this.experimentsRoot.label.toLowerCase().includes(searchString.toLowerCase()); + searchString !== null && this.filterSearchChanged.emit({value: searchString, loadMore: false}); + } + + loadMore(searchString) { + this.loading = true; + this.filterSearchChanged.emit({value: searchString || '', loadMore: true}); + } + + isFocused(locationRef: HTMLInputElement) { + return document.activeElement === locationRef; + } +} + diff --git a/src/app/webapp-common/pipelines/pipelines.actions.ts b/src/app/webapp-common/pipelines/pipelines.actions.ts index cf6b1ac5..600f80cd 100644 --- a/src/app/webapp-common/pipelines/pipelines.actions.ts +++ b/src/app/webapp-common/pipelines/pipelines.actions.ts @@ -2,6 +2,8 @@ import {createAction, props} from '@ngrx/store'; // import {ReportsGetAllExResponse} from '~/business-logic/model/reports/reportsGetAllExResponse'; // import {IReport} from './reports.consts'; import { Pipeline, PipelinesCreateRequest } from '~/business-logic/model/pipelines/models'; +import { PipelinesCreateStepsRequest } from '~/business-logic/model/pipelines/pipelinesCreateStepsRequest'; +import { Task } from '~/business-logic/model/tasks/task'; export const PIPELINES_PREFIX = 'PIPELINES_'; @@ -10,7 +12,21 @@ export const createPipeline = createAction( props<{ pipelinesCreateRequest: PipelinesCreateRequest }>() ); +export const createPipelineStep = createAction( + PIPELINES_PREFIX + 'CREATE_PIPELINE_STEP', + props<{ pipelinesCreateStepRequest: PipelinesCreateStepsRequest }>() +); +export const getAllExperiments = createAction( + PIPELINES_PREFIX + 'GET_EXPERIMENTS', + props<{ query: string; regExp?: boolean }>() +); + + +export const setExperimentsResults = createAction( + PIPELINES_PREFIX + 'SET_EXPERIMENTS', + props<{ experiments: Task[]}>() +); export const updateProject = createAction( PIPELINES_PREFIX + '[update pipeline]', diff --git a/src/app/webapp-common/pipelines/pipelines.effects.ts b/src/app/webapp-common/pipelines/pipelines.effects.ts index 792a5ec6..3f622b44 100644 --- a/src/app/webapp-common/pipelines/pipelines.effects.ts +++ b/src/app/webapp-common/pipelines/pipelines.effects.ts @@ -6,7 +6,7 @@ import {catchError, filter, map, mergeMap, switchMap, /* tap */} from 'rxjs/oper import {activeLoader, /* addMessage, */ deactivateLoader, setServerError} from '../core/actions/layout.actions'; import {requestFailed} from '../core/actions/http.actions'; import { - createPipeline + createPipeline, createPipelineStep, getAllExperiments, setExperimentsResults } from './pipelines.actions'; // import {ApiReportsService} from '~/business-logic/api-services/reports.service'; /* import {IReport, PAGE_SIZE} from './reports.consts'; @@ -30,10 +30,11 @@ import { selectSelectedProjectId } from '../core/reducers/projects.reducer'; import {TABLE_SORT_ORDER} from '../shared/ui-components/data/table/table.consts'; -import {selectCurrentUser, selectShowOnlyUserWork} from '../core/reducers/users-reducer'; + import {escapeRegex} from '../shared/utils/escape-regex'; import {MESSAGES_SEVERITY} from '../constants'; */ import {MatDialog} from '@angular/material/dialog'; +import {selectCurrentUser} from '../core/reducers/users-reducer'; /* import { ChangeProjectDialogComponent } from '@common/experiments/shared/components/change-project-dialog/change-project-dialog.component'; @@ -45,6 +46,8 @@ import {selectActiveWorkspaceReady} from '~/core/reducers/view.reducer'; import {HttpClient} from '@angular/common/http'; import { PipelinesCreateResponse } from '~/business-logic/model/pipelines/pipelinesCreateResponse'; import { ApiPipelinesService } from '~/business-logic/api-services/pipelines.service'; +import { PipelinesCreateStepsResponse } from '~/business-logic/model/pipelines/pipelinesCreateStepsResponse'; +import { ApiTasksService } from '~/business-logic/api-services/tasks.service'; /* import {selectRouterParams} from '@common/core/reducers/router-reducer'; import {setMainPageTagsFilter} from '@common/core/actions/projects.actions'; import {cleanTag} from '@common/shared/utils/helpers.util'; @@ -59,6 +62,7 @@ export class PipelinesEffects { private route: ActivatedRoute, private router: Router, private pipelinesApiService: ApiPipelinesService, + private experimentsApiService: ApiTasksService, private http: HttpClient, private matDialog: MatDialog, // public projectsApi: ApiProjectsService, @@ -66,7 +70,7 @@ export class PipelinesEffects { } activeLoader = createEffect(() => this.actions.pipe( - ofType(/* getReports, getReport, */ createPipeline,/* updateReport, restoreReport, archiveReport */), + ofType(/* getReports, getReport, */ createPipeline, createPipelineStep, getAllExperiments/* updateReport, restoreReport, archiveReport */), filter(action => !action['refresh']), map(action => activeLoader(action.type)) )); @@ -88,6 +92,51 @@ export class PipelinesEffects { }))) )); + + createPipelineStep$ = createEffect(() => this.actions.pipe( + ofType(createPipelineStep), + switchMap((action) => this.pipelinesApiService.pipelinesCreateStep(action.pipelinesCreateStepRequest) + .pipe(mergeMap((res: PipelinesCreateStepsResponse) => { + // eslint-disable-next-line no-console + console.log(res) + // this.router.navigate(['pipelines', res.id, 'edit']); + return [deactivateLoader(createPipeline.type)]; + }), + catchError(err => { + return [ + requestFailed(err), + setServerError(err, null, 'failed to create a new pipeline step'), + deactivateLoader(createPipelineStep.type), + ] + }))) + )); + + + + + + getAllExperiments$ = createEffect(() => this.actions.pipe( + ofType(getAllExperiments), + switchMap((action) => this.experimentsApiService.tasksGetAllEx({ + _any_: { + pattern: action.query ? action.query : '', + fields: ['name', 'id'] + }, + size: 20, + // user: this.store.select(selectCurrentUser)?.id, + only_fields: ['name', 'created', 'status', 'type', 'user.name', 'id', 'company'], + // order_by: orderBy, + // type: [excludedKey, 'annotation_manual', excludedKey, 'annotation', excludedKey, 'dataset_import'], + // system_tags: ['-archived', '-pipeline', '-dataset'], + search_hidden: false, + /* eslint-enable @typescript-eslint/naming-convention */ + }).pipe( + mergeMap(res => [setExperimentsResults({ + experiments: res.tasks, + }), deactivateLoader(getAllExperiments.type)]), + catchError(error => [deactivateLoader(getAllExperiments.type), requestFailed(error)]))) + )); + // activeLoader = createEffect(() => this.actions.pipe( // ofType(updateProject, getAllProjectsPageProjects), // map(action => activeLoader(action.type)) diff --git a/src/app/webapp-common/pipelines/pipelines.module.ts b/src/app/webapp-common/pipelines/pipelines.module.ts index 32bbc888..10349627 100644 --- a/src/app/webapp-common/pipelines/pipelines.module.ts +++ b/src/app/webapp-common/pipelines/pipelines.module.ts @@ -51,6 +51,8 @@ import { PipelineState, pipelinesReducer, PIPELINES_KEY } from "./pipelines.redu import { UserPreferences } from "@common/user-preferences"; import { createUserPrefFeatureReducer } from "@common/core/meta-reducers/user-pref-reducer"; import { PIPELINES_PREFIX } from "./pipelines.actions"; +import { PipelineAddStepDialogComponent } from "./pipeline-add-step-dialog/pipeline-add-step-dialog.component"; +import { PipelineAddStepFormComponent } from "./pipeline-add-step-dialog/pipeline-add-step-form/pipeline-add-step-form.component"; export const pipelinesSyncedKeys = ["projects.showPipelineExamples"]; const pipelinesSyncedKeys2 = ['orderBy', 'sortOrder']; @@ -96,7 +98,9 @@ const getInitState = (userPreferences: UserPreferences) => ({ declarations: [ PipelinesPageComponent, PipelineDialogComponent, + PipelineAddStepDialogComponent, CreateNewPipelineFormComponent, + PipelineAddStepFormComponent, EditPipelinePageComponent, EditPipelineHeaderComponent, ], diff --git a/src/app/webapp-common/pipelines/pipelines.reducer.ts b/src/app/webapp-common/pipelines/pipelines.reducer.ts index 2a56190b..232203da 100644 --- a/src/app/webapp-common/pipelines/pipelines.reducer.ts +++ b/src/app/webapp-common/pipelines/pipelines.reducer.ts @@ -8,6 +8,7 @@ import { resetPipelinesSearchQuery, resetReadyToDelete, setCurrentScrollId, + setExperimentsResults, setNoMorePipelines, setPipelinesOrderBy, setPipelinesSearchQuery, @@ -18,7 +19,7 @@ import { } from './pipelines.actions'; import {SearchState} from '../common-search/common-search.reducer'; import { Pipeline } from '~/business-logic/model/pipelines/pipeline'; - +import { Task } from '~/business-logic/model/tasks/task'; export const PIPELINES_KEY = 'pipelines'; @@ -50,6 +51,7 @@ export interface PipelineState { tableModeAwareness: boolean; showPipelineExamples: boolean; showDatasetExamples: boolean; + experiments: Task[]; } export const pipelinesInitState: PipelineState = { @@ -67,6 +69,7 @@ export const pipelinesInitState: PipelineState = { tableModeAwareness: true, showPipelineExamples: false, showDatasetExamples: false, + experiments: null }; const getCorrectSortingOrder = (currentSortOrder: TableSortOrderEnum, currentOrderField: string, nextOrderField: string) => { @@ -132,7 +135,11 @@ export const pipelinesReducers = [ on(setTableModeAwareness, (state, action) => ({...state, tableModeAwareness: (action as ReturnType).awareness})), on(showExamplePipelines, state => ({...state, showPipelineExamples: true})), - on(showExampleDatasets, state => ({...state, showDatasetExamples: true})) + on(showExampleDatasets, state => ({...state, showDatasetExamples: true})), + on(setExperimentsResults, (state, action) => ({ + ...state, + experiments: [...action.experiments], + })), ] as ReducerTypes[]; export const pipelinesReducer = createReducer(pipelinesInitState, ...pipelinesReducers); @@ -156,3 +163,4 @@ export const selectPipelinesScrollId = createSelector(pipelines, (state): string export const selectTableModeAwareness = createSelector(pipelines, state => state?.tableModeAwareness); export const selectShowPipelineExamples = createSelector(pipelines, state => state?.showPipelineExamples); export const selectShowDatasetExamples = createSelector(pipelines, state => state?.showDatasetExamples); +export const selectExperiments = createSelector(pipelines, state => state?.experiments); \ No newline at end of file