import {Injectable} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {Actions, concatLatestFrom, createEffect, ofType} from '@ngrx/effects'; import {Action, Store} from '@ngrx/store'; import {cloneDeep, flatten, isEqual} from 'lodash-es'; import {EMPTY, iif, interval, Observable, of} from 'rxjs'; import { auditTime, catchError, debounce, debounceTime, expand, filter, map, mergeMap, reduce, switchMap, tap, } from 'rxjs/operators'; import {ApiProjectsService} from '~/business-logic/api-services/projects.service'; import {ApiTasksService} from '~/business-logic/api-services/tasks.service'; import {GET_ALL_QUERY_ANY_FIELDS, INITIAL_EXPERIMENT_TABLE_COLS} from '~/features/experiments/experiments.consts'; import {selectSelectedExperiment} from '~/features/experiments/reducers'; import {excludeTypes, EXPERIMENTS_TABLE_COL_FIELDS} from '~/features/experiments/shared/experiments.const'; import {requestFailed} from '../../core/actions/http.actions'; import {activeLoader, addMessage, deactivateLoader, setServerError} from '../../core/actions/layout.actions'; import {setURLParams} from '../../core/actions/router.actions'; import { selectIsArchivedMode, selectIsDeepMode, selectRouterProjectId, selectSelectedProjectId, selectShowHidden } from '../../core/reducers/projects.reducer'; import {selectRouterConfig, selectRouterParams} from '../../core/reducers/router-reducer'; import {FilterMetadata} from 'primeng/api/filtermetadata'; import {ISmCol} from '../../shared/ui-components/data/table/table.consts'; import {addMultipleSortColumns, getRouteFullUrl} from '../../shared/utils/shared-utils'; import { createFiltersFromStore, decodeHyperParam, encodeColumns, encodeOrder, excludedKey, getTagsFilters, TableFilter } from '../../shared/utils/tableParamEncode'; import {autoRefreshExperimentInfo, getExperimentInfo} from '../actions/common-experiments-info.actions'; import * as exActions from '../actions/common-experiments-view.actions'; import {getSelectedExperiments, setActiveParentsFilter, setParents, setSelectedExperiments, updateManyExperiment} from '../actions/common-experiments-view.actions'; import * as exSelectors from '../reducers/index'; import {selectHyperParamsFiltersPage, selectSelectedExperiments, selectTableRefreshList} from '../reducers/index'; import {ITableExperiment} from '../shared/common-experiment-model.model'; import {EXPERIMENTS_PAGE_SIZE} from '../shared/common-experiments.const'; import {convertStopToComplete} from '../shared/common-experiments.utils'; import {sortByField} from '../../tasks/tasks.utils'; import {MODEL_TAGS} from '../../models/shared/models.const'; import {emptyAction} from '~/app.constants'; import {selectExperimentsList, selectTableFilters, selectTableMode} from '../reducers'; import {ProjectsGetTaskParentsResponse} from '~/business-logic/model/projects/projectsGetTaskParentsResponse'; import {ProjectsGetTaskParentsRequest} from '~/business-logic/model/projects/projectsGetTaskParentsRequest'; import {SearchState} from '../../common-search/common-search.reducer'; import {SortMeta} from 'primeng/api'; import {hasValue} from '../../shared/utils/helpers.util'; import {ProjectsGetHyperParametersResponse} from '~/business-logic/model/projects/projectsGetHyperParametersResponse'; import { CountAvailableAndIsDisableSelectedFiltered, MenuItems, selectionDisabledAbort, selectionDisabledAbortAllChildren, selectionDisabledArchive, selectionDisabledContinue, selectionDisabledDelete, selectionDisabledDequeue, selectionDisabledEnqueue, selectionDisabledMoveTo, selectionDisabledPublishExperiments, selectionDisabledQueue, selectionDisabledReset, selectionDisabledTags, selectionDisabledViewWorker } from '../../shared/entity-page/items.utils'; import {MINIMUM_ONLY_FIELDS} from '../experiment.consts'; import {ProjectsGetHyperparamValuesResponse} from '~/business-logic/model/projects/projectsGetHyperparamValuesResponse'; import {TasksGetAllExResponse} from '~/business-logic/model/tasks/tasksGetAllExResponse'; import { selectIsCompare, selectIsDatasets, selectIsPipelines, selectViewArchivedInAddTable } from '../../experiments-compare/reducers'; import { compareAddDialogTableSortChanged, compareAddTableClearAllFilters, compareAddTableFilterChanged, compareAddTableFilterInit, setAddTableViewArchived as setCompareAddTableViewArchived } from '../../experiments-compare/actions/compare-header.actions'; import {TaskTypeEnum} from '~/business-logic/model/tasks/taskTypeEnum'; import {downloadForGetAll, getFilteredUsers, setProjectUsers} from '@common/core/actions/projects.actions'; import {selectActiveWorkspaceReady} from '~/core/reducers/view.reducer'; import {escapeRegex} from '@common/shared/utils/escape-regex'; import {MESSAGES_SEVERITY, rootProjectsPageSize} from '@common/constants'; import {modelExperimentsTableFilterChanged} from '@common/models/actions/models-info.actions'; import {ApiOrganizationService} from '~/business-logic/api-services/organization.service'; import {INITIAL_CONTROLLER_TABLE_COLS} from '@common/pipelines-controller/controllers.consts'; import {prepareColsForDownload} from '@common/shared/utils/download'; import {TasksGetAllExRequest} from '~/business-logic/model/tasks/tasksGetAllExRequest'; import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult'; import {ApiEventsService} from '~/business-logic/api-services/events.service'; import {EventTypeEnum} from '~/business-logic/model/events/eventTypeEnum'; import {EventsGetMultiTaskMetricsResponse} from '~/business-logic/model/events/eventsGetMultiTaskMetricsResponse'; import {TasksCreateResponse} from '~/business-logic/model/tasks/tasksCreateResponse'; import {ErrorService} from '@common/shared/services/error.service'; import * as menuActions from '@common/experiments/actions/common-experiments-menu.actions'; @Injectable() export class CommonExperimentsViewEffects { /* eslint-disable @typescript-eslint/naming-convention */ constructor( private actions$: Actions, private store: Store, private apiTasks: ApiTasksService, private projectsApi: ApiProjectsService, private eventsApi: ApiEventsService, private router: Router, private route: ActivatedRoute, private orgApi: ApiOrganizationService, private errService: ErrorService ) { } activeLoader = createEffect(() => this.actions$.pipe( ofType( exActions.getNextExperiments, exActions.getExperiments, exActions.globalFilterChanged, compareAddDialogTableSortChanged, compareAddTableFilterChanged, setCompareAddTableViewArchived, compareAddTableClearAllFilters, exActions.selectAllExperiments, exActions.getCustomMetrics ), filter((action) => !(action as ReturnType).hideLoader), map(action => activeLoader(action.type)) )); tableSortChange = createEffect(() => this.actions$.pipe( ofType(exActions.tableSortChanged), concatLatestFrom(() => this.store.select(exSelectors.selectTableSortFields)), switchMap(([action, oldOrders]) => { const orders = addMultipleSortColumns(oldOrders, action.colId, action.isShift); return [setURLParams({orders, update: true})]; }) )); tableFilterChange = createEffect(() => this.actions$.pipe( ofType(exActions.tableFilterChanged), concatLatestFrom(() => this.store.select(exSelectors.selectTableFilters)), switchMap(([action, oldFilters]) => [setURLParams({ filters: { ...oldFilters, ...action.filters.reduce((acc, updatedFilter) => { acc[updatedFilter.col] = {value: updatedFilter.value, matchMode: updatedFilter.filterMatchMode}; return acc; }, {} as { [col: string]: FilterMetadata }) }, update: true })] ) )); private refreshActions = []; prepareTableForDownload = createEffect(() => this.actions$.pipe( ofType(exActions.prepareTableForDownload), // eslint-disable-next-line @typescript-eslint/naming-convention concatLatestFrom(() => [ this.store.select(selectRouterParams).pipe(map(params => params?.projectId)), this.store.select(selectIsArchivedMode), this.store.select(exSelectors.selectGlobalFilter), this.store.select(exSelectors.selectTableSortFields), this.store.select(exSelectors.selectTableFilters), this.store.select(exSelectors.selectSelectedExperiments), this.store.select(exSelectors.selectShowAllSelectedIsActive), this.store.select(exSelectors.selectExperimentsTableCols), this.store.select(exSelectors.selectExperimentsMetricsColsForProject), this.store.select(selectIsDeepMode), this.store.select(selectShowHidden), this.store.select(selectIsCompare), this.store.select(selectViewArchivedInAddTable), this.store.select(selectIsPipelines), this.store.select(selectIsDatasets), ]), switchMap(([ , projectId, isArchived, gb, orderFields, filters, selectedExperiments, showAllSelectedIsActive, cols, metricCols, deep, showHidden, isCompare, showArcived, isPipeline, isDataset ]) => { const tableFilters = cloneDeep(filters) || {} as { [key: string]: FilterMetadata }; if (tableFilters && tableFilters.status && tableFilters.status.value.includes('completed')) { tableFilters.status.value.push('closed'); } const selectedIds = showAllSelectedIsActive ? selectedExperiments.map(exp => exp.id) : []; return this.orgApi.organizationPrepareDownloadForGetAll({ entity_type: 'task', only_fields: [], field_mappings: prepareColsForDownload(cols.concat(metricCols)), ...this.getGetAllQuery({ projectId, searchQuery: gb, archived: isArchived, orderFields, tableFilters, selectedIds, cols, metricCols, deep, showHidden, isCompare, showArchived: isCompare && showArcived, isPipeline, isDataset }) }) .pipe( map((res) => downloadForGetAll({prepareId: res.prepare_id})), catchError(error => [requestFailed(error)]) ); } ) ) ); reFetchExperiment = createEffect(() => this.actions$.pipe( ofType( exActions.getExperiments, exActions.getExperimentsWithPageSize, exActions.globalFilterChanged, exActions.setTableSort, exActions.tableFilterChanged, exActions.setTableFilters, exActions.showOnlySelected, compareAddDialogTableSortChanged, compareAddTableFilterChanged, compareAddTableFilterInit, compareAddTableClearAllFilters, setCompareAddTableViewArchived, modelExperimentsTableFilterChanged ), tap(action => this.refreshActions.push(action.type)), switchMap((action) => this.store.select(selectActiveWorkspaceReady).pipe( filter(ready => ready), map(() => action))), auditTime(50), switchMap((action) => this.fetchExperiments$(null, true, action.type === modelExperimentsTableFilterChanged.type, (action as ReturnType).pageSize as number) .pipe( mergeMap(res => { res.tasks = convertStopToComplete(res.tasks); const deactivate = this.refreshActions.map(a => deactivateLoader(a)); this.refreshActions = []; return [ exActions.setNoMoreExperiments({hasMore: (res.tasks.length < EXPERIMENTS_PAGE_SIZE)}), exActions.setExperiments({experiments: res.tasks as ITableExperiment[]}), exActions.setCurrentScrollId({scrollId: res.scroll_id}), ...deactivate ]; }), catchError(error => [ requestFailed(error), deactivateLoader(action.type), addMessage('warn', 'Fetch Experiments failed', error?.meta && [{ name: 'More info', actions: [setServerError(error, null, 'Fetch Experiments failed')] }]) ]) ) ) )); lockRefresh = false; refreshExperiments = createEffect(() => this.actions$.pipe( ofType(exActions.refreshExperiments), filter(() => !this.lockRefresh), concatLatestFrom(() => [ this.store.select(exSelectors.selectCurrentScrollId), this.store.select(selectSelectedExperiment), this.store.select(selectExperimentsList), this.store.select(selectTableRefreshList) ]), switchMap(([action, currentScrollId, selectedExperiment, experiments, refreshPending]) => { this.lockRefresh = !action.autoRefresh; return this.fetchExperiments$(currentScrollId, true) .pipe( mergeMap(res => { res.tasks = convertStopToComplete(res.tasks); this.lockRefresh = false; const actions: Action[] = [deactivateLoader(action.type)]; if (refreshPending) { return [ exActions.getExperimentsWithPageSize({pageSize: experiments.length}), exActions.setTableRefreshPending({refresh: false}), deactivateLoader(action.type) ]; } if (res.scroll_id !== currentScrollId && currentScrollId) { actions.push( exActions.setCurrentScrollId({scrollId: res.scroll_id}), exActions.setNoMoreExperiments({hasMore: (res.tasks.length < EXPERIMENTS_PAGE_SIZE)})); if (!action.autoRefresh) { actions.push( addMessage(MESSAGES_SEVERITY.WARN, 'Session expired'), exActions.setTableRefreshPending({refresh: true})); } } if (action.autoRefresh || res.scroll_id === currentScrollId) { if (selectedExperiment && isEqual(experiments.map(exp => exp.id).sort(), res.tasks.map(exp => exp.id).sort())) { actions.push(exActions.setExperimentInPlace({experiments: res.tasks as ITableExperiment[]})); } else { // SetExperiments must be before GetExperimentInfo! actions.push(exActions.setExperiments({ experiments: res.tasks as ITableExperiment[], noPreferences: true })); } } if (selectedExperiment) { if (action.autoRefresh) { actions.push(autoRefreshExperimentInfo({id: selectedExperiment.id})); } else { // SetExperiments must be before GetExperimentInfo! actions.push(getExperimentInfo({id: selectedExperiment.id})); } } return actions; }), catchError(error => { this.lockRefresh = false; return [ requestFailed(error), deactivateLoader(action.type), ...(action.autoRefresh ? [] : [addMessage('warn', 'Fetch Experiments failed', error?.meta && [{ name: 'More info', actions: [setServerError(error, null, 'Fetch Experiments failed')] }])]) ]; }) ); } ) )); getNextExperiments = createEffect(() => this.actions$.pipe( ofType(exActions.getNextExperiments), concatLatestFrom(() => [ this.store.select(exSelectors.selectCurrentScrollId), this.store.select(selectExperimentsList), this.store.select(selectTableRefreshList) ]), switchMap(([action, scrollId, tasks, refreshPending]) => this.fetchExperiments$(scrollId, false, action.allProjects) .pipe( mergeMap(res => { if (refreshPending) { return [ exActions.getExperimentsWithPageSize({pageSize: tasks.length}), exActions.setTableRefreshPending({refresh: false}), deactivateLoader(action.type) ]; } res.tasks = convertStopToComplete(res.tasks); const addTasksAction = scrollId === res.scroll_id || !scrollId ? [exActions.addExperiments({experiments: res.tasks as ITableExperiment[]})] : [exActions.setTableRefreshPending({refresh: true}), addMessage(MESSAGES_SEVERITY.WARN, 'Session expired')]; return [ exActions.setNoMoreExperiments({hasMore: (res.tasks.length < EXPERIMENTS_PAGE_SIZE)}), ...addTasksAction, exActions.setCurrentScrollId({scrollId: res.scroll_id}), deactivateLoader(action.type), ]; }), catchError(error => [ requestFailed(error), deactivateLoader(action.type), addMessage('warn', 'Fetch Experiments failed', error?.meta && [{ name: 'More info', actions: [setServerError(error, null, 'Fetch Experiments failed')] }]) ]) ) ) )); experimentSelectionChanged = createEffect(() => this.actions$.pipe( ofType(exActions.experimentSelectionChanged), concatLatestFrom(() => this.store.select(selectRouterConfig)), tap(([action, routeConfig]) => this.navigateAfterExperimentSelectionChanged(action.experiment as ITableExperiment, action.project, routeConfig, action.replaceURL)), mergeMap(() => [exActions.setTableMode({mode: 'info'})]) )); selectNextExperimentEffect = createEffect(() => this.actions$.pipe( ofType(exActions.selectNextExperiment), concatLatestFrom(() => [ this.store.select(selectRouterConfig), this.store.select(selectExperimentsList), this.store.select(selectRouterParams).pipe(map(params => params?.projectId)), this.store.select(selectTableMode) ]), filter(([, , , , tableMode]) => tableMode !== 'table'), tap(([, routeConfig, tasks, projectId]) => this.navigateAfterExperimentSelectionChanged(tasks[0] as ITableExperiment, projectId, routeConfig, true)), mergeMap(([, , , , tableMode]) => [exActions.setTableMode({mode: tableMode}), exActions.setSelectedExperiments({experiments: []})]) )); getTypesEffect = createEffect(() => this.actions$.pipe( ofType(exActions.getProjectTypes), concatLatestFrom(() => this.store.select(selectRouterParams).pipe(map(params => params?.projectId)) ), switchMap(([action, projectId]) => this.apiTasks.tasksGetTypes({projects: (action.allProjects || projectId === '*' ? [] : [projectId])}).pipe( concatLatestFrom(() => this.store.select(exSelectors.selectTableFilters)), mergeMap(([res, tableFilters]) => { let shouldFilterFilters: boolean; let filteredTableFilter = {} as TableFilter; if (tableFilters?.type?.value) { filteredTableFilter = { col: 'type', value: tableFilters.type.value.filter(filterType => res.types.includes(filterType)) }; shouldFilterFilters = filteredTableFilter.value.length !== tableFilters.type.value.length; } return [ shouldFilterFilters ? exActions.tableFilterChanged({ filters: [filteredTableFilter], projectId }) : emptyAction(), exActions.setProjectsTypes(res), deactivateLoader(action.type) ]; }), catchError(error => [ requestFailed(error), deactivateLoader(action.type), addMessage('warn', 'Fetch types failed', error?.meta && [{ name: 'More info', actions: [setServerError(error, null, 'Fetch types failed')] }])] ) )))); getUsersEffect = createEffect(() => this.actions$.pipe( ofType(setProjectUsers), concatLatestFrom(() => this.store.select(selectTableFilters)), map(([action, filters]) => { const userFiltersValue = filters?.[EXPERIMENTS_TABLE_COL_FIELDS.USER]?.value ?? []; const resIds = action.users.map(user => user.id); const shouldGetFilteredUsersNames = !(userFiltersValue.every(id => resIds.includes(id))); return shouldGetFilteredUsersNames ? getFilteredUsers({filteredUsers: userFiltersValue}) : emptyAction(); }), catchError(error => [ requestFailed(error), addMessage('warn', 'Fetch users failed', error?.meta && [{ name: 'More info', actions: [setServerError(error, null, 'Fetch users failed')] }])] ) )); getParentsEffect = createEffect(() => this.actions$.pipe( ofType(exActions.getParents), debounceTime(300), concatLatestFrom(() => [ this.store.select(selectRouterParams).pipe(map(params => params?.projectId)), this.store.select(selectIsArchivedMode) ]), switchMap(([action, projectId, isArchive]) => this.projectsApi.projectsGetTaskParents({ projects: (projectId === '*' || action.allProjects) ? [] : [projectId], tasks_state: isArchive ? ProjectsGetTaskParentsRequest.TasksStateEnum.Archived : ProjectsGetTaskParentsRequest.TasksStateEnum.Active, ...(action.searchValue && {task_name: `(?i)${action.searchValue}`}) // (?i) is case insensitive }).pipe( concatLatestFrom(() => this.store.select(selectTableFilters).pipe(map(filters => filters?.['parent.name']?.value || []))), mergeMap(([{parents}, filteredParentIds]: [ProjectsGetTaskParentsResponse, string[]]) => { const missingParentsIds = filteredParentIds.filter(parentId => !parents.find(parent => parent.id === parentId)); return (missingParentsIds.length ? this.apiTasks.tasksGetAllEx({ id: missingParentsIds, only_fields: ['name', 'project.name'] }) : of({tasks: []})).pipe( mergeMap((parentsTasks) => [ setActiveParentsFilter({parents: parentsTasks.tasks || []}), setParents({parents: parents})]) ); }), catchError(error => [ requestFailed(error), addMessage('warn', 'Fetch parents failed', error?.meta && [{ name: 'More info', actions: [setServerError(error, null, 'Fetch parents failed')] }]) ] ) )) )); getCustomMetrics = createEffect(() => this.actions$.pipe( ofType(exActions.getCustomMetrics), concatLatestFrom(() => [ this.store.select(selectRouterParams).pipe(map(params => params?.projectId)), this.store.select(selectIsDeepMode) ]), switchMap(([action, projectId, isDeep]) => this.projectsApi.projectsGetUniqueMetricVariants({ project: projectId === '*' ? null : projectId, include_subprojects: isDeep }) .pipe( mergeMap(res => [ exActions.setCustomMetrics({metrics: sortByField(res.metrics, 'metric'), projectId, compareView: EventTypeEnum.TrainingStatsScalar}), deactivateLoader(action.type) ]), catchError(error => [ requestFailed(error), deactivateLoader(action.type), addMessage('warn', 'Fetch custom metrics failed', error?.meta && [{ name: 'More info', actions: [setServerError(error, null, 'Fetch custom metrics failed')] }]), exActions.setCustomHyperParams({params: []})]) ) ) )); getCustomMetricsPerType = createEffect(() => this.actions$.pipe( ofType(exActions.getCustomMetricsPerType), concatLatestFrom(() => [ this.store.select(selectRouterParams).pipe(map(params => params?.projectId)), ]), switchMap(([action, projectId]) => { if (action.ids.length > 0) { return this.eventsApi.eventsGetMultiTaskMetrics({ tasks: action.ids, ...(action.metricsType && {event_type: action.metricsType}), model_events: action.isModel }) .pipe( map((res: EventsGetMultiTaskMetricsResponse) => [ // {metric: singleValueChartTitle, variant: '', metric_hash: singleValueChartTitle, variant_hash: singleValueChartTitle}, ...res.metrics.map(metric => metric.variants.map(variant => ( {metric: metric.metric.replace(/^Summary$/, ' Summary'), metric_hash: metric.metric, variant: variant, variant_hash: variant}))).flat(1) ] as MetricVariantResult[]), mergeMap(metrics => [ exActions.setCustomMetrics({metrics: sortByField(metrics, 'metric'), projectId, compareView: action.metricsType}), deactivateLoader(action.type) ]), catchError(error => [ requestFailed(error), deactivateLoader(action.type), addMessage('warn', 'Fetch custom metrics failed', error?.meta && [{ name: 'More info', actions: [setServerError(error, null, 'Fetch custom metrics failed')] }]), exActions.setCustomHyperParams({params: []})]) ); } else { return of(action) .pipe(map((action) => exActions.setCustomMetrics({metrics: [], projectId, compareView: action.metricsType}))); } } ))); getCustomHyperParams = createEffect(() => this.actions$.pipe( ofType(exActions.getCustomHyperParams), concatLatestFrom(() => [ this.store.select(selectRouterParams).pipe(map(params => params?.projectId)), this.store.select(selectIsDeepMode) ]), switchMap(([action, projectId, isDeep]) => this.projectsApi.projectsGetHyperParameters({ project: projectId === '*' ? null : projectId, page_size: 5000, include_subprojects: isDeep }) .pipe( mergeMap((res: ProjectsGetHyperParametersResponse) => [ exActions.setCustomHyperParams({params: res.parameters}), deactivateLoader(action.type) ]), catchError(error => [ requestFailed(error), deactivateLoader(action.type), addMessage('warn', 'Fetch hyperparameters failed', error?.meta && [{ name: 'More info', actions: [setServerError(error, null, 'Fetch hyperparameters failed')] }]), exActions.setCustomHyperParams({params: []})]) ) ) )); hyperParameterMenuClicked = createEffect(() => this.actions$.pipe( ofType(exActions.hyperParamSelectedExperiments), debounce((action) => interval(action.searchValue ? 300 : 0)), concatLatestFrom(() => [ this.store.select(selectIsDeepMode), this.store.select(selectRouterProjectId), this.store.select(selectIsCompare), this.store.select(selectHyperParamsFiltersPage), ]), switchMap(([action, isDeep, selectedProjectId, isCompare, page]) => { const projectId = action.col.projectId || selectedProjectId; const {section, name} = decodeHyperParam(action.col); return this.projectsApi.projectsGetHyperparamValues({ include_subprojects: isDeep, section, name, ...((!isCompare && projectId !== '*') && {projects: [projectId]}), page, page_size: rootProjectsPageSize, pattern: escapeRegex(action.searchValue), }).pipe( map((data: ProjectsGetHyperparamValuesResponse) => { const values = data.values.filter(x => hasValue(x) && x !== ''); return exActions.hyperParamSelectedInfoExperiments({col: action.col, values, loadMore: page > 0}); }), ); }) )); selectAll = createEffect(() => this.actions$.pipe( ofType(exActions.selectAllExperiments), concatLatestFrom(() => [ this.store.select(selectRouterParams).pipe(map(params => params?.projectId)), this.store.select(selectIsArchivedMode), this.store.select(exSelectors.selectGlobalFilter), this.store.select(exSelectors.selectExperimentsTableCols), this.store.select(exSelectors.selectExperimentsMetricsColsForProject), this.store.select(exSelectors.selectTableFilters), this.store.select(selectIsDeepMode), this.store.select(selectShowHidden), this.store.select(selectIsPipelines), this.store.select(selectIsDatasets), ]), switchMap(([ action, projectId, archived, globalSearch, cols, metricCols, tableFilters, deep, showHidden, isPipeline, isDataset ]) => { const pageSize = 5000; const query = this.getGetAllQuery({ projectId, searchQuery: globalSearch, archived, cols, metricCols, tableFilters: action.filtered ? tableFilters : {}, orderFields: [{order: -1, field: EXPERIMENTS_TABLE_COL_FIELDS.LAST_UPDATE}], deep, showHidden, pageSize, isPipeline, isDataset }); query.only_fields = [EXPERIMENTS_TABLE_COL_FIELDS.NAME, EXPERIMENTS_TABLE_COL_FIELDS.PARENT, EXPERIMENTS_TABLE_COL_FIELDS.STATUS, EXPERIMENTS_TABLE_COL_FIELDS.TYPE, 'company.id', 'system_tags']; return this.apiTasks.tasksGetAllEx(query).pipe( expand((res: TasksGetAllExResponse) => res.tasks.length === pageSize ? this.apiTasks.tasksGetAllEx({ ...query, scroll_id: res.scroll_id }) : EMPTY), reduce((acc, res) => acc.concat(res.tasks), []) ); }), switchMap(experiments => [exActions.setSelectedExperiments({experiments}), deactivateLoader(exActions.selectAllExperiments.type)]), catchError(error => [ requestFailed(error), deactivateLoader(exActions.selectAllExperiments.type), setServerError(error, null, 'Fetch experiments for selection failed'), ]) )); getSelectedExperiments = createEffect(() => this.actions$.pipe( ofType(getSelectedExperiments), concatLatestFrom(() => [ this.store.select(selectSelectedExperiments), this.store.select(selectExperimentsList) ]), mergeMap(([action, selectedExperiments, experiments]) => iif(() => selectedExperiments.length > 0, [setSelectedExperiments({experiments: selectedExperiments.slice(0, 100)})], iif(() => { const experimentsFromStore = experiments.filter(entity => action.ids.includes(entity.id)); return experimentsFromStore.length === action.ids.length; }, [setSelectedExperiments({experiments: experiments.filter(entity => action.ids.includes(entity.id)).slice(0, 100)})], this.apiTasks.tasksGetAllEx({id: action.ids}).pipe( map(res => setSelectedExperiments({experiments: res.tasks})) )))) )); setURLParams = createEffect(() => this.actions$.pipe( ofType(exActions.updateUrlParams, exActions.toggleColHidden), concatLatestFrom(() => [ this.store.select(selectRouterParams).pipe(map(params => params?.projectId)), this.store.select(selectIsArchivedMode), this.store.select(exSelectors.selectGlobalFilter), this.store.select(exSelectors.selectTableSortFields), this.store.select(exSelectors.selectTableFilters), this.store.select(exSelectors.selectExperimentsTableCols), this.store.select(exSelectors.selectExperimentsHiddenTableCols), this.store.select(exSelectors.selectExperimentsMetricsCols), this.store.select(exSelectors.selectExperimentsTableColsOrder), this.store.select(selectIsDeepMode), this.route.queryParams ]), map(([, projectId, isArchived, , sortFields, filters, cols, hiddenCols, metricsCols, colsOrder, isDeep, queryParams ]) => { const columns = encodeColumns(cols, hiddenCols, this.filterColumns(projectId, metricsCols), colsOrder ? colsOrder : queryParams.columns); return setURLParams({ columns, filters, orders: sortFields, isArchived, isDeep, update: true }); }) )); navigateAfterExperimentSelectionChanged(selectedExperiment: ITableExperiment, experimentProject: string, routeConfig: string[], replaceUrl = false) { const module = routeConfig.includes('datasets') ? 'datasets/simple' : routeConfig.includes('pipelines') ? 'pipelines' : 'projects'; // wow angular really suck... const activeChild = this.route?.firstChild?.firstChild?.firstChild?.firstChild?.firstChild?.firstChild; const activeChildUrl = activeChild ? getRouteFullUrl(activeChild) : ''; selectedExperiment ? this.router.navigate( [module, experimentProject, 'experiments', selectedExperiment.id].concat(activeChild ? activeChildUrl.split('/') : []), {queryParamsHandling: 'preserve'} ) : this.router.navigate([module, experimentProject, 'experiments'], { queryParamsHandling: 'preserve', replaceUrl }); } getGetAllQuery({ refreshScroll = false, scrollId = null, projectId, searchQuery, archived, orderFields = [], tableFilters, selectedIds = [], cols = [], metricCols = [], deep = false, showHidden = false, isCompare, showArchived = false, isPipeline = false, isDataset = false, pageSize = EXPERIMENTS_PAGE_SIZE }: { refreshScroll?: boolean; scrollId?: string; projectId: string; searchQuery?: SearchState['searchQuery']; archived: boolean; orderFields?: SortMeta[]; tableFilters: { [columnId: string]: FilterMetadata }; selectedIds?: string[]; cols?: ISmCol[]; metricCols?: ISmCol[]; deep?: boolean; showHidden?: boolean; isCompare?: boolean; showArchived?: boolean; isPipeline?: boolean; isDataset?: boolean; pageSize?: number; }): TasksGetAllExRequest { const projectFilter = tableFilters?.[EXPERIMENTS_TABLE_COL_FIELDS.PROJECT]?.value; const typeFilter = tableFilters?.[EXPERIMENTS_TABLE_COL_FIELDS.TYPE]?.value; const statusFilter = tableFilters?.[EXPERIMENTS_TABLE_COL_FIELDS.STATUS]?.value; const userFilter = tableFilters?.[EXPERIMENTS_TABLE_COL_FIELDS.USER]?.value; const tagsFilter = tableFilters?.[EXPERIMENTS_TABLE_COL_FIELDS.TAGS]?.value; const tagsFilterAnd = tableFilters?.[EXPERIMENTS_TABLE_COL_FIELDS.TAGS]?.matchMode === 'AND'; const parentFilter = tableFilters?.[EXPERIMENTS_TABLE_COL_FIELDS.PARENT]?.value; const systemTags = tableFilters?.system_tags?.value as string[]; let systemTagsFilter = (showArchived ? [] : archived ? ['__$and', MODEL_TAGS.HIDDEN] : [excludedKey, MODEL_TAGS.HIDDEN]); if (isDataset) { systemTagsFilter.push('dataset'); } else if (!isPipeline && !showHidden) { systemTagsFilter = systemTagsFilter.concat([excludedKey, 'pipeline', excludedKey, 'dataset', excludedKey, 'reports']); } if (systemTags) { systemTagsFilter = systemTagsFilter.concat(systemTags); } let filters = createFiltersFromStore(tableFilters, true); const tableCols = cols.length > 0 ? cols : isDataset ? INITIAL_CONTROLLER_TABLE_COLS : INITIAL_EXPERIMENT_TABLE_COLS; filters = Object.keys(filters).reduce((acc, colId) => { const col = [...metricCols, ...tableCols].find(c => c.id === colId); let key = col?.getter || colId; if (col?.isParam && typeof col.getter === 'string') { key = col.getter; } if (Array.isArray(key)) { key = key[0]; } acc[key] = filters[colId]; return acc; }, {}); orderFields = orderFields.map(field => { let getter; const col = metricCols.find(c => c.id === field.field); if (col?.isParam && typeof col.getter === 'string') { getter = col.getter; } else { getter = Array.isArray(col?.getter) ? col.getter[0] : col?.getter; } return getter ? {...field, field: getter} : field; }); const colsFilters = flatten(tableCols.filter(col => col.id !== 'selected' && !col.hidden).map(col => col.getter || col.id)); const metricColsFilters = metricCols ? flatten(metricCols.map(col => col.getter || col.id)) : []; const only_fields = [...new Set([...MINIMUM_ONLY_FIELDS, ...colsFilters, ...metricColsFilters] .concat(isPipeline || isDataset ? ['parent.name', 'runtime._pipeline_hash', 'runtime.version', 'execution.queue', 'type', 'hyperparams.properties.version'] : []))]; delete filters['tags']; return { ...filters, id: selectedIds, ...(searchQuery?.query && { _any_: { pattern: searchQuery.regExp ? searchQuery.query : escapeRegex(searchQuery.query), fields: GET_ALL_QUERY_ANY_FIELDS } }), project: ((!filters['project.name'] && (!projectId || projectId === '*'))) ? undefined : isCompare ? ((filters['project.name'] || undefined)) : (filters['project.name'] || [projectId]), scroll_id: scrollId || null, // null to create new scroll (undefined doesn't generate scroll) refresh_scroll: refreshScroll, size: pageSize, order_by: encodeOrder(orderFields), status: (statusFilter && statusFilter.length > 0) ? statusFilter : undefined, type: isPipeline ? [TaskTypeEnum.Controller] : isDataset ? [TaskTypeEnum.DataProcessing] : (typeFilter?.length > 0) ? typeFilter : excludeTypes, user: (userFilter?.length > 0) ? userFilter : [], ...(parentFilter?.length > 0 && {parent: parentFilter}), ...(systemTagsFilter?.length > 0 && {system_tags: systemTagsFilter}), ...(tagsFilter?.length > 0 && { filters: { tags: getTagsFilters(tagsFilterAnd, tagsFilter), } }), include_subprojects: deep && !projectFilter, search_hidden: showHidden, only_fields }; } fetchExperiments$(scrollId1: string, refreshScroll = false, allProjects = false, pageSize = EXPERIMENTS_PAGE_SIZE): Observable { return of(scrollId1) .pipe( concatLatestFrom(() => [ this.store.select(selectRouterProjectId), this.store.select(selectIsArchivedMode), this.store.select(exSelectors.selectGlobalFilter), this.store.select(exSelectors.selectTableSortFields), this.store.select(exSelectors.selectTableFilters), this.store.select(exSelectors.selectSelectedExperiments), this.store.select(exSelectors.selectShowAllSelectedIsActive), this.store.select(exSelectors.selectExperimentsTableCols), this.store.select(exSelectors.selectExperimentsMetricsColsForProject), this.store.select(selectIsDeepMode), this.store.select(selectShowHidden), this.store.select(selectIsCompare), this.store.select(selectViewArchivedInAddTable), this.store.select(selectIsPipelines), this.store.select(selectIsDatasets), ]), switchMap(([ scrollId, projectId, isArchived, gb, orderFields, filters, selectedExperiments, showAllSelectedIsActive, cols, metricCols, deep, showHidden, isCompare, showArcived, isPipeline, isDataset ]) => { const tableFilters = cloneDeep(filters) || {} as { [key: string]: FilterMetadata }; if (tableFilters && tableFilters.status && tableFilters.status.value.includes('completed')) { tableFilters.status.value.push('closed'); } const selectedIds = showAllSelectedIsActive ? selectedExperiments.map(exp => exp.id) : []; return this.apiTasks.tasksGetAllEx(this.getGetAllQuery({ refreshScroll, scrollId, projectId: allProjects ? '*' : projectId, searchQuery: gb, archived: isArchived, orderFields, tableFilters, selectedIds, cols, metricCols, deep, showHidden, isCompare, showArchived: isCompare && showArcived, isPipeline, pageSize, isDataset })); }) ); } private filterColumns(projectId: string, metricsCols: ISmCol[]) { return metricsCols.filter(col => col.projectId === projectId); } getTagsEffect = createEffect(() => this.actions$.pipe( ofType(exActions.getTags), concatLatestFrom(() => this.store.select(selectRouterParams).pipe(map(params => params?.projectId))), switchMap(([action, projectId]) => this.projectsApi.projectsGetTaskTags({ projects: (projectId === '*' || action.allProjects) ? [] : [projectId] }).pipe( mergeMap(res => [ exActions.setTags({tags: res.tags.concat(null)}), deactivateLoader(action.type) ]), catchError(error => [ requestFailed(error), deactivateLoader(action.type), addMessage('warn', 'Fetch tags failed', error?.meta && [{ name: 'More info', actions: [setServerError(error, null, 'Fetch tags failed')] }])] ) )) )); setSelectedExperiments = createEffect(() => this.actions$.pipe( ofType(exActions.setSelectedExperiments, exActions.updateExperiment, updateManyExperiment.type), concatLatestFrom(() => this.store.select(exSelectors.selectSelectedExperiments), ), switchMap(([action, selectSelectedExperiments]) => { const experiments = action.type === exActions.setSelectedExperiments.type ? (action as ReturnType).experiments : selectSelectedExperiments; const selectedExperimentsDisableAvailable: Record = { [MenuItems.abort]: selectionDisabledAbort(experiments), [MenuItems.abortAllChildren]: selectionDisabledAbortAllChildren(experiments), [MenuItems.publish]: selectionDisabledPublishExperiments(experiments), [MenuItems.reset]: selectionDisabledReset(experiments), [MenuItems.delete]: selectionDisabledDelete(experiments), [MenuItems.moveTo]: selectionDisabledMoveTo(experiments), [MenuItems.continue]: selectionDisabledContinue(experiments), [MenuItems.enqueue]: selectionDisabledEnqueue(experiments), [MenuItems.dequeue]: selectionDisabledDequeue(experiments), [MenuItems.queue]: selectionDisabledQueue(experiments), [MenuItems.viewWorker]: selectionDisabledViewWorker(experiments), [MenuItems.archive]: selectionDisabledArchive(experiments), [MenuItems.tags]: selectionDisabledTags(experiments), }; //allHasExamples: selectionAllExamples(action.experiments), // allArchive: selectionAllIsArchive(action.experiments), return [exActions.setSelectedExperimentsDisableAvailable({selectedExperimentsDisableAvailable})]; }) ) ); createExperiment = createEffect(() => { return this.actions$.pipe( ofType(exActions.createExperiment), concatLatestFrom(() => this.store.select(selectSelectedProjectId)), switchMap(([action, projectId]) => this.apiTasks.tasksCreate({ project: projectId, name: action.data.name, type: 'training', script: { repository: action.data.repo, ...(action.data.type === 'branch' ? {branch: action.data.branch ?? 'master'} : action.data.type === 'tag' ? {tag: action.data.tag} : {version_num: action.data.commit} ), working_dir: action.data.directory, entry_point: action.data.script, binary: action.data.binary, requirements: action.data.requirements === 'manual' ? {pip: action.data.pip} : null, }, hyperparams: { Args: action.data.args .filter(arg => arg.key?.length > 0) .reduce((acc, arg) => { const name = arg.key.startsWith('--') ? arg.key.slice(2) : arg.key; acc[name] = {name, value: arg.value, section: 'Args'}; return acc; }, {}) }, ...(action.data.output && {output_dest: action.data.output}), ...((action.data.docker.image || action.data.taskInit) && { container: { image: action.data.docker.image, arguments: `${action.data.docker.args}${action.data.taskInit ? ' -e CLEARML_AGENT_FORCE_TASK_INIT=1' : ''}${action.data.poetry ? ' -e CLEARML_AGENT_FORCE_POETRY' : ''}${action.data.venv ? ' -e CLEARML_AGENT_SKIP_PIP_VENV_INSTALL=' + action.data.venv : ''}${action.data.requirements === 'skip' ? '-e CLEARML_AGENT_SKIP_PYTHON_ENV_INSTALL=1' : ''}`.trimStart(), setup_shell_script: action.data.docker.script } }), }).pipe( map((res: TasksCreateResponse) => exActions.createExperimentSuccess({data: {...action.data, id: res.id}, project: projectId})) )), catchError(error => [addMessage(MESSAGES_SEVERITY.ERROR, `Failed to create experiment.\n${this.errService.getErrorMsg(error.error)}`)]) ); }); createExperimentSuccess = createEffect(() => { return this.actions$.pipe( ofType(exActions.createExperimentSuccess), map(action => addMessage(MESSAGES_SEVERITY.SUCCESS, `Successfully created experiment ${action.data.name}`, [{name: 'open experiment', actions: [exActions.openExperiment({id: action.data.id, project: action.project})]}])) ); }); updateExperimentsAfterCreate = createEffect(() => { return this.actions$.pipe( ofType(exActions.createExperimentSuccess), map(() => exActions.refreshExperiments({autoRefresh: false, hideLoader: false})) ); }); openExperiment = createEffect(() => { return this.actions$.pipe( ofType(exActions.openExperiment), map(action => this.router.navigate(['projects', action.project, 'experiments', action.id]),) ); }, {dispatch: false}); enqueueCreateExperiment = createEffect(() => { return this.actions$.pipe( ofType(exActions.createExperimentSuccess), filter(action => !!action.data.queue), switchMap(action => this.apiTasks.tasksEnqueue({ queue: action.data.queue.id, task: action.data.id, verify_watched_queue: true, }).pipe( map(res => res.queue_watched === false ? menuActions.openEmptyQueueMessage({queue: action.data.queue, entityName: action.data.name}) : {type: 'EMPTY'}), )), catchError(error => [addMessage(MESSAGES_SEVERITY.ERROR, `Failed to enqueue experiment.\n${this.errService.getErrorMsg(error.error)}`)]) ); }); }