diff --git a/src/app/business-logic/api-services/pipelines.service.ts b/src/app/business-logic/api-services/pipelines.service.ts index 8c3d3437..253af357 100644 --- a/src/app/business-logic/api-services/pipelines.service.ts +++ b/src/app/business-logic/api-services/pipelines.service.ts @@ -345,6 +345,52 @@ export class ApiPipelinesService { ); } + + public pipelinesDeleteStep( + request: {step: string}, + 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 pipelinesDeleteStep." + ); + } + + 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.delete_step`, + request, + { + withCredentials: this.configuration.withCredentials, + headers: headers, + observe: observe, + reportProgress: reportProgress, + } + ); + } + /** * * Create a new pipeline step diff --git a/src/app/webapp-common/pipelines/edit-pipeline-page/edit-pipeline-page.component.html b/src/app/webapp-common/pipelines/edit-pipeline-page/edit-pipeline-page.component.html index 44d10838..a28f951e 100644 --- a/src/app/webapp-common/pipelines/edit-pipeline-page/edit-pipeline-page.component.html +++ b/src/app/webapp-common/pipelines/edit-pipeline-page/edit-pipeline-page.component.html @@ -3,7 +3,10 @@ [pipelineData]="selectedPipeline">
- + { - return params?.id - })); - this.selectedPipeline$ = this.store.select(selectSelectedPipeline) + this.selectedPipelineId$ = this.store.select(selectRouterParams).pipe( + map((params: Params) => { + return params?.id; + }) + ); + this.selectedPipeline$ = this.store.select(selectSelectedPipeline); } private recomputeSelectedStepInputOutputOptions() { - const incommingNodes = this.getIncomingNodesForNode(this._selectedStep, this.reactFlowState .nodes, this.reactFlowState .edges); - const options:Array = []; + const incommingNodes = this.getIncomingNodesForNode( + this._selectedStep, + this.reactFlowState.nodes, + this.reactFlowState.edges + ); + const options: Array = []; incommingNodes.forEach((node) => { - if(node.data?.experimentDetails?.execution?.artifacts?.length) { + if (node.data?.experimentDetails?.execution?.artifacts?.length) { // for now we are using only artifacts of i/o mapping. node.data.experimentDetails.execution.artifacts.forEach((artifact) => { - if(artifact.mode === ArtifactModeEnum.Output) { + if (artifact.mode === ArtifactModeEnum.Output) { options.push({ ...artifact, stepName: node.data.name, }); } - }) + }); } - }) + }); this.selectedStepInputOutputOptions = options; console.log(options); } @@ -68,80 +85,90 @@ export class EditPipelinePageComponent implements OnInit, OnDestroy { set selectedStep(data) { this._selectedStep = data; this.recomputeSelectedStepInputOutputOptions(); - } get selectedStep() { return this._selectedStep; } ngOnInit() { - this.subs.add(this.selectedPipelineId$.pipe( - ).subscribe((pipelineId) => { + this.subs.add( + this.selectedPipelineId$.pipe().subscribe((pipelineId) => { this.pipelineId = pipelineId; - setTimeout(()=> { - this.store.dispatch(getPipelineById({id: pipelineId, name: ''})) - }, 1000) - } - )); + setTimeout(() => { + this.store.dispatch(getPipelineById({ id: pipelineId, name: "" })); + }, 1000); + }) + ); - this.subs.add(this.selectedPipeline$.pipe( - ).subscribe((pipelineData) => { - this.selectedPipeline = pipelineData; - // eslint-disable-next-line no-console - console.log(pipelineData); - } - )); + this.subs.add( + this.selectedPipeline$.pipe().subscribe((pipelineData) => { + this.selectedPipeline = pipelineData; + // eslint-disable-next-line no-console + console.log(pipelineData); + }) + ); } - + ngOnDestroy() { this.subs.unsubscribe(); this.store.dispatch(resetPipelines()); this.store.dispatch(resetPipelinesSearchQuery()); } - savePipeline () { + savePipeline() { const pipelineState = cloneDeep(this.selectedPipeline); pipelineState.flow_display.nodes = this.reactFlowState.nodes; - pipelineState.flow_display.edges = this.reactFlowState.edges; - this.store.dispatch(updatePipeline({changes: {...pipelineState}})); + pipelineState.flow_display.edges = this.reactFlowState.edges; + this.store.dispatch(updatePipeline({ changes: { ...pipelineState } })); } - compilePipeline () { + compilePipeline() { const requestPayload: PipelinesCompileRequest = { pipeline_id: this.selectedPipeline.id, steps: this.selectedPipeline.flow_display.nodes.map((nodeItem) => { return { - nodeName: nodeItem?.id - } + nodeName: nodeItem?.id, + }; }), connections: this.selectedPipeline.flow_display.edges.map((edgeItem) => { return { startNodeName: edgeItem.source, endNodeName: edgeItem.target, - } - }) - } + }; + }), + }; - this.store.dispatch(compilePipeline({data: requestPayload})) + this.store.dispatch(compilePipeline({ data: requestPayload })); } - runPipeline () { - this.store.dispatch(runPipeline({data: { - pipeline_id: this.selectedPipeline.id - }})) + runPipeline() { + this.store.dispatch( + runPipeline({ + data: { + pipeline_id: this.selectedPipeline.id, + }, + }) + ); } createPipelineStep() { - - this.dialog.open(PipelineAddStepDialogComponent, { - data: {defaultExperimentId: ''}, - panelClass: 'light-theme', - width: '640px' - }) + this.dialog + .open(PipelineAddStepDialogComponent, { + data: { defaultExperimentId: "" }, + panelClass: "light-theme", + width: "640px", + }) .afterClosed() - .subscribe(pipeline => { + .subscribe((pipeline) => { if (pipeline) { - this.store.dispatch(createPipelineStep({pipelinesCreateStepRequest: {...pipeline, pipeline_id: this.pipelineId}})); + this.store.dispatch( + createPipelineStep({ + pipelinesCreateStepRequest: { + ...pipeline, + pipeline_id: this.pipelineId, + }, + }) + ); } }); @@ -151,19 +178,21 @@ export class EditPipelinePageComponent implements OnInit, OnDestroy { // }, // width: '640px' // }); - } settings() { - this.dialog.open(PipelineSettingDialogComponent, { - data: {defaultExperimentId: ''}, - panelClass: 'light-theme', - width: '640px' - }) + this.dialog + .open(PipelineSettingDialogComponent, { + data: { defaultExperimentId: "" }, + panelClass: "light-theme", + width: "640px", + }) .afterClosed() - .subscribe(pipeline => { + .subscribe((pipeline) => { if (pipeline) { - this.store.dispatch(pipelineSettings({pipelinesSettingsRequest: pipeline})); + this.store.dispatch( + pipelineSettings({ pipelinesSettingsRequest: pipeline }) + ); } }); } @@ -178,26 +207,46 @@ export class EditPipelinePageComponent implements OnInit, OnDestroy { public edgesChangedInReactFlow(data) { this.reactFlowState.edges = cloneDeep(data); this.recomputeSelectedStepInputOutputOptions(); - } public nodeSelected(data) { this.selectedStep = cloneDeep(data); } + public stepDelete(data) { + this.store.dispatch(deletePipelineStep({ stepId: data.id })); + const filteredNodes = this.reactFlowState.nodes.filter( + (node) => node?.id !== data.id + ); + const filteredEdges = this.reactFlowState.edges.filter( + (edge) => edge?.source !== data.id && edge?.target !== data.id + ); + + this.reactFlowState.nodes = cloneDeep(filteredNodes); + this.reactFlowState.edges = cloneDeep(filteredEdges); + console.log(this.reactFlowState); + setTimeout(() => this.savePipeline(), 1000) + this.selectedStep = null; + } + public selectedStepParamsChanged(changedParams) { this.reactFlowState.nodes.map((node, index) => { - if(node.id === this.selectedStep.id) { - this.reactFlowState.nodes[index].data.parameters = cloneDeep(changedParams) + if (node.id === this.selectedStep.id) { + this.reactFlowState.nodes[index].data.parameters = + cloneDeep(changedParams); } return node; }); //update node API call here. Update silently. - this.store.dispatch(updatePipelineStep({changes: { - step: this.selectedStep.id, - parameters: cloneDeep(changedParams) - }})) + this.store.dispatch( + updatePipelineStep({ + changes: { + step: this.selectedStep.id, + parameters: cloneDeep(changedParams), + }, + }) + ); //console.log(pipelineState); // pipelineState.flow_display?.nodes.map((node) => { @@ -211,21 +260,19 @@ export class EditPipelinePageComponent implements OnInit, OnDestroy { } /** - * @function getIncomingNodeIds - * @description It is used to get the incoming node ids. - * @param {object} node - Object of the node. - * @param {array} edges - list of all edges. - * @returns {array} list of incoming node ids. - */ -private getIncomingNodesForNode (node, nodes, edges) { - const incomingNodes = []; - edges.forEach((edge) => { - if (node?.id === edge.target) { - incomingNodes.push(nodes.find((node) => node.id == edge?.source)); - } - }); - return incomingNodes; -} - - + * @function getIncomingNodeIds + * @description It is used to get the incoming node ids. + * @param {object} node - Object of the node. + * @param {array} edges - list of all edges. + * @returns {array} list of incoming node ids. + */ + private getIncomingNodesForNode(node, nodes, edges) { + const incomingNodes = []; + edges.forEach((edge) => { + if (node?.id === edge.target) { + incomingNodes.push(nodes.find((node) => node.id == edge?.source)); + } + }); + return incomingNodes; + } } diff --git a/src/app/webapp-common/pipelines/edit-pipeline-page/pipeline-step-info/pipeline-step-info.component.ts b/src/app/webapp-common/pipelines/edit-pipeline-page/pipeline-step-info/pipeline-step-info.component.ts index 966e3d29..74d9ab0c 100644 --- a/src/app/webapp-common/pipelines/edit-pipeline-page/pipeline-step-info/pipeline-step-info.component.ts +++ b/src/app/webapp-common/pipelines/edit-pipeline-page/pipeline-step-info/pipeline-step-info.component.ts @@ -9,6 +9,8 @@ import { MatOptionSelectionChange } from '@angular/material/core'; import { NgModel } from '@angular/forms'; import { trackByValue } from '@common/shared/utils/forms-track-by'; import { cloneDeep } from 'lodash-es'; +import { ConfirmDialogComponent } from '@common/shared/ui-components/overlay/confirm-dialog/confirm-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; @Component({ selector: 'sm-pipeline-step-info', @@ -23,7 +25,7 @@ export class PipelineStepInfoComponent { private _step; private _ioOptions: Array<{ label: string; value: string, type: string }> ; - @Output() deleteStep = new EventEmitter(); + @Output() deleteStep = new EventEmitter(); @Output() stepParamsChanged = new EventEmitter(); @Input() set ioOptions(options: any) { @@ -37,6 +39,8 @@ export class PipelineStepInfoComponent { this._ioOptions = cloneDeep(opts); } + + get ioOptions() { return this._ioOptions; } @@ -82,11 +86,29 @@ export class PipelineStepInfoComponent { @Input() project: string; - constructor(private store: Store) { + constructor(private store: Store, private matDialog: MatDialog) { } public deleteClicked() { - this.deleteStep.emit(this._step); + this.matDialog + .open(ConfirmDialogComponent, { + data: { + title: "DELETE", + body: '

Are you sure you want to delete the step?

', + yes: "Delete", + no: "Cancel", + iconClass: "al-ico-trash", + width: 430, + }, + }) + .afterClosed().subscribe((data) => { + // eslint-disable-next-line no-console + console.log(data); + if(data?.isConfirmed) { + this.deleteStep.emit(this._step); + } + }) + /* setTimeout(() => { // eslint-disable-next-line no-console console.log(this._step); 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 index 7f6dfede..e475caa6 100644 --- 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 @@ -168,7 +168,7 @@ export class PipelineAddStepFormComponent implements OnChanges, OnDestroy { } send() { - if (this.stepParamsForm.formData.length > 0) { + if (this.stepParamsForm?.formData?.length > 0) { this.step.parameters = cloneDeep(this.stepParamsForm.formData); } this.stepCreated.emit(this.step); diff --git a/src/app/webapp-common/pipelines/pipelines.actions.ts b/src/app/webapp-common/pipelines/pipelines.actions.ts index 142ae637..724515b0 100644 --- a/src/app/webapp-common/pipelines/pipelines.actions.ts +++ b/src/app/webapp-common/pipelines/pipelines.actions.ts @@ -22,6 +22,12 @@ export const updatePipelineStep = createAction( props<{changes: Partial}>() ); +export const deletePipelineStep = createAction( + PIPELINES_PREFIX + 'DELETE_PIPELINE_STEP', + props<{stepId: string}>() +); + + export const pipelineSettings= createAction( PIPELINES_PREFIX + 'SETTINGS_PIPELINE_ACTION', props<{ pipelinesSettingsRequest: pipelinesSettingsModel }>() @@ -39,11 +45,11 @@ export const setSelectedPipeline = createAction( export const updatePipeline = createAction( - PIPELINES_PREFIX + '[update pipeline]', + PIPELINES_PREFIX + '[UPDATE_PIPELINE]', props<{changes: Partial}>() ); export const updatePipelineSuccess = createAction( - PIPELINES_PREFIX + '[update pipeline success]', + PIPELINES_PREFIX + '[UPDATE_PIPELINE_SUCCESS]', props<{changes: Partial}>() ); @@ -218,10 +224,6 @@ export const showExampleDatasets = createAction(PIPELINES_PREFIX + '[show datase // props<{ query: string; regExp?: boolean }>() // ); -// export const deleteResource = createAction( -// PIPELINES_PREFIX + 'DELETE_RESOURCE', -// props<{resource: string}>() -// ); // export const setEditMode = createAction( // PIPELINES_PREFIX + 'Set Edit Mode', diff --git a/src/app/webapp-common/pipelines/pipelines.effects.ts b/src/app/webapp-common/pipelines/pipelines.effects.ts index 8fb76f75..93ea3d65 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, /* addMessage, */ deactivateLoader, setServerError} from '../core/actions/layout.actions'; import {requestFailed} from '../core/actions/http.actions'; import {pipelineSettings, - createPipeline, createPipelineStep, getAllExperiments, getExperimentById, getPipelineById, setExperimentsResults, setSelectedPipeline, updatePipeline, updatePipelineSuccess, compilePipeline, runPipeline, updatePipelineStep + createPipeline, createPipelineStep, getAllExperiments, getExperimentById, getPipelineById, setExperimentsResults, setSelectedPipeline, updatePipeline, updatePipelineSuccess, compilePipeline, runPipeline, updatePipelineStep, deletePipelineStep } from './pipelines.actions'; // import {ApiReportsService} from '~/business-logic/api-services/reports.service'; /* import {IReport, PAGE_SIZE} from './reports.consts'; @@ -53,6 +53,7 @@ import { PipelinesUpdateResponse } from '~/business-logic/model/pipelines/pipeli import { selectSelectedPipeline } from './pipelines.reducer'; import { cloneDeep } from 'lodash-es'; import { MESSAGES_SEVERITY } from '@common/constants'; +import { ConfirmDialogComponent } from '@common/shared/ui-components/overlay/confirm-dialog/confirm-dialog.component'; /* import {selectRouterParams} from '@common/core/reducers/router-reducer'; import {setMainPageTagsFilter} from '@common/core/actions/projects.actions'; import {cleanTag} from '@common/shared/utils/helpers.util'; @@ -217,6 +218,25 @@ export class PipelinesEffects { ] }))) )); + + + deletePipelineStep$ = createEffect(() => this.actions.pipe( + ofType(deletePipelineStep), + switchMap((action) => this.pipelinesApiService.pipelinesDeleteStep({step: action.stepId}).pipe( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + mergeMap((res) => { + return [ deactivateLoader(action.type), + addMessage(MESSAGES_SEVERITY.SUCCESS, 'Pipeline step deleted successfully')] + } + + ), + catchError(error => [ + requestFailed(error), + deactivateLoader(action.type), + setServerError(error, null, 'Failed to delete pipeline step.') + ]) + )) + )); pipelineSettings$ = createEffect(() => this.actions.pipe( ofType(pipelineSettings), @@ -289,11 +309,15 @@ export class PipelinesEffects { ofType(updatePipeline), mergeMap(action => this.pipelinesApiService.pipelinesUpdate({...action.changes}) .pipe( - mergeMap((res: PipelinesUpdateResponse) => [ - deactivateLoader(action.type), - updatePipelineSuccess({changes: res}), - addMessage(MESSAGES_SEVERITY.SUCCESS, 'Pipeline saved successfully') - ]), + mergeMap((res: any) => { + /* // eslint-disable-next-line no-console + console.log("response from update pipeline",res); */ + return [ + deactivateLoader(action.type), + updatePipelineSuccess({changes: res?.pipelines}), + addMessage(MESSAGES_SEVERITY.SUCCESS, 'Pipeline saved successfully') + ] + }), catchError(error => [deactivateLoader(action.type), requestFailed(error), setServerError(error, undefined, error?.error?.meta?.result_subcode === 800 ? 'Name should be 3 characters long' : error?.error?.meta?.result_subcode === 801 ? 'Name' + @@ -870,38 +894,7 @@ export class PipelinesEffects { // ) // )); - // deleteReport = createEffect(() => this.actions.pipe( - // ofType(deleteReport), - // switchMap(action => this.matDialog.open( - // ConfirmDialogComponent, - // { - // data: { - // title: 'DELETE', - // body: '

Permanently Delete Report

', - // yes: 'Delete', - // no: 'Cancel', - // iconClass: 'al-ico-trash', - // width: 430 - // } - // }).afterClosed().pipe( - // filter(confirm => !!confirm), - // switchMap(() => this.pipelinesApiService.reportsDelete({task: action.report.id})), - // mergeMap(() => { - // this.router.navigate(['reports'], {queryParamsHandling: 'merge'}); - // return [ - // removeReport({id: action.report.id}), - // getReports(), - // deactivateLoader(action.type), - // addMessage(MESSAGES_SEVERITY.SUCCESS, 'Report deleted successfully') - // ]; - // }), - // catchError(error => [ - // requestFailed(error), - // deactivateLoader(action.type), - // setServerError(error, null, 'Failed To delete reports') - // ]) - // )), - // )); + // deleteResource = createEffect(() => this.actions.pipe( // ofType(deleteResource), diff --git a/src/app/webapp-common/pipelines/pipelines.reducer.ts b/src/app/webapp-common/pipelines/pipelines.reducer.ts index f59585e7..31972b1b 100644 --- a/src/app/webapp-common/pipelines/pipelines.reducer.ts +++ b/src/app/webapp-common/pipelines/pipelines.reducer.ts @@ -84,65 +84,96 @@ const getCorrectSortingOrder = (currentSortOrder: TableSortOrderEnum, currentOrd export const pipelinesReducers = [ on(addToPipelinesList, (state, action) => ({ ...state, - pipelines: action.reset ? action.pipelines : [...(state.pipelines || []), ...action.pipelines] + pipelines: action.reset + ? action.pipelines + : [...(state.pipelines || []), ...action.pipelines], + })), + on(setCurrentScrollId, (state, action) => ({ + ...state, + scrollId: action.scrollId, + })), + on(setNoMorePipelines, (state, action) => ({ + ...state, + noMorePipelines: action.payload, })), - on(setCurrentScrollId, (state, action) => ({...state, scrollId: action.scrollId})), - on(setNoMorePipelines, (state, action) => ({...state, noMorePipelines: action.payload})), on(updatePipelineSuccess, (state, action) => ({ - ...state, pipelines: state.pipelines?.map(pr => pr.id === action.changes.id ? { - ...pr, - ...action.changes, - ...(!!action.changes?.name && {basename: action.changes?.name.split('/').at(-1)}) - } : pr) + ...state, + pipelines: state.pipelines?.map((pr) => + pr.id === action.changes.id + ? { + ...pr, + ...action.changes, + ...(!!action.changes?.name && { + basename: action.changes?.name.split("/").at(-1), + }), + } + : pr + ), + selectedPipeline: + state.selectedPipeline.id === action.changes.id + ? { ...state.selectedPipeline, ...action.changes } + : state.selectedPipeline, })), - on(setSelectedPipeline, (state, action) => ({...state, selectedPipeline: {...action.data}})), - on(resetPipelines, state => ({ + on(setSelectedPipeline, (state, action) => ({ + ...state, + selectedPipeline: { ...action.data }, + })), + on(resetPipelines, (state) => ({ ...state, scrollId: null, noMorePipelines: pipelinesInitState.noMorePipelines, - pipelines: pipelinesInitState.pipelines + pipelines: pipelinesInitState.pipelines, })), on(setPipelinesOrderBy, (state, action) => ({ ...state, orderBy: action.orderBy, - sortOrder: getCorrectSortingOrder(state.sortOrder, state.orderBy, action.orderBy), + sortOrder: getCorrectSortingOrder( + state.sortOrder, + state.orderBy, + action.orderBy + ), scrollId: null, noMorePipelines: pipelinesInitState.noMorePipelines, - pipelines: pipelinesInitState.pipelines + pipelines: pipelinesInitState.pipelines, })), on(setPipelinesSearchQuery, (state, action) => ({ ...state, - searchQuery: (action as ReturnType), + searchQuery: action as ReturnType, scrollId: null, noMorePipelines: pipelinesInitState.noMorePipelines, - pipelines: pipelinesInitState.pipelines + pipelines: pipelinesInitState.pipelines, })), - on(resetPipelinesSearchQuery, state => ({ + on(resetPipelinesSearchQuery, (state) => ({ ...state, // searchQuery: pipelinesInitState.searchQuery, scrollId: null, noMorePipelines: pipelinesInitState.noMorePipelines, - pipelines: pipelinesInitState.pipelines + pipelines: pipelinesInitState.pipelines, })), on(checkPipelineForDeletion, (state, action) => ({ ...state, validatedProject: action.pipeline, - projectReadyForDeletion: pipelinesInitState.projectReadyForDeletion + projectReadyForDeletion: pipelinesInitState.projectReadyForDeletion, })), - on(resetReadyToDelete, state => ({ + on(resetReadyToDelete, (state) => ({ ...state, projectReadyForDeletion: pipelinesInitState.projectReadyForDeletion, - validatedProject: pipelinesInitState.validatedProject + validatedProject: pipelinesInitState.validatedProject, })), - on(setTableModeAwareness, (state, action) => - ({...state, tableModeAwareness: (action as ReturnType).awareness})), - on(showExamplePipelines, state => ({...state, showPipelineExamples: true})), - on(showExampleDatasets, state => ({...state, showDatasetExamples: true})), + on(setTableModeAwareness, (state, action) => ({ + ...state, + tableModeAwareness: (action as ReturnType) + .awareness, + })), + on(showExamplePipelines, (state) => ({ + ...state, + showPipelineExamples: true, + })), + on(showExampleDatasets, (state) => ({ ...state, showDatasetExamples: true })), on(setExperimentsResults, (state, action) => ({ ...state, experiments: [...action.experiments], })), - ] as ReducerTypes[]; export const pipelinesReducer = createReducer(pipelinesInitState, ...pipelinesReducers);