Merge pull request #8 from allegroai/v1.1

ClearML v1.1
This commit is contained in:
Jake Henning
2021-07-27 15:04:42 +03:00
committed by GitHub
360 changed files with 25745 additions and 7490 deletions

View File

@@ -27,11 +27,17 @@
"assets": [
"src/assets",
"src/favicon.ico",
"src/app/webapp-common/assets"
"src/app/webapp-common/assets",
{
"glob": "**/*",
"input": "node_modules/ace-builds/src-min",
"output": "./assets/ace-builds/"
}
],
"styles": [
"node_modules/primeicons/primeicons.css",
"node_modules/primeng/resources/components/table/table.css",
"node_modules/ngx-markdown-editor/assets/highlight.js/agate.min.css",
"src/styles.scss",
"src/fonts.scss"
],

22376
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{
"name": "ClearML-webapp",
"version": "0.17.0",
"version": "1.1.0",
"license": "",
"scripts": {
"ng": "ng",
"start": "npx ng serve --proxy-config proxy.config.js --live-reload false --port 4300",
"hmr": "npx ng serve --proxy-config proxy.config.js --hmr --port 4300",
"build": "node --max_old_space_size=3248 ./node_modules/.bin/ng build --prod --source-map --extract-css=false --vendor-chunk --crossOrigin=use-credentials",
"build": "node --max_old_space_size=3248 ./node_modules/.bin/ng build --prod --source-map --vendor-chunk --crossOrigin=use-credentials",
"build-demo": "node --max_old_space_size=3248 ./node_modules/.bin/ng build --configuration demo --source-map --extract-css=false --crossOrigin=use-credentials",
"build-guest": "node --max_old_space_size=3248 ./node_modules/.bin/ng build --prod --configuration guest --source-map --extract-css=false --crossOrigin=use-credentials",
"build-community": "node --max_old_space_size=3248 ./node_modules/.bin/ng build --prod --configuration community --source-map --extract-css=false --crossOrigin=use-credentials",
@@ -20,33 +20,35 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^11.2.7",
"@angular/cdk": "^11.2.6",
"@angular/common": "^11.2.7",
"@angular/compiler": "^11.2.7",
"@angular/core": "^11.2.7",
"@angular/forms": "^11.2.7",
"@angular/material": "^11.2.6",
"@angular/platform-browser": "^11.2.7",
"@angular/platform-browser-dynamic": "^11.2.7",
"@angular/platform-server": "^11.2.7",
"@angular/router": "^11.2.7",
"@angular/service-worker": "^11.2.7",
"@ngrx/effects": "^11.0.1",
"@ngrx/entity": "^11.0.1",
"@ngrx/router-store": "^11.0.1",
"@ngrx/store": "^11.0.1",
"@angular/animations": "^11.2.10",
"@angular/cdk": "^11.2.9",
"@angular/common": "^11.2.10",
"@angular/compiler": "^11.2.10",
"@angular/core": "^11.2.10",
"@angular/forms": "^11.2.10",
"@angular/material": "^11.2.9",
"@angular/platform-browser": "^11.2.10",
"@angular/platform-browser-dynamic": "^11.2.10",
"@angular/platform-server": "^11.2.10",
"@angular/router": "^11.2.10",
"@angular/service-worker": "^11.2.10",
"@ngrx/effects": "^11.1.0",
"@ngrx/entity": "^11.1.0",
"@ngrx/router-store": "^11.1.0",
"@ngrx/store": "^11.1.0",
"@types/d3-selection": "^2.0.0",
"@types/plotly.js": "^1.54.10",
"ace-builds": "^1.4.12",
"amazon-s3-uri": "0.1.0",
"angular-google-tag-manager": "^1.3.0",
"amazon-s3-uri": "0.1.1",
"angular-google-tag-manager": "~1.3.2",
"angular-resizable-element": "^3.3.5",
"angular-split": "^5.0.0",
"ansi-to-html": "^0.6.14",
"aws-sdk": "^2.874.0",
"aws-sdk": "^2.888.0",
"bootstrap": "^4.6.0",
"britecharts": "^2.17.6",
"britecharts": "^2.18.0",
"diff": "^5.0.0",
"has-ansi": "^4.0.1",
"has-ansi": "^5.0.0",
"hocon-parser": "^1.0.1",
"jwt-decode": "^3.1.2",
"localforage": "^1.9.0",
@@ -55,43 +57,43 @@
"ngx-clipboard": "^14.0.1",
"ngx-color-picker": "^11.0.0",
"ngx-filesize": "^2.0.16",
"ngx-markdown-editor": "^3.3.2",
"ngx-markdown-editor": "^3.3.3",
"object-hash": "^2.1.1",
"primeicons": "^4.1.0",
"primeng": "^11.3.1",
"primeng": "^11.3.2",
"process": "^0.11.10",
"rxjs": "^6.6.7",
"string-to-color": "^2.2.2",
"tslib": "^2.1.0",
"tslib": "^2.2.0",
"uuid": "^8.3.2",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1102.6",
"@angular-devkit/core": "^11.2.6",
"@angular-devkit/schematics": "^11.2.6",
"@angular-devkit/schematics-cli": "^0.1102.6",
"@angular-eslint/builder": "^2.0.2",
"@angular-eslint/eslint-plugin": "^2.0.2",
"@angular-eslint/eslint-plugin-template": "^2.0.2",
"@angular-eslint/schematics": "2.0.2",
"@angular-eslint/template-parser": "^2.0.2",
"@angular/cli": "^11.2.6",
"@angular/compiler-cli": "^11.2.7",
"@angular/language-service": "^11.2.7",
"@angular-devkit/build-angular": "~0.1102.9",
"@angular-devkit/core": "^11.2.9",
"@angular-devkit/schematics": "^11.2.9",
"@angular-devkit/schematics-cli": "^0.1102.9",
"@angular-eslint/builder": "^4.0.0",
"@angular-eslint/eslint-plugin": "^4.0.0",
"@angular-eslint/eslint-plugin-template": "^4.0.0",
"@angular-eslint/schematics": "4.0.0",
"@angular-eslint/template-parser": "^4.0.0",
"@angular/cli": "^11.2.9",
"@angular/compiler-cli": "^11.2.10",
"@angular/language-service": "^11.2.10",
"@fortawesome/fontawesome-free": "^5.15.3",
"@ngrx/schematics": "^11.0.1",
"@ngrx/store-devtools": "^11.0.1",
"@schematics/schematics": "^0.1102.6",
"@ngrx/schematics": "^11.1.0",
"@ngrx/store-devtools": "^11.1.0",
"@schematics/schematics": "^0.1102.9",
"@types/d3-selection": "^2.0.0",
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.37",
"@types/plotly.js": "^1.54.10",
"@types/node": "^14.14.41",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "4.19.0",
"@typescript-eslint/parser": "4.19.0",
"@typescript-eslint/eslint-plugin": "4.22.0",
"@typescript-eslint/parser": "4.22.0",
"babel-loader": "^8.2.2",
"codelyzer": "^6.0.1",
"eslint": "^7.23.0",
"eslint": "^7.24.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jsdoc": "32.3.0",
"eslint-plugin-prefer-arrow": "1.2.3",

View File

@@ -13,7 +13,7 @@
<sm-side-nav *ngIf="currentUser"></sm-side-nav>
<div class="app-container" [class.login-page]="!currentUser"
[class.notifier-open]="update?.active || ((showSurvey$ | async) && showSurvey && !demo && !guestUser)">
<sm-header *ngIf="currentUser" [isDashboard]="isDashboardContext" [isLogin]="isLoginContext"
<sm-header *ngIf="currentUser" [isLogin]="isLoginContext"
[isShareMode]="isSharedAndNotOwner$ | async"></sm-header>
<router-outlet class="main-router"></router-outlet>
</div>

View File

@@ -1,45 +1,45 @@
import {ApiUsersService} from './business-logic/api-services/users.service';
import {selectCurrentUser, selectActiveWorkspace} from './webapp-common/core/reducers/users-reducer';
import {selectCurrentUser} from '@common/core/reducers/users-reducer';
import {Component, OnDestroy, OnInit, ViewEncapsulation, HostListener, Renderer2, Injector} from '@angular/core';
import {ActivatedRoute, NavigationEnd, Router, Params, RouterEvent} from '@angular/router';
import {Title} from '@angular/platform-browser';
import {selectLoggedOut} from './webapp-common/core/reducers/view-reducer';
import {selectLoggedOut} from '@common/core/reducers/view-reducer';
import {Store} from '@ngrx/store';
import {get} from 'lodash/fp';
import {selectRouterParams, selectRouterUrl} from './webapp-common/core/reducers/router-reducer';
import {selectRouterParams, selectRouterUrl} from '@common/core/reducers/router-reducer';
import {ApiProjectsService} from './business-logic/api-services/projects.service';
import {Project} from './business-logic/model/projects/project';
import {GetAllProjects, SetSelectedProjectId, UpdateProject} from './webapp-common/core/actions/projects.actions';
import {selectSelectedProject} from './webapp-common/core/reducers/projects.reducer';
import {GetAllSystemProjects, SetSelectedProjectId, UpdateProject} from '@common/core/actions/projects.actions';
import {selectSelectedProject} from '@common/core/reducers/projects.reducer';
import {
selectS3BucketCredentialsBucketCredentials,
selectS3PopUpDetails,
selectShowLocalFilesPopUp,
selectShowS3PopUp
} from './webapp-common/core/reducers/common-auth-reducer';
} from '@common/core/reducers/common-auth-reducer';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
import {S3AccessResolverComponent} from './webapp-common/layout/s3-access-resolver/s3-access-resolver.component';
import {cancelS3Credentials, getTutorialBucketCredentials} from './webapp-common/core/actions/common-auth.actions';
import {termsOfUseAccepted} from './webapp-common/core/actions/users.actions';
import {S3AccessResolverComponent} from '@common/layout/s3-access-resolver/s3-access-resolver.component';
import {cancelS3Credentials, getTutorialBucketCredentials} from '@common/core/actions/common-auth.actions';
import {termsOfUseAccepted} from '@common/core/actions/users.actions';
import {debounceTime, distinctUntilChanged, filter, map, tap, withLatestFrom} from 'rxjs/operators';
import * as routerActions from './webapp-common/core/actions/router.actions';
import {combineLatest, Observable, Subscription} from 'rxjs';
import {selectBreadcrumbsStrings} from './webapp-common/layout/layout.reducer';
import {selectBreadcrumbsStrings} from '@common/layout/layout.reducer';
import {prepareNames} from './layout/breadcrumbs/breadcrumbs.utils';
import {formatStaticCrumb} from './webapp-common/layout/breadcrumbs/breadcrumbs-common.utils';
import {ServerUpdatesService} from './webapp-common/shared/services/server-updates.service';
import {formatStaticCrumb} from '@common/layout/breadcrumbs/breadcrumbs-common.utils';
import {ServerUpdatesService} from '@common/shared/services/server-updates.service';
import {selectAvailableUpdates, selectShowSurvey} from './core/reducers/view-reducer';
import {UPDATE_SERVER_PATH} from './app.constants';
import {firstLogin, plotlyReady, setScaleFactor, VisibilityChanged} from './webapp-common/core/actions/layout.actions';
import {UiUpdatesService} from './webapp-common/shared/services/ui-updates.service';
import {firstLogin, plotlyReady, setScaleFactor, visibilityChanged} from '@common/core/actions/layout.actions';
import {UiUpdatesService} from '@common/shared/services/ui-updates.service';
import {UsageStatsService} from './core/Services/usage-stats.service';
import {dismissSurvey} from './core/Actions/layout.actions';
import {getScaleFactor} from './webapp-common/shared/utils/shared-utils';
import {dismissSurvey} from './core/actions/layout.actions';
import {getScaleFactor} from '@common/shared/utils/shared-utils';
import {User} from './business-logic/model/users/user';
import {ConfigurationService} from './webapp-common/shared/services/configuration.service';
import {ConfigurationService} from '@common/shared/services/configuration.service';
import {GoogleTagManagerService} from 'angular-google-tag-manager';
import {selectIsSharedAndNotOwner} from './features/experiments/reducers';
import {TipsService} from './webapp-common/shared/services/tips.service';
import {TipsService} from '@common/shared/services/tips.service';
import {USER_PREFERENCES_KEY} from '@common/user-preferences';
@Component({
@@ -50,7 +50,6 @@ import {USER_PREFERENCES_KEY} from '@common/user-preferences';
})
export class AppComponent implements OnInit, OnDestroy {
public loggedOut$: Observable<any>;
public isDashboardContext: boolean;
public activeFeature: string;
private urlSubscription: Subscription;
public selectedProject$: Observable<Project>;
@@ -78,7 +77,7 @@ export class AppComponent implements OnInit, OnDestroy {
private plotlyURL: string;
@HostListener('document:visibilitychange') onVisibilityChange() {
this.store.dispatch(new VisibilityChanged(!document.hidden));
this.store.dispatch(visibilityChanged({visible: !document.hidden}));
}
@HostListener('window:beforeunload', ['$event'])
@@ -163,7 +162,7 @@ export class AppComponent implements OnInit, OnDestroy {
distinctUntilChanged((prev, next) => prev?.id === next?.id)
)
.subscribe(() => {
this.store.dispatch(new GetAllProjects());
this.store.dispatch(new GetAllSystemProjects());
this.store.dispatch(getTutorialBucketCredentials());
this.store.dispatch(termsOfUseAccepted());
this.uiUpdatesService.checkForUiUpdate();
@@ -192,13 +191,9 @@ export class AppComponent implements OnInit, OnDestroy {
}
});
this.store.select(selectActiveWorkspace).pipe(filter(ws => !!ws))
.subscribe(workspace => this.activeWorkspace = workspace?.id);
this.urlSubscription = combineLatest([this.store.select(selectRouterUrl), this.store.select(selectRouterParams)])
.subscribe(([url, params]) => {
this.projectId = get('projectId', params);
this.isDashboardContext = url && url.includes('dashboard');
this.isLoginContext = url && url.includes('login');
this.isWorkersContext = url && url.includes('workers-and-queues');
if (this.projectId) {
@@ -319,4 +314,3 @@ export class AppComponent implements OnInit, OnDestroy {
setTimeout(init);
}
}

View File

@@ -53,14 +53,6 @@ export const TASK_TYPES = {
TESTING : 'testing',
};
const formPrefix = 'FORM_';
export const FORMS_ACTIONS = {
SET_FORM_DATA : formPrefix + 'SET_FORM_DATA',
SET_FORM_STATUS : formPrefix + 'SET_FORM_STATUS',
SET_FORM_SUBMITTED: formPrefix + 'SET_FORM_SUBMITTED'
};
const recentTasksPrefix = 'RECENT_TASKS';
export const RECENT_TASKS_ACTIONS = {
@@ -69,28 +61,6 @@ export const RECENT_TASKS_ACTIONS = {
};
export const VIEW_PREFIX = 'VIEW_';
export const VIEW_ACTIONS = {
SET_SERVER_UPDATES_AVAILABLE: VIEW_PREFIX + 'SET_SERVER_UPDATES_AVAILABLE',
SET_SERVER_ERROR : VIEW_PREFIX + 'SET_SERVER_ERROR',
SET_RESULT_MESSAGE : VIEW_PREFIX + 'SET_RESULT_MESSAGE',
DEACTIVE_LOADER : VIEW_PREFIX + 'DEACTIVE_LOADER',
ACTIVE_LOADER : VIEW_PREFIX + 'ACTIVE_LOADER',
VISIBILITY_CHANGED : VIEW_PREFIX + 'VISIBILITY_CHANGED',
RESET_LOADER : VIEW_PREFIX + 'RESET_LOADER',
SET_BACKDROP : VIEW_PREFIX + 'SET_BACKDROP',
OPEN_DIALOG : VIEW_PREFIX + 'OPEN_DIALOG',
CLOSE_DIALOG : VIEW_PREFIX + 'CLOSE_DIALOG',
ADD_MESSAGE : VIEW_PREFIX + 'ADD_MESSAGE',
REMOVE_MESSAGE : VIEW_PREFIX + 'REMOVE_MESSAGE',
SET_SERVER_ERROR_STATE : VIEW_PREFIX + 'SET_SERVER_ERROR_STATE',
SET_MORE_INFO : VIEW_PREFIX + 'SET_MORE_INFO',
SET_AUTO_REFRESH : VIEW_PREFIX + 'SET_AUTO_REFRESH',
SET_NOTIFICATION_DIALOG : VIEW_PREFIX + 'SET_NOTIFICATION_DIALOG',
SET_COMPARE_AUTO_REFRESH : VIEW_PREFIX + 'SET_COMPARE_AUTO_REFRESH'
};
export type MediaContentTypeEnum = 'image/bmp' | 'image/jpeg' | 'image/png' | 'video/mp4' | 'image/jpeg';
@@ -105,56 +75,6 @@ export const MESSAGES_SEVERITY = {
WARN : 'warn' as MessageSeverityEnum
};
const TASKS_PREFIX = 'TASKS_';
export const TASKS_ACTIONS = {
SET_TASK_IN_TABLE : TASKS_PREFIX + 'SET_TASK_IN_TABLE',
GET_TASK_BY_ID_AFTER_EFFECT: TASKS_PREFIX + 'GET_TASK_BY_ID_AFTER_EFFECT',
DEQUEUE_TASK : TASKS_PREFIX + 'DEQUEUE_TASK',
SET_TASK_FOR_METRICS : TASKS_PREFIX + 'SET_TASK_FOR_METRICS',
GET_TASK_FOR_METRICS : TASKS_PREFIX + 'GET_TASK_FOR_METRICS',
CLOSE_TASK : TASKS_PREFIX + 'CLOSE_TASK',
FAIL_TASK : TASKS_PREFIX + 'FAIL_TASK',
RESUM_TASK : TASKS_PREFIX + 'RESUM_TASK',
PUBLISH_TASK : TASKS_PREFIX + 'PUBLISH_TASK',
RESET_TASK : TASKS_PREFIX + 'RESET_TASK',
GET_TASK_TOKEN : TASKS_PREFIX + ' GET_TASK_TOKEN',
ADD_SELECTED_TASK : TASKS_PREFIX + 'ADD_SELECTED_TASK',
AFTER_TASK_CHANGED : TASKS_PREFIX + 'AFTER_TASK_CHANGED',
CLOSE_TASK_LOG : TASKS_PREFIX + 'CLOSE_TASK_LOG',
CLEAR_TOKEN : TASKS_PREFIX + 'CLEAR_TOKEN',
GET_ALL : TASKS_PREFIX + 'GET_ALL',
GET_TASK_SUCCESS : TASKS_PREFIX + 'GET_TASK_SUCCESS',
GET_PROTOTEXT_SUCCESS : TASKS_PREFIX + 'GET_PROTOTEXT_SUCCESS',
GET_TASK_LOG_SUCCESS : TASKS_PREFIX + 'GET_TASK_LOG_SUCCESS',
GLOBAL_FILTER_CHANGED : TASKS_PREFIX + 'GLOBAL_FILTER_CHANGED',
VIEW_MODE_CHANGED : TASKS_PREFIX + 'VIEW_MODE_CHANGED',
TASK_CREATED : TASKS_PREFIX + 'TASK_CREATED',
TASK_CHECKED : TASKS_PREFIX + 'TASK_CHECKED',
TASK_UNCHECKED : TASKS_PREFIX + 'TASK_UNCHECKED',
TABLE_SORT_CHANGED : TASKS_PREFIX + 'TABLE_SORT_CHANGED',
TABLE_FILTER_CHANGED : TASKS_PREFIX + 'TABLE_FILTER_CHANGED',
TOGGLE_HIDDEN : TASKS_PREFIX + 'TOGGLE_HIDDEN',
TASK_DELETED_SUCCESS : TASKS_PREFIX + 'TASK_DELETED_SUCCESS',
TASK_ENQUEUE : TASKS_PREFIX + 'TASK_ENQUEUE',
TASKS_SUCCESS : TASKS_PREFIX + 'GET_ALL_SUCCESS',
TASKS_OPTIMISTIC : TASKS_PREFIX + 'OPTIMISTIC',
TASKS_TRAINING_SUCCESS : TASKS_PREFIX + 'TRAINING_SUCCESS',
TASKS_IMPORT_SUCCESS : TASKS_PREFIX + 'IMPORT_SUCCESS',
TASKS_TESTING_SUCCESS : TASKS_PREFIX + 'TESTING_SUCCESS',
RESET_SELECTED_TASK : TASKS_PREFIX + 'RESET_SELECTED_TASK',
REMOVE_SELECTED_TASK : TASKS_PREFIX + 'REMOVE_SELECTED_TASK',
RESET_SUCCESS : TASKS_PREFIX + 'RESET_SUCCESS',
SET_FIRST : TASKS_PREFIX + 'SET_FIRST',
SET_GLOBAL_FILTER : TASKS_PREFIX + 'SET_GLOBAL_FILTER',
SET_SELECTED_TASK : TASKS_PREFIX + 'SET_SELECTED_TASK',
SET_SHOW_CHECKED_TASKS : TASKS_PREFIX + 'SET_SHOW_CHECKED_TASKS',
SET_TASKS_DATA : TASKS_PREFIX + 'SET_TASKS_DATA',
SET_TABLE_COLUMNS : TASKS_PREFIX + 'SET_TABLE_COLUMNS',
STOP_CHECKED_TASKS : TASKS_PREFIX + 'STOP_CHECKED_TASKS',
STOP_TASK : TASKS_PREFIX + 'STOP_TASK',
UPDATE_TASK : TASKS_PREFIX + 'UPDATE_TASK',
};
export const USERS_PREFIX = 'USERS_';
export const USERS_ACTIONS = {
FETCH_CURRENT_USER: USERS_PREFIX + 'FETCH_USER',
@@ -207,93 +127,12 @@ if (environment.fileBaseUrl) {
apiBaseUrl += ENVIRONMENT.API_VERSION;
const HTTP_PREFIX = 'HTTP_';
export const HTTP_ACTIONS = {
REQUEST_FAILED: HTTP_PREFIX + 'REQUEST_FAILED'
};
export const HTTP_PREFIX = 'HTTP_';
export const HTTP = {
API_REQUEST : 'HTTP_API_REQUEST',
API_REQUEST_SUCCESS : 'API_REQUEST_SUCCESS',
API_REQUEST_CANCELED : 'API_REQUEST_CANCELED',
API_BASE_URL : apiBaseUrl, // <-- DIRECT CALL DOESN'T WORK
API_BASE_URL_NO_VERSION: apiBaseUrlNoVersion,
FILE_BASE_URL: fileBaseUrl,
API_METHODS: {
GET : 'GET',
POST: 'POST'
},
AUTH: {
CREATE : 'auth.create_credentials',
GET_ALL : 'auth.get_credentials',
REVOKE : 'auth.revoke_credentials',
GET_TASK_TOKEN: 'auth.get_task_token'
},
MODELS: {
GET_ALL : 'models.get_all',
GET_BY_ID : 'models.get_by_id',
CREATE_MODEL: 'models.create',
UPDATE : 'models.update'
},
USERS: {
GET_CURRENT_USER: 'users.get_current_user',
GET_BY_ID : 'users.get_by_id',
LOGOUT : '/logout'
},
QUEUES: {
GET_ALL: 'queues.get_all'
},
PROJECTS: {
GET_ALL: 'projects.get_all',
CREATE : 'projects.create',
DELETE : 'projects.delete'
},
TASKS: {
CLOSE : 'tasks.close',
PUBLISH : 'tasks.publish',
GET_ALL : 'tasks.get_all',
GET_BY_ID : 'tasks.get_by_id',
ENQUEUE : 'tasks.enqueue',
DEQUEUE : 'tasks.dequeue',
CREATE : 'tasks.create',
UPDATE : 'tasks.update',
EDIT : 'tasks.edit',
RESET : 'tasks.reset',
START : 'tasks.started',
STOP : 'tasks.stop',
STOPPED : 'tasks.stopped',
COMPLETED : 'tasks.completed',
TASK_LOG : 'events.get_task_log',
RESUME : 'tasks.resume',
FAILED : 'tasks.failed',
DEBUG_IMAGES : 'events.debug_images',
VECTOR_METRICS : 'events.get_vector_metrics_and_variants',
VECTOR_METRICS_HISTOGRAM: 'events.vector_metrics_iter_histogram',
SCALAR_METRICS : 'events.scalar_metrics_iter_histogram',
PLOT : 'events.get_task_plots',
DELETE : 'tasks.delete'
},
FRAMES: {
GET_BATCH : 'frames.get_next',
GET_BATCH_FOR_TASK: 'frames.get_next_for_task',
SET_ROIS : 'frames.set_rois',
COMMIT : 'frames.commit',
},
SOURCES: {
GET_ALL: 'storage.get_all',
}
};
export class EmptyAction implements Action {

View File

@@ -6,32 +6,29 @@ import {BusinessLogicModule} from './business-logic/business-logic.module';
import {AppComponent} from './app.component';
import {routes} from './app.routes';
import {SMCoreModule} from './core/core.module';
import {SMSharedModule} from './webapp-common/shared/shared.module';
import {SMSharedModule} from '@common/shared/shared.module';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {CommonLayoutModule} from './webapp-common/layout/layout.module';
import {CommonLayoutModule} from '@common/layout/layout.module';
import {HTTP_INTERCEPTORS} from '@angular/common/http';
import {WebappIntercptor} from './webapp-common/core/interceptors/webapp-interceptor';
import {CustomReuseStrategy} from './webapp-common/core/router-reuse-strategy';
import {WebappInterceptor} from '@common/core/interceptors/webapp-interceptor';
import {CustomReuseStrategy} from '@common/core/router-reuse-strategy';
import {ApiUsersService} from './business-logic/api-services/users.service';
import {loadUserAndPreferences} from './webapp-common/user-preferences';
import {AdminModule} from './webapp-common/admin/admin.module';
import {loadUserAndPreferences} from '@common/user-preferences';
import {AdminModule} from '@common/admin/admin.module';
import {AngularSplitModule} from 'angular-split';
import {NotifierModule} from './webapp-common/angular-notifier';
import {NotifierModule} from '@common/angular-notifier';
import {LayoutModule} from './layout/layout.module';
import {ColorHashService} from './webapp-common/shared/services/color-hash/color-hash.service';
import {LoginService} from './webapp-common/shared/services/login.service';
import {ColorHashService} from '@common/shared/services/color-hash/color-hash.service';
import {LoginService} from '@common/shared/services/login.service';
import {Store} from '@ngrx/store';
import {SharedModule} from './shared/shared.module';
import {ErrorService} from './webapp-common/shared/services/error.service';
import {ConfigurationService} from './webapp-common/shared/services/configuration.service';
import {ProjectsSharedModule} from "./features/projects/shared/projects-shared.module";
import { UserManagementComponent } from './webapp-common/user-management/user-management.component';
import { UserManagementDialogComponent } from './webapp-common/user-management/user-management-dialog/user-management-dialog.component';
import { UserManagementInvitesComponent } from './webapp-common/user-management/user-managment-invites/user-management-invites.component';
import {MAT_FORM_FIELD_DEFAULT_OPTIONS} from "@angular/material/form-field";
import {ErrorService} from '@common/shared/services/error.service';
import {ConfigurationService} from '@common/shared/services/configuration.service';
import {ProjectsSharedModule} from './features/projects/shared/projects-shared.module';
import {MAT_FORM_FIELD_DEFAULT_OPTIONS} from '@angular/material/form-field';
@NgModule({
declarations : [AppComponent, UserManagementComponent, UserManagementDialogComponent, UserManagementInvitesComponent],
declarations : [AppComponent],
imports: [
FormsModule,
ReactiveFormsModule,
@@ -72,7 +69,7 @@ import {MAT_FORM_FIELD_DEFAULT_OPTIONS} from "@angular/material/form-field";
deps : [ApiUsersService, LoginService, Store, ErrorService, ConfigurationService]
},
ColorHashService,
{provide: HTTP_INTERCEPTORS, useClass: WebappIntercptor, multi: true},
{provide: HTTP_INTERCEPTORS, useClass: WebappInterceptor, multi: true},
{provide: RouteReuseStrategy, useClass: CustomReuseStrategy},
{
provide: 'googleTagManagerId',

View File

@@ -1,7 +1,6 @@
import {Routes} from '@angular/router';
import {AdminComponent} from './webapp-common/admin/admin.component';
import {AccountAdministrationGuard} from "./webapp-common/shared/guards/account-administration.guard";
import {UserManagementComponent} from "./webapp-common/user-management/user-management.component";
import {ProjectRedirectGuardGuard} from './webapp-common/shared/guards/project-redirect.guard';
@@ -22,7 +21,6 @@ export const routes: Routes = [
{path: 'signup', loadChildren: () => import('./webapp-common/login/login.module').then(m => m.LoginModule)},
{path: 'profile', component: AdminComponent, data: {workspaceNeutral: true}},
{path: 'account-administration', component: UserManagementComponent, data: {workspaceNeutral: false, }, canActivate: [AccountAdministrationGuard]},
{
path: 'projects',

View File

@@ -51,6 +51,8 @@ import {ProjectsGetHyperparamValuesRequest} from '../model/projects/projectsGetH
import {ProjectsGetHyperparamValuesResponse} from '../model/projects/projectsGetHyperparamValuesResponse';
import {ProjectsMoveRequest} from "../model/projects/projectsMoveRequest";
import {ProjectsMoveResponse} from "../model/projects/projectsMoveResponse";
import { ProjectsValidateDeleteRequest } from '../model/projects/projectsValidateDeleteRequest';
import { ProjectsValidateDeleteResponse } from '../model/projects/projectsValidateDeleteResponse';
@Injectable()
@@ -633,7 +635,7 @@ export class ApiProjectsService {
* @param request request body
*/
public projectsGetHyperparamValuesWithHttpInfo(request: ProjectsGetHyperparamValuesRequest, options?: any, observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
public projectsGetHyperparamValues(request: ProjectsGetHyperparamValuesRequest, options?: any, observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
if (request === null || request === undefined) {
throw new Error('Required parameter request was null or undefined when calling projectsUpdate.');
}
@@ -670,4 +672,50 @@ export class ApiProjectsService {
}
);
}
/**
*
* Validates that the project existis and can be deleted
* @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 projectsValidateDelete(request: ProjectsValidateDeleteRequest, options?: any, observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
if (request === null || request === undefined) {
throw new Error('Required parameter request was null or undefined when calling projectsValidateDelete.');
}
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<ProjectsValidateDeleteResponse>(`${this.basePath}/projects.validate_delete`,
request,
{
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
}

View File

@@ -69,6 +69,8 @@ export interface ProjectsGetAllExRequest {
stats_for_state?: ProjectsGetAllExRequest.StatsForStateEnum;
check_own_contents?: boolean;
search_hidden?: boolean;
}
export namespace ProjectsGetAllExRequest {

View File

@@ -0,0 +1,20 @@
/**
* projects
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
export interface ProjectsValidateDeleteRequest {
/**
* Project ID
*/
project: string;
}

View File

@@ -0,0 +1,40 @@
/**
* projects
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
export interface ProjectsValidateDeleteResponse {
/**
* The total number of tasks under the project and all its children
*/
tasks?: number;
/**
* The total number of non-archived tasks under the project and all its children
*/
non_archived_tasks?: number;
/**
* The total number of models under the project and all its children
*/
models?: number;
/**
* The total number of non-archived models under the project and all its children
*/
non_archived_models?: number;
/**
* The total number of dataviews under the project and all its children
*/
dataviews?: number;
/**
* The total number of non-archived dataviews under the project and all its children
*/
non_archived_dataviews?: number;
}

View File

@@ -29,4 +29,5 @@ export interface ServerInfoResponse {
* Server UID
*/
uid?: string;
api_version?: string;
}

View File

@@ -132,4 +132,5 @@ export interface Task {
* Task configuration params
*/
configuration?: { [key: string]: ConfigurationItem; };
runtime?: { [key: string]: string};
}

View File

@@ -89,4 +89,6 @@ export interface TasksGetAllExRequest {
* If set to 'true' and project field is set then tasks from the subprojects are searched too
*/
include_subprojects?: boolean;
search_hidden?: boolean;
}

View File

@@ -0,0 +1,7 @@
import {createAction, props} from '@ngrx/store';
import {USERS_PREFIX} from '../../app.constants';
import {GetCurrentUserResponseUserObject} from '../../business-logic/model/users/getCurrentUserResponseUserObject';
export const setCurrentUser = createAction(USERS_PREFIX + 'SET_CURRENT_USER',
props<{user: GetCurrentUserResponseUserObject; terms_of_use?: any}>()
);

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import {Store} from '@ngrx/store';
import {filter} from 'rxjs/operators';
import {updateUsageStats} from '../Actions/usage-stats.actions';
import {updateUsageStats} from '../actions/usage-stats.actions';
import {selectPromptUser} from '../reducers/usage-stats.reducer';
import {MatDialog} from '@angular/material/dialog';
import {ConfirmDialogComponent} from '../../webapp-common/shared/ui-components/overlay/confirm-dialog/confirm-dialog.component';

View File

@@ -8,7 +8,6 @@ import {sourcesReducer} from './reducers/sources-reducer';
import {viewReducer} from './reducers/view-reducer';
import {USERS_PREFIX, VIEW_PREFIX} from '../app.constants';
import {merge, pick} from 'lodash/fp';
import {usersReducer} from '../webapp-common/core/reducers/users-reducer';
import {projectsReducer} from '../webapp-common/core/reducers/projects.reducer';
import {EffectsModule} from '@ngrx/effects';
import {SmSyncStateSelectorService} from '../webapp-common/core/services/sync-state-selector.service';
@@ -27,6 +26,7 @@ import {PROJECTS_PREFIX} from '../webapp-common/core/actions/projects.actions';
import {loginReducer} from '../webapp-common/login/login-reducer';
import {ConfigurationService} from '../webapp-common/shared/services/configuration.service';
import {AUTH_PREFIX} from '../webapp-common/core/actions/common-auth.actions';
import {usersReducer} from './reducers/users.reducer';
export const reducers = {
auth: commonAuthReducer,
@@ -93,7 +93,7 @@ export function setViewPreferencesReducer(reducer: ActionReducer<any>): ActionRe
}
export function setRootProjectPreferencesReducer(reducer: ActionReducer<any>): ActionReducer<any> {
return createLocalStorageReducer('rootProjects', ['rootProjects.tagsColors', 'rootProjects.tagsFilterByProject'] , [PROJECTS_PREFIX])(reducer);
return createLocalStorageReducer('rootProjects', ['rootProjects.tagsColors', 'rootProjects.tagsFilterByProject', 'rootProjects.graphVariant'] , [PROJECTS_PREFIX])(reducer);
}
export function getMetaReducers() {

View File

@@ -1,13 +1,12 @@
import {Injectable} from '@angular/core';
import {Actions, Effect, ofType, createEffect} from '@ngrx/effects';
import {USERS_ACTIONS} from '../../app.constants';
import {filter, take, mergeMap, switchMap} from 'rxjs/operators';
import {CookiesService} from '../../shared/cookies.service';
import {ApiAuthService} from '../../business-logic/api-services/auth.service';
import {ApiServerService} from '../../business-logic/api-services/server.service';
import {ServerReportStatsOptionResponse} from '../../business-logic/model/server/serverReportStatsOptionResponse';
import {setUsageStats, updateUsageStats} from '../Actions/usage-stats.actions';
import {FetchCurrentUser} from '../../webapp-common/core/actions/users.actions';
import {setUsageStats, updateUsageStats} from '../actions/usage-stats.actions';
import {fetchCurrentUser} from '@common/core/actions/users.actions';
@Injectable()
@@ -18,7 +17,7 @@ export class UserEffects {
@Effect()
setUser$ = this.actions.pipe(
ofType<FetchCurrentUser>(USERS_ACTIONS.SET_CURRENT_USER),
ofType(fetchCurrentUser),
filter(user => !!user),
take(1),
mergeMap(() => this.serverService.serverReportStatsOption({})

View File

@@ -1,5 +1,5 @@
import {Action, createReducer, on, createSelector} from '@ngrx/store';
import {setUsageStats} from '../Actions/usage-stats.actions';
import {setUsageStats} from '../actions/usage-stats.actions';
export const userStatsFeatureKey = 'userStats';

View File

@@ -0,0 +1,18 @@
import {createReducer, createSelector, on} from '@ngrx/store';
import {initUsers, users, usersReducerFunctions, UsersState} from '../../webapp-common/core/reducers/users-reducer';
import {setCurrentUser} from '../actions/users.action';
export const selectHasDataFeature = createSelector(users, () => false);
export const selectHasUserManagement = createSelector(users, () => false);
export const usersReducer = createReducer<UsersState>(initUsers,
...usersReducerFunctions,
on(setCurrentUser, (state, action) => ({
...state,
currentUser: action.user,
activeWorkspace: action.user.company,
userWorkspaces: [action.user.company],
termsOfUse: action.terms_of_use,
}))
);

View File

@@ -1,12 +1,11 @@
import {VIEW_ACTIONS} from '../../app.constants';
import {createSelector} from '@ngrx/store';
import {
initViewState as commonInitState,
viewReducer as commonViewReducer,
ViewState as CommonViewState
} from '../../webapp-common/core/reducers/view-reducer';
import {dismissSurvey} from '../Actions/layout.actions';
import {setServerUpdatesAvailable} from '../../webapp-common/core/actions/layout.actions';
import {dismissSurvey} from '../actions/layout.actions';
import {setServerUpdatesAvailable} from '@common/core/actions/layout.actions';
interface ViewState extends CommonViewState {
availableUpdates: string;

View File

@@ -3,7 +3,7 @@ import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import {Store} from '@ngrx/store';
import {selectAllowed} from '../../../core/reducers/usage-stats.reducer';
import {Observable} from 'rxjs';
import { updateUsageStats } from '../../../core/Actions/usage-stats.actions';
import { updateUsageStats } from '../../../core/actions/usage-stats.actions';
import {ConfigurationService} from '../../../webapp-common/shared/services/configuration.service';
@@ -13,6 +13,7 @@ import {ConfigurationService} from '../../../webapp-common/shared/services/confi
styleUrls: ['./usage-stats.component.scss']
})
export class UsageStatsComponent implements OnInit {
public shown = true;
public demo = ConfigurationService.globalEnvironment.demo;
public allowed$: Observable<boolean>;

View File

@@ -1,19 +1,18 @@
<sm-dashboard-search [ngClass]="{'dashboard-search': (activeSearch$ | async)}"></sm-dashboard-search>
<div *ngIf="!(activeSearch$ | async)" class="recent">
<div class="container h-100">
<div *ngIf="!(activeSearch$ | async)" class="container h-100">
<div class="recent">
<sm-dashboard-projects></sm-dashboard-projects>
<sm-dashboard-experiments>
<div header-buttons>
<button
*smCheckPermission="true"
class="btn btn-primary d-flex align-items-center"
(click)="redirectToWorkers()"
><i class="al-icon al-ico-queues al-color light-grey-blue sm mr-2"></i>MANAGE WORKERS AND QUEUES</button>
</div>
</sm-dashboard-experiments>
<sm-dashboard-search [class.dashboard-search]="activeSearch$ | async"></sm-dashboard-search>
<div *ngIf="(activeSearch$ | async) !== true" class="dashboard-body">
<div class="recent">
<sm-dashboard-projects (width)="width = $event"></sm-dashboard-projects>
<sm-dashboard-experiments
[style.width]="width && width > 900 ? width + 'px' : '100%'"
[recentTasks]="recentTasks$| async"
>
<div header-buttons>
<button
*smCheckPermission="true"
class="btn btn-primary d-flex align-items-center"
(click)="redirectToWorkers()"
><i class="al-icon al-ico-queues al-color light-grey-blue sm mr-2"></i>MANAGE WORKERS AND QUEUES</button>
</div>
</div>
</sm-dashboard-experiments>
</div>
</div>

View File

@@ -2,31 +2,15 @@
@import "../../webapp-common/shared/ui-components/styles/variables";
:host {
.projects.row {
min-height: 228px;
}
.datasets.row {
min-height: 228px;
.dashboard-body {
height: 100%;
padding: 0 24px;
overflow: auto;
}
.recent {
height: 100%;
overflow: auto;
@include recent-title();
.recent-title {
color: #5d6787;
margin-top: 20px;
}
.right {
text-align: right;
}
.recent-row {
margin-top: 30px;
}
}
.search-bar {
@@ -66,7 +50,8 @@
sm-dashboard-experiments {
display: block;
height: calc(100% - 344px);
margin: 0 auto;
height: calc(100% - 356px);
}
.dashboard-search {

View File

@@ -2,19 +2,19 @@ import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {Observable, Subscription} from 'rxjs';
import {Store} from '@ngrx/store';
import {ActivatedRoute, Router} from '@angular/router';
import {selectActiveWorkspace, selectShowOnlyUserWork} from '../../webapp-common/core/reducers/users-reducer';
import {selectShowOnlyUserWork} from '../../webapp-common/core/reducers/users-reducer';
import {GetCurrentUserResponseUserObjectCompany} from '../../business-logic/model/users/getCurrentUserResponseUserObjectCompany';
import {addWorkspace} from '../../webapp-common/core/actions/users.actions';
import {distinctUntilKeyChanged, filter, skip, take} from 'rxjs/operators';
import {filter, skip, take} from 'rxjs/operators';
import {LoginService} from '../../webapp-common/shared/services/login.service';
import {ResetSelectedProject, setDeep} from '../../webapp-common/core/actions/projects.actions';
import {setDeep} from '../../webapp-common/core/actions/projects.actions';
import {GetRecentProjects, GetRecentTasks} from '../../webapp-common/dashboard/common-dashboard.actions';
import {InitSearch} from '../../webapp-common/common-search/common-search.actions';
import {selectActiveSearch} from '../../webapp-common/common-search/common-search.reducer';
import {selectFirstLogin} from '../../webapp-common/core/reducers/view-reducer';
import {MatDialog} from '@angular/material/dialog';
import {WelcomeMessageComponent} from '../../webapp-common/dashboard/dumb/welcome-message/welcome-message.component';
import {firstLogin} from '../../webapp-common/core/actions/layout.actions';
import {IRecentTask, selectRecentTasks} from '../../webapp-common/dashboard/common-dashboard.reducer';
@Component({
@@ -24,10 +24,12 @@ import {firstLogin} from '../../webapp-common/core/actions/layout.actions';
})
export class DashboardComponent implements OnInit, OnDestroy {
public activeSearch$: Observable<boolean>;
public recentTasks$: Observable<Array<IRecentTask>>;
public workspace: GetCurrentUserResponseUserObjectCompany;
public width: number;
private workspaceSub: Subscription;
private welcomeSub: Subscription;
showOnlyUserWorkSub: Subscription;
private showOnlyUserWorkSub: Subscription;
constructor(
private store: Store<any>,
@@ -38,22 +40,13 @@ export class DashboardComponent implements OnInit, OnDestroy {
) {
const inviteId: string = this.activatedRoute.snapshot.queryParams['invite'];
this.activeSearch$ = this.store.select(selectActiveSearch);
this.recentTasks$ = this.store.select(selectRecentTasks);
this.showOnlyUserWorkSub = this.store.select(selectShowOnlyUserWork).pipe(skip(1)).subscribe(() => {
this.store.dispatch(new GetRecentProjects());
this.store.dispatch(new GetRecentTasks());
});
this.workspaceSub = this.store.select(selectActiveWorkspace).pipe(
filter(ws => !!ws),
distinctUntilKeyChanged('id'),
skip(1)
).subscribe(() => {
this.store.dispatch(new ResetSelectedProject());
this.store.dispatch(new InitSearch('Search for all'));
this.store.dispatch(new GetRecentProjects());
this.store.dispatch(new GetRecentTasks());
});
if (inviteId) {
this.store.dispatch(addWorkspace({inviteId}));
this.removeInviteFromURL();

View File

@@ -0,0 +1,24 @@
import {Actions} from '@ngrx/effects';
import {ApiProjectsService} from '../../business-logic/api-services/projects.service';
import {Injectable} from '@angular/core';
import {Store} from '@ngrx/store';
import {AdminService} from '../admin/admin.service';
import {ApiTasksService} from '../../business-logic/api-services/tasks.service';
import {ApiModelsService} from '../../business-logic/api-services/models.service';
import {DeleteDialogEffectsBase} from '../../webapp-common/shared/entity-page/entity-delete/base-delete-dialog.effects';
import {ConfigurationService} from '../../webapp-common/shared/services/configuration.service';
@Injectable()
export class DeleteDialogEffects extends DeleteDialogEffectsBase {
constructor(actions$: Actions,
store: Store<any>,
tasksApi: ApiTasksService,
modelsApi: ApiModelsService,
projectsApi: ApiProjectsService,
adminService: AdminService,
configService: ConfigurationService) {
super(actions$, store, tasksApi, modelsApi, projectsApi, adminService, configService);
}
// (Nir) don't delete this. Other repositories need to override some base functions.
}

View File

@@ -1,30 +1,6 @@
export const COMPARE_DETAILS_ONLY_FIELDS = [
'id',
'name',
'type',
'status',
'last_update',
'project.name',
'models.input.name',
'models.output.name',
'models.output.model.name',
'models.output.model.uri',
'models.output.model.framework',
'models.output.model.design',
'models.input.name',
'models.input.model.name',
'models.input.model.uri',
'models.input.model.framework',
'models.input.model.labels',
'models.input.model.design',
'execution.artifacts',
'container.*',
'script',
'tags',
'published',
'last_iteration',
'configuration'
];
import {COMPARE_DETAILS_ONLY_FIELDS_BASE} from '../../webapp-common/experiments-compare/experiments-compare.constants';
export const getCompareDetailsOnlyFields = param => COMPARE_DETAILS_ONLY_FIELDS_BASE;
export const COMPARE_PARAMS_ONLY_FIELDS = [
'id',

View File

@@ -1,5 +1,5 @@
export abstract class ExperimentCompareDetailsBase {
public buildExperimentTree(experiment, baseExperiment, mergedExperiment) {
public buildExperimentTree(experiment, baseExperiment, mergedExperiment, param) {
return {
artifacts: this.buildSectionTree(experiment, 'artifacts', mergedExperiment),
execution: this.buildSectionTree(experiment, 'execution', mergedExperiment),
@@ -8,5 +8,6 @@ export abstract class ExperimentCompareDetailsBase {
}
abstract buildCompareTree(experiments, hasDataFeature?);
abstract buildSectionTree(experiment: any, execution1: string, mergedExperiment: any);
}

View File

@@ -12,8 +12,8 @@ import {isReadOnly} from '../../../../webapp-common/shared/utils/shared-utils';
import {selectRouterConfig, selectRouterParams, selectRouterQueryParams} from '../../../../webapp-common/core/reducers/router-reducer';
import * as commonInfoActions from '../../../../webapp-common/experiments/actions/common-experiments-info.actions';
import {ExperimentDetailsUpdated} from '../../../../webapp-common/experiments/actions/common-experiments-info.actions';
import {AddMessage} from '../../../../webapp-common/core/actions/layout.actions';
import {IExperimentInfo, ISelectedExperiment} from '../../shared/experiment-info.model';
import {addMessage} from '../../../../webapp-common/core/actions/layout.actions';
import {IExperimentInfo} from '../../shared/experiment-info.model';
import {selectSelectedTableExperiment} from '../../../../webapp-common/experiments/reducers';
import {ITableExperiment} from '../../../../webapp-common/experiments/shared/common-experiment-model.model';
@@ -99,7 +99,7 @@ export class ExperimentInfoComponent implements OnInit, OnDestroy {
if (name.trim().length > 2) {
this.store.dispatch(new ExperimentDetailsUpdated({id: this.selectedExperiment.id, changes: {name: name}}));
} else {
this.store.dispatch(new AddMessage(MESSAGES_SEVERITY.ERROR, 'Name must be more than three letters long'));
this.store.dispatch(addMessage(MESSAGES_SEVERITY.ERROR, 'Name must be more than three letters long'));
}
}

View File

@@ -13,7 +13,7 @@
>
</sm-experiment-info-header>
<nav [class.minimized]="minimized">
<nav [class.minimized]="minimized" [smOverflows]="'nav'" (onOverflows)="overflow = $event">
<ng-container *ngIf="!minimized">
<span [routerLink]="['execution']" queryParamsHandling="preserve">
<sm-navbar-item header="execution" [active]="routerConfig.includes('execution')"></sm-navbar-item>
@@ -28,18 +28,32 @@
<sm-navbar-item header="info" [active]="routerConfig.includes('general')"></sm-navbar-item>
</span>
</ng-container>
<span [routerLink]="['log']" queryParamsHandling="preserve">
<sm-navbar-item header="console" [active]="routerConfig.includes('log')"></sm-navbar-item>
</span>
<span [routerLink]="['metrics','scalar']" queryParamsHandling="preserve">
<sm-navbar-item header="Scalars" [active]="routerConfig.includes('metrics') && routerConfig.includes('scalar')"></sm-navbar-item>
</span>
<span [routerLink]="['metrics','plots']" queryParamsHandling="preserve">
<sm-navbar-item header="PLOTS" [active]="routerConfig.includes('metrics') && routerConfig.includes('plots')"></sm-navbar-item>
</span>
<span [routerLink]="['debugImages']" queryParamsHandling="preserve">
<sm-navbar-item header="DEBUG SAMPLES" [active]="routerConfig.includes('debugImages')"></sm-navbar-item>
<span [matMenuTriggerFor]="results" *ngIf="!minimized && overflow">
<sm-navbar-item header="results" [multi]="true" [active]="console.active || scalar.active || plots.active || samples.active"></sm-navbar-item>
</span>
<mat-menu #results="matMenu">
<button mat-menu-item [routerLink]="['log']" [class.active]="routerConfig.includes('log')">CONSOLE</button>
<button mat-menu-item [routerLink]="['metrics','scalar']" [class.active]="routerConfig.includes('metrics') && routerConfig.includes('scalar')">SCALARS</button>
<button mat-menu-item [routerLink]="['metrics','plots']" [class.active]="routerConfig.includes('metrics') && routerConfig.includes('plots')">PLOTS</button>
<button mat-menu-item [routerLink]="['debugImages']" [class.active]="routerConfig.includes('debugImages')">DEBUG SAMPLES</button>
</mat-menu>
<div class="d-inline-block" [style.visibility]="overflow && !minimized ? 'hidden' : 'visible'">
<span [routerLink]="['log']" queryParamsHandling="preserve">
<sm-navbar-item #console header="console" [active]="routerConfig.includes('log')"></sm-navbar-item>
</span>
<span [routerLink]="['metrics','scalar']" queryParamsHandling="preserve">
<sm-navbar-item #scalar header="Scalars" [active]="routerConfig.includes('metrics') && routerConfig.includes('scalar')"></sm-navbar-item>
</span>
<span [routerLink]="['metrics','plots']" queryParamsHandling="preserve">
<sm-navbar-item #plots header="PLOTS" [active]="routerConfig.includes('metrics') && routerConfig.includes('plots')"></sm-navbar-item>
</span>
<span [routerLink]="['debugImages']" queryParamsHandling="preserve">
<sm-navbar-item #samples header="DEBUG SAMPLES" [active]="routerConfig.includes('debugImages')"></sm-navbar-item>
</span>
</div>
<span class="refresh-position">
<sm-experiment-settings
[class.maximized]="!minimized"

View File

@@ -7,4 +7,5 @@ import {BaseExperimentOutputComponent} from '../../../../webapp-common/experimen
styleUrls: ['../../../../webapp-common/experiments/containers/experiment-ouptut/base-experiment-output.component.scss']
})
export class ExperimentOutputComponent extends BaseExperimentOutputComponent {
public overflow: boolean;
}

View File

@@ -1,8 +1,9 @@
export {EXPERIMENT_INFO_ONLY_FIELDS} from '@common/experiments/experiment.consts';
import {EXPERIMENT_INFO_ONLY_FIELDS_BASE} from '@common/experiments/experiment.consts';
export {INITIAL_EXPERIMENT_TABLE_COLS} from '../../webapp-common/experiments/experiment.consts';
export const GET_ALL_QUERY_ANY_FIELDS = ['id', 'name', 'comment', 'system_tags', 'models.output.model', 'models.input.model'];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const getExperimentInfoOnlyFields = (hasDataFeature: boolean) => EXPERIMENT_INFO_ONLY_FIELDS_BASE;
export const DEFAULT_EXPERIMENT_TAB = 'execution';

View File

@@ -34,10 +34,10 @@ import {ExperimentOutputComponent} from './containers/experiment-ouptut/experime
const syncedKeys = [
'view.tableSortField',
'view.tableSortOrder',
'view.tableFilters',
'view.hiddenTableCols',
'view.projectColumnsSortOrder',
'view.projectColumnFilter',
'view.projectColumnsWidth',
'view.hiddenProjectTableCols',
'view.metricsCols',
'view.colsOrder',
'info.userKnowledge',

View File

@@ -2,14 +2,13 @@ import {ActionReducerMap, createSelector} from '@ngrx/store';
import {experimentsViewReducer, IExperimentsViewState} from './experiments-view.reducer';
import {experimentInfoReducer, IExperimentInfoState} from './experiment-info.reducer';
import {experimentOutputReducer} from './experiment-output.reducer';
import {IExperimentInfo, ISelectedExperiment} from '../shared/experiment-info.model';
import {IExperimentInfo} from '../shared/experiment-info.model';
import {TaskStatusEnum} from '../../../business-logic/model/tasks/taskStatusEnum';
import {MetricVariantResult} from '../../../business-logic/model/projects/metricVariantResult';
import {isReadOnly, isSharedAndNotOwner} from '../../../webapp-common/shared/utils/shared-utils';
import {EXPERIMENTS_STORE_KEY} from '../../../webapp-common/experiments/shared/common-experiments.const';
import {CommonExperimentOutputState} from '../../../webapp-common/experiments/reducers/common-experiment-output.reducer';
import {selectActiveWorkspace} from '../../../webapp-common/core/reducers/users-reducer';
import {selectSelectedModel} from "../../../webapp-common/models/reducers";
import {selectCurrentUser} from '@common/core/reducers/users-reducer';
export const experimentsReducers: ActionReducerMap<any, any> = {
view: experimentsViewReducer,
@@ -42,12 +41,12 @@ export const selectShowExtraDataSpinner = createSelector(experimentInfo, state =
// output selectors
export const experimentOutput = createSelector(experiments, (state): CommonExperimentOutputState => state ? state.output : {});
export const selectIsExperimentEditable = createSelector(selectSelectedExperiment, selectActiveWorkspace,
(experiment, user): boolean => experiment && experiment.status === TaskStatusEnum.Created && !isReadOnly(experiment) && !isSharedAndNotOwner(experiment, user));
export const selectIsSharedAndNotOwner = createSelector(selectSelectedExperiment, selectSelectedModel, selectActiveWorkspace,
export const selectIsExperimentEditable = createSelector(selectSelectedExperiment, selectCurrentUser,
(experiment, user): boolean => experiment && experiment.status === TaskStatusEnum.Created && !isReadOnly(experiment) && !isSharedAndNotOwner(experiment, user.company));
export const selectIsSharedAndNotOwner = createSelector(selectSelectedExperiment, selectSelectedModel, selectCurrentUser,
(experiment, model, user): boolean => {
const item = experiment || model;
return item && isSharedAndNotOwner(item, user);
return item && isSharedAndNotOwner(item, user.company);
}
);
export const selectExperimentInfoDataFreeze = createSelector(experimentInfo, (state): IExperimentInfo => state.infoDataFreeze);

View File

@@ -1 +1 @@
export {ProjectRoute, PROJECT_ROUTES} from '@common/projects/common-projects.consts';

View File

@@ -2,10 +2,8 @@ import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {SMSharedModule} from '../../webapp-common/shared/shared.module';
import {StoreModule} from '@ngrx/store';
import {ProjectRouterModule} from '../projects/projects-routing.module';
import {EffectsModule} from '@ngrx/effects';
import {projectsReducer} from '../projects/projects.reducer';
import {ProjectsEffects} from './projects.effects';
import {ProjectRouterModule} from './projects-routing.module';
import {projectsReducer} from './projects.reducer';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {CommonProjectsModule} from '../../webapp-common/projects/common-projects.module';
@@ -18,7 +16,6 @@ import {CommonProjectsModule} from '../../webapp-common/projects/common-projects
ReactiveFormsModule,
CommonProjectsModule,
StoreModule.forFeature('projects', projectsReducer),
EffectsModule.forFeature([ProjectsEffects]),
],
declarations : []
})

View File

@@ -5,7 +5,7 @@ import {Task} from '../../business-logic/model/tasks/task';
import {selectSelectedTableModel} from '../../webapp-common/models/reducers';
import {createSelector} from '@ngrx/store';
import {selectSelectedExperiment} from '../../features/experiments/reducers';
import {selectProjects, selectSelectedProject} from '../../webapp-common/core/reducers/projects.reducer';
import {selectRootProjects, selectSelectedProject} from '../../webapp-common/core/reducers/projects.reducer';
import {formatStaticCrumb, prepareLinkData} from '../../webapp-common/layout/breadcrumbs/breadcrumbs-common.utils';
export interface IBreadcrumbs {
@@ -17,13 +17,12 @@ export interface IBreadcrumbs {
}
export const selectBreadcrumbsStringsBase = createSelector(
selectSelectedProject, selectSelectedExperiment, selectSelectedTableModel,selectProjects,
selectSelectedProject, selectSelectedExperiment, selectSelectedTableModel, selectRootProjects,
(project, experiment, model,projects) =>
({project, experiment, model, projects}) as IBreadcrumbs);
export function prepareNames(data: IBreadcrumbs) {
export const prepareNames = (data: IBreadcrumbs) => {
const project = prepareLinkData(data.project, true);
if (data.project) {
const subProjects = [];
@@ -35,12 +34,12 @@ export function prepareNames(data: IBreadcrumbs) {
...data.projects,
{id: '*', name: 'All Experiments'},
{...data.project}
].find(project => currentName === project.name);
].find(proj => currentName === proj.name);
subProjects.push(foundProject);
});
const subProjectsLinks = subProjects.map(project => ({
name: project?.name.substring(project?.name.lastIndexOf('/') + 1),
url: `projects/${project?.id}/projects`
const subProjectsLinks = subProjects.map(subProject => ({
name: subProject?.name.substring(subProject?.name.lastIndexOf('/') + 1),
url: `projects/${subProject?.id}/projects`
})) as { name: string; url: string }[];
project.name = project.name.substring(project.name.lastIndexOf('/') + 1);
project.subCrumbs = subProjectsLinks;
@@ -71,9 +70,9 @@ export function prepareNames(data: IBreadcrumbs) {
artifacts: formatStaticCrumb('artifacts'),
general: formatStaticCrumb('general'),
log: formatStaticCrumb('logs'),
'scalar': formatStaticCrumb('scalars'),
'plots': formatStaticCrumb('plots'),
scalar: formatStaticCrumb('scalars'),
plots: formatStaticCrumb('plots'),
accountAdministration,
debugImages: formatStaticCrumb('Debug Samples'),
};
}
};

View File

@@ -1,17 +1,6 @@
import {TaskStatusEnum} from '../../business-logic/model/tasks/taskStatusEnum';
import {TaskTypeEnum} from '../../business-logic/model/tasks/taskTypeEnum';
export interface ProjectRoute {
header: 'overview' | 'models' | 'experiments';
subHeader: string;
}
export const PROJECT_ROUTES = [
{header: 'overview', subHeader: ''},
{header: 'experiments', subHeader: '(ARCHIVED)'},
{header: 'models', subHeader: '(ARCHIVED)'},
] as ProjectRoute[];
export enum EntityTypeEnum {
experiment = 'experiment',
model = 'model',

View File

@@ -14,7 +14,7 @@ export class CheckPermissionDirective implements OnDestroy{
private blocked = true;
@Input() set smCheckPermission(permission: boolean | string) {
this.blocked = !permission;
this.blocked = permission === false;
this.setUpView();
}

View File

@@ -1 +0,0 @@
cd -

View File

@@ -14,7 +14,7 @@
<td>{{credential?.last_used_from ? credential?.last_used_from : 'Not available'}}</td>
<td>
<span class="delete-button" (click)="confirmPopUp(credential)" type="button" smTooltip="Revoke">
<i class="fa" [ngClass]="ICONS.REMOVE"></i>
<i class="al-icon sm-md" [ngClass]="ICONS.REMOVE"></i>
</span>
</td>
</tr>

View File

@@ -37,11 +37,8 @@ tbody {
}
.delete-button {
width: 32px;
height: 32px;
text-align: center;
line-height: 32px;
display: flex;
align-items: center;
color: $light-grey-blue;
margin-right: 12px;
}

View File

@@ -5,9 +5,10 @@
<div class="body">
<div class="title first">User Preferences</div>
<div class="section toggles">
<sm-usage-stats></sm-usage-stats>
<sm-usage-stats #usage></sm-usage-stats>
<mat-slide-toggle
*ngIf="supportReScaling"
[class.first]="!usage.shown"
(change)="HidpiChange($event)"
[checked]="disableHidpi">
Disable HiDPI browser scale override
@@ -33,13 +34,15 @@
<sm-s3-access [S3BucketCredentials]="S3BucketCredentials | async"
(S3BucketCredentialsChanged)="onS3BucketCredentialsChanged($event)"></sm-s3-access>
</div>
<ng-container *smCheckPermission="'manageAppCredentials'">
<ng-container *smCheckPermission="'engineer'">
<div *ngIf="communityServer; else credsTitle" class="title">WORKSPACES</div>
<ng-template #credsTitle><div class="title">APP CREDENTIALS<i
class="fas fa-info-circle info"
smTooltip="Credentials providing API access to this workspace"
matTooltipPosition="above"
></i></div></ng-template>
<ng-template #credsTitle>
<div class="title">APP CREDENTIALS<i
class="fas fa-info-circle info"
smTooltip="Credentials providing API access to this workspace"
matTooltipPosition="above"
></i></div>
</ng-template>
<div *ngIf="communityServer; else credsSection" class="section mb-4">
<mat-expansion-panel
*ngFor="let workspace of workspaces; trackBy: trackByWorkspace"
@@ -72,9 +75,9 @@
(credentialRevoked)="onCredentialRevoked($event, workspace)">
</sm-admin-credential-table>
<span
class="add-button d-flex align-items-center pointer"
class="add-button d-flex align-items-center pointer"
[class.disabled]="creatingCredentials"
(click)="createCredential(workspace)"
(click)="createCredential(workspace)"
>
<i class="al-icon sm al-ico-plus mr-1"></i> Create new credentials
</span>
@@ -83,15 +86,7 @@
<div class="space-title"><i class="al-icon al-ico-users mr-2"></i>Members</div>
<div class="d-flex align-items-center flex-wrap">
<div class="util-text">Workspace has {{workspace?.allocated}} of {{workspace?.allowed}} users</div>
<div class="circles">
<div *ngFor="let user of workspace?.users; let index=index; trackBy: trackByUser"
class="circle"
[class.empty]="!user.name"
[class.long]="user.initials?.length > 4"
[style.left.px]="-6 * index"
[smTooltip]="user.name" matTooltipPosition="above"
>{{user.name ? user.initials : ''}}</div>
</div>
<sm-circles-in-row [data]="workspace?.users"></sm-circles-in-row>
</div>
<ng-container *ngIf="workspace?.id === currentUser.company.id; else leave">
<button class="btn button-outline-dark mt-4"
@@ -134,5 +129,12 @@
<span class="notice">Please <a href="https://www.clear.ml/contact-us" target="_blank">contact us</a> to remove your account from the service.</span>
</ng-template>
</ng-container>
<span class="version float-right">Version: {{version}}</span>
<div class="version float-right">
<span>Version: </span>
<span>{{version}}</span>
<ng-container *ngIf="(serverVersions$ | async) as serverVersions">
<span> • {{serverVersions?.server}}</span>
<span> • {{serverVersions?.api}}</span>
</ng-container>
</div>
</footer>

View File

@@ -69,9 +69,20 @@ $tab-width: 15vw;
margin: 6px 0 0 24px;
}
.toggles > * {
display: block;
margin-top: 12px;
.toggles {
display: inline-flex;
position: relative;
flex-direction: column;
margin-top: 0;
& > *:not(:first-child){
display: block;
margin-top: 12px;
&.first {
margin-top: 0;
}
}
}
mat-expansion-panel, mat-expansion-panel-header {
@@ -191,53 +202,23 @@ $tab-width: 15vw;
}
}
.circles {
sm-circles-in-row {
flex: 1;
max-width: 290px;
display: flex;
margin: 6px 0 0 24px;
.circle {
width: 34px;
min-width: 34px;
height: 34px;
line-height: 32px;
text-align: center;
background-color: $light-grey-blue;
color: $blue-800;
border-radius: 50%;
font-size: 12px;
border: $blue-800 solid 2px;
position: relative;
box-sizing: border-box;
cursor: pointer;
&.empty {
background-color: $blue-700;
border-color: $light-grey-blue;
height: 32px;
width: 32px;
min-width: 32px;
margin-top: 1px;
}
&.long {
width: unset;
min-width: unset;
border-radius: 18px;
padding: 0 4px;
}
}
}
.reload {
color: transparent;
text-decoration: underline;
transition: color 0.5s;
position: absolute;
right: 50px;
transition: color 0.5s ease-in-out, right 0.5s ease-in-out;
&.highlight {
color: $neon-yellow;
transition: color 0.5s;
right: -100px;
}
}
}

View File

@@ -2,13 +2,14 @@ import {Component, OnDestroy, OnInit} from '@angular/core';
import {combineLatest, Observable, Subscription} from 'rxjs';
import {Store} from '@ngrx/store';
import {AdminService} from '../../features/admin/admin.service';
import {selectActiveWorkspace, selectCurrentUser, selectUserWorkspaces} from '../core/reducers/users-reducer';
import {selectActiveWorkspace, selectCurrentUser, selectServerVersions, selectUserWorkspaces} from '../core/reducers/users-reducer';
import {createCredential, credentialRevoked, getAllCredentials, updateS3Credential} from '../core/actions/common-auth.actions';
import {selectCredentials, selectNewCredential, selectS3BucketCredentials} from '../core/reducers/common-auth-reducer';
import { MatDialog } from '@angular/material/dialog';
import {CreateCredentialDialogComponent} from './create-credential-dialog/create-credential-dialog.component';
import {debounceTime, filter, take, tap} from 'rxjs/operators';
import {
getApiVersion,
getUserWorkspaces,
leaveWorkspace,
logout,
@@ -64,6 +65,7 @@ export class AdminComponent implements OnInit, OnDestroy {
creatingCredentials = false;
public panelState = {} as { [workspaceId: string]: boolean };
private confSub: Subscription;
public serverVersions$: Observable<{ server: string; api: string }>;
constructor(
public adminService: AdminService,
@@ -95,9 +97,11 @@ export class AdminComponent implements OnInit, OnDestroy {
this.neverShowTipsAgain$ = store.select(selectNeverShowPopups);
this.credentials$ = this.store.select(selectCredentials);
this.serverVersions$ = this.store.select(selectServerVersions);
}
ngOnInit() {
this.store.dispatch(getApiVersion());
this.store.select(selectCurrentUser)
.pipe(filter(user => !!user), take(1))
.subscribe(() => {

View File

@@ -1,7 +1,6 @@
import {Injectable} from '@angular/core';
import {select, Store} from '@ngrx/store';
import {from, Observable, Subject} from 'rxjs';
import {HTTP} from '../../app.constants';
import {
selectDontShowAgainForBucketEndpoint, selectRevokeSucceed, selectS3BucketCredentials, selectS3BucketCredentialsBucketCredentials
} from '../core/reducers/common-auth-reducer';
@@ -16,16 +15,17 @@ import {ConfigurationService} from '../shared/services/configuration.service';
import {selectActiveWorkspace} from '../core/reducers/users-reducer';
import {GetCurrentUserResponseUserObjectCompany} from '../../business-logic/model/users/getCurrentUserResponseUserObjectCompany';
import {Environment} from '../../../environments/base';
import {apiRequest} from '@common/core/actions/http.actions';
const LOCAL_SERVER_PORT = 27878;
@Injectable()
export class BaseAdminService {
bucketCredentials: Observable<any>;
public S3Services = {};
public s3Services = {};
revokeSucceed: Observable<any>;
protected store: Store<any>;
private S3BucketCredentials: Observable<any>;
private readonly s3BucketCredentials: Observable<any>;
private previouslySignedUrls = {};
private localServerWorking = false;
private workspace: GetCurrentUserResponseUserObjectCompany;
@@ -36,8 +36,8 @@ export class BaseAdminService {
this.store = store;
this.revokeSucceed = store.pipe(select(selectRevokeSucceed));
this.revokeSucceed.subscribe(this.onRevokeSucceed);
this.S3BucketCredentials = store.pipe(select(selectS3BucketCredentials));
this.S3BucketCredentials.pipe(skip(1)).subscribe(bucketEndpoint => {
this.s3BucketCredentials = store.pipe(select(selectS3BucketCredentials));
this.s3BucketCredentials.pipe(skip(1)).subscribe(() => {
this.previouslySignedUrls = {};
});
this.bucketCredentials = store.pipe(select(selectS3BucketCredentialsBucketCredentials));
@@ -47,30 +47,35 @@ export class BaseAdminService {
}
showS3PopUp(bucketKeyEndpoint, error = null, isAzure = false) {
delete this.S3Services[bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint];
delete this.s3Services[bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint];
const selectDontShowAgainBucketEndpoint = this.syncSelector.selectSync(selectDontShowAgainForBucketEndpoint);
if (selectDontShowAgainBucketEndpoint !== (bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint)) {
this.store.dispatch(showS3PopUp({payload: {credentials: bucketKeyEndpoint, credentialsError: error, isAzure}}));
}
return this.S3BucketCredentials;
return this.s3BucketCredentials;
}
showLocalFilePopUp(url) {
this.store.dispatch(showLocalFilePopUp({url}));
}
signUrlIfNeeded(url, skipLocalFile = true, skipFileServer = true) {
signUrlIfNeeded(url: string, config?: {skipLocalFile?: boolean; skipFileServer?: boolean; disableCache?: number}) {
config = {...{skipLocalFile: true, skipFileServer: true, disableCache: null}, ...config};
if (isFileserverUrl(url, window.location.hostname)) {
if (this.environment.communityServer) {
url = this.addTenant(url);
}
if (!skipFileServer && this.environment.useFilesProxy) {
if (!config.skipFileServer && this.environment.useFilesProxy) {
return convertToReverseProxy(url);
}
if (config.disableCache) {
url = this.addS3TimeStamp(url, config.disableCache);
}
return url;
}
if (this.isLocalFile(url) && !skipLocalFile) {
if (this.isLocalFile(url) && !config.skipLocalFile) {
this.checkLocalServerRunning(url);
return this.redirectToLocalServer(url);
}
@@ -97,11 +102,13 @@ export class BaseAdminService {
}
const s3 = this.findOrInitBucketS3(bucketKeyEndpoint);
if (s3) {
/* eslint-disable @typescript-eslint/naming-convention */
const bucketKey = {
Bucket: bucketKeyEndpoint.Bucket,
Key: bucketKeyEndpoint.Key,
Expires: 60 * 60 * 24 * 4
};
/* eslint-enable @typescript-eslint/naming-convention */
let signedURL = '';
try {
signedURL = s3.getSignedUrl('getObject', bucketKey);
@@ -110,10 +117,13 @@ export class BaseAdminService {
}
const timeToExperation = (new Date()).setDate(now.getDate() + 3);
this.previouslySignedUrls[url] = {
signedURL: signedURL,
signedURL,
expires: timeToExperation,
bucketKeyEndpoint: bucketKeyEndpoint
bucketKeyEndpoint
};
if(config.disableCache){
return this.addS3TimeStamp(signedURL, config.disableCache);
}
return signedURL;
} else {
delete bucketKeyEndpoint.Key;
@@ -133,13 +143,13 @@ export class BaseAdminService {
findOrInitBucketS3(bucketKeyEndpoint) {
const set = this.findS3CredentialsInStore(bucketKeyEndpoint);
const S3Service = this.S3Services[bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint];
if (set && S3Service) {
return S3Service;
const s3Service = this.s3Services[bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint];
if (set && s3Service) {
return s3Service;
} else {
if (set) {
this.S3Services[bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint] = this.createS3Service(set);
return this.S3Services[bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint];
this.s3Services[bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint] = this.createS3Service(set);
return this.s3Services[bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint];
} else {
return false;
}
@@ -160,31 +170,29 @@ export class BaseAdminService {
}
resetS3Services() {
this.S3Services = {};
this.s3Services = {};
}
public checkImgUrl(src) {
const bucketKeyEndpoint = src && this.getBucketAndKeyFromSrc(src);
if (bucketKeyEndpoint) {
delete this.S3Services[bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint];
const S3OldCredentials = this.syncSelector.selectSync(selectS3BucketCredentialsBucketCredentials)
delete this.s3Services[bucketKeyEndpoint.Bucket + bucketKeyEndpoint.Endpoint];
const s3OldCredentials = this.syncSelector.selectSync(selectS3BucketCredentialsBucketCredentials)
.filter(bucket => bucket.Bucket === bucketKeyEndpoint.Bucket && bucket.Endpoint === bucketKeyEndpoint.Endpoint);
if (S3OldCredentials[0]) {
this.showS3PopUp(S3OldCredentials[0], 'Error', this.isAzureUrl(src));
if (s3OldCredentials[0]) {
this.showS3PopUp(s3OldCredentials[0], 'Error', this.isAzureUrl(src));
}
}
}
public getAllCredentials() {
this.store.dispatch({
type: HTTP.API_REQUEST,
meta: {
this.store.dispatch(apiRequest({
method: 'GET',
endpoint: 'auth.get_credentials',
success: 'AUTH_GET_SUCCESS'
}
});
})
);
}
@@ -205,39 +213,45 @@ export class BaseAdminService {
}
public getBucketAndKeyFromSrc = (src) => {
let Key = '';
let key = '';
if (src && src.includes('azure://')) {
/* eslint-disable @typescript-eslint/naming-convention */
return {
Bucket: 'azure',
Endpoint: 'azure',
Key: 'azure',
Secret: Key
Secret: key
};
/* eslint-enable @typescript-eslint/naming-convention */
}
const src_arr = src.split('/');
const srcArr = src.split('/');
if (!this.isS3Url(src)) {
return null;
} else if (src_arr[2].includes(':')) {
src_arr.forEach((part, index) => {
} else if (srcArr[2].includes(':')) {
srcArr.forEach((part, index) => {
if (index > 3) {
Key += part + '/';
key += part + '/';
}
});
Key = Key.slice(0, -1);
key = key.slice(0, -1);
/* eslint-disable @typescript-eslint/naming-convention */
return {
Bucket: src_arr[3],
Endpoint: src_arr[2],
Key: Key,
Bucket: srcArr[3],
Endpoint: srcArr[2],
Key: key,
};
/* eslint-enable @typescript-eslint/naming-convention */
} else {
try {
src = this.encodeSpecialCharacters(src);
const amazon = AmazonS3URI(src);
/* eslint-disable @typescript-eslint/naming-convention */
return {
Bucket: amazon.bucket,
Key: amazon.key,
Endpoint: null
};
/* eslint-enable @typescript-eslint/naming-convention */
} catch (err) {
console.log(err);
return null;
@@ -268,7 +282,6 @@ export class BaseAdminService {
signGoogleCloudUrl(url: string): string {
return 'https://storage.cloud.google.com' + (url.slice(4));
}
signAzureUrl(url: string, azureBucket) {
@@ -307,6 +320,7 @@ export class BaseAdminService {
const bucketKeyEndpoint = url && this.getBucketAndKeyFromSrc(url);
const s3 = this.findOrInitBucketS3(bucketKeyEndpoint) as S3;
/* eslint-disable @typescript-eslint/naming-convention */
const req = {
Bucket: bucketKeyEndpoint.Bucket,
Delete: {
@@ -314,13 +328,14 @@ export class BaseAdminService {
Objects: files.map(file => ({Key: file} as S3.ObjectIdentifier))
}
} as S3.Types.DeleteObjectsRequest;
/* eslint-enable @typescript-eslint/naming-convention */
if (s3) {
s3.deleteObjects(req, (err, data) => {
s3.deleteObjects(req, (err) => {
if (err) {
this.deleteS3FilesSubject.next({success: false, files: files});
this.deleteS3FilesSubject.next({success: false, files});
} else {
this.deleteS3FilesSubject.next({success: true, files: files});
this.deleteS3FilesSubject.next({success: true, files});
}
});
} else {
@@ -330,4 +345,16 @@ export class BaseAdminService {
}
return !skipSubjectReturn && this.deleteS3FilesSubject.asObservable();
}
addS3TimeStamp(url: string, timestamp: number) {
timestamp = timestamp || new Date().getTime();
try {
const parsed = new URL(url);
parsed.searchParams.append('X-Amz-Date', `${timestamp}`);
return parsed.toString();
} catch {
return url;
}
}
}

View File

@@ -26,7 +26,7 @@
</div>
<div class="col-2 d-flex justify-content-end">
<span class="delete-button" (click)="removeBucket(i)" type="button" smTooltip="Remove">
<i class="fa" [ngClass]="ICONS.REMOVE"></i>
<i class="al-icon sm-md" [ngClass]="ICONS.REMOVE"></i>
</span>
</div>
</div>

View File

@@ -27,10 +27,8 @@
}
.delete-button {
width: 32px;
height: 32px;
text-align: center;
line-height: 32px;
display: flex;
align-items: center;
}
.add-button {

View File

@@ -2,7 +2,7 @@
@font-face {
font-family: '#{$icomoon-font-family}';
src: url('./#{$icomoon-font-family}.ttf?23s406') format('truetype');
src: url('./#{$icomoon-font-family}.ttf?ck7fvi') format('truetype');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -43,9 +43,9 @@
content: $al-ico-projects;
}
}
.al-ico-datasets1 {
.al-ico-datasets {
&:before {
content: $al-ico-datasets1;
content: $al-ico-datasets;
}
}
.al-ico-queues {
@@ -88,11 +88,6 @@
content: $al-ico-compare-c;
}
}
.al-ico-datasets {
&:before {
content: $al-ico-datasets;
}
}
.al-ico-published-title {
&:before {
content: $al-ico-published-title;
@@ -884,6 +879,82 @@
content: $al-ico-dequeue;
}
}
.al-ico-applications {
&:before {
content: $al-ico-applications;
}
}
.al-ico-ico-chevron-up {
&:before {
content: $al-ico-ico-chevron-up;
}
}
.al-ico-ico-chevron-down {
&:before {
content: $al-ico-ico-chevron-down;
}
}
.al-ico-no-data-graph {
&:before {
content: $al-ico-no-data-graph;
}
}
.al-ico-no-scatter-graph {
&:before {
content: $al-ico-no-scatter-graph;
}
}
.al-ico-applications-exp {
&:before {
content: $al-ico-applications-exp;
}
}
.al-ico-auto-refresh-play .path1 {
&:before {
content: $al-ico-auto-refresh-play-path1;
color: rgb(90, 101, 142);
}
}
.al-ico-auto-refresh-play .path2 {
&:before {
content: $al-ico-auto-refresh-play-path2;
margin-left: -1em;
color: rgb(211, 253, 0);
}
}
.al-ico-auto-refresh-pause .path1 {
&:before {
content: $al-ico-auto-refresh-pause-path1;
color: rgb(90, 101, 142);
}
}
.al-ico-auto-refresh-pause .path2 {
&:before {
content: $al-ico-auto-refresh-pause-path2;
margin-left: -1em;
color: rgb(211, 253, 0);
}
}
.al-ico-sqr-ok {
&:before {
content: $al-ico-sqr-ok;
}
}
.al-ico-sqr-cancel {
&:before {
content: $al-ico-sqr-cancel;
}
}
.al-ico-queue-lg {
&:before {
content: $al-ico-queue-lg;
}
}
.al-ico-started-lg {
&:before {
content: $al-ico-started-lg;
}
}
.al-ico-status-draft {
&:before {
content: $al-ico-status-draft;

View File

@@ -5,7 +5,7 @@ $al-ico-t-logo-b: "\e908";
$al-ico-bars-menu: "\e933";
$al-ico-home: "\e909";
$al-ico-projects: "\e90a";
$al-ico-datasets1: "\e90b";
$al-ico-datasets: "\e90b";
$al-ico-queues: "\e90c";
$al-ico-annotator: "\e90d";
$al-ico-account: "\e998";
@@ -14,7 +14,6 @@ $al-ico-archive: "\e92c";
$al-ico-logout: "\e9a6";
$al-ico-how-to: "\e997";
$al-ico-compare-c: "\e93a";
$al-ico-datasets: "\e944";
$al-ico-published-title: "\e9d2";
$al-ico-publish: "\e9cf";
$al-ico-unpublish: "\ea0f";
@@ -171,6 +170,20 @@ $al-ico-task-desc-outline: "\e970";
$al-ico-manage-queue: "\e968";
$al-ico-enqueue: "\e969";
$al-ico-dequeue: "\e971";
$al-ico-applications: "\e972";
$al-ico-ico-chevron-up: "\e973";
$al-ico-ico-chevron-down: "\e974";
$al-ico-no-data-graph: "\e975";
$al-ico-no-scatter-graph: "\e976";
$al-ico-applications-exp: "\e944";
$al-ico-auto-refresh-play-path1: "\e977";
$al-ico-auto-refresh-play-path2: "\e978";
$al-ico-auto-refresh-pause-path1: "\e979";
$al-ico-auto-refresh-pause-path2: "\e97a";
$al-ico-sqr-ok: "\e97b";
$al-ico-sqr-cancel: "\e97c";
$al-ico-queue-lg: "\e97d";
$al-ico-started-lg: "\e97e";
$al-ico-status-draft: "\e902";
$al-ico-status-pending: "\e903";
$al-ico-status-running: "\e904";
@@ -179,3 +192,4 @@ $al-ico-status-published: "\e906";
$al-ico-status-failed: "\e907";
$al-ico-status-aborted: "\e917";
$al-ico-status-aborted-sec: "\e918";

View File

@@ -36,7 +36,12 @@ $green-theme: mat-light-theme($custom-theme-primary, $custom-theme-accent, $cust
$dark-theme: mat-dark-theme($mat-allegro, $mat-allegro, $mat-allegro);
$light-theme: mat-light-theme($mat-allegro, $mat-allegro, $mat-allegro);
$sm-theme: mat-light-theme($sm-theme-primary, $sm-theme-accent, $sm-theme-warn);
.cdk-drag-preview.form-group-drag {
padding: 8px 16px 32px 16px;
border-radius: 4px;
border: solid 1px #d4d6e0;
background-color: white;
}
.dark-theme {
@include mat-slide-toggle-theme($green-theme);
@include mat-radio-theme($green-theme);
@@ -66,11 +71,21 @@ $sm-theme: mat-light-theme($sm-theme-primary, $sm-theme-accent, $sm-theme-warn);
@include mat-slide-toggle-theme($sm-theme);
@include mat-radio-theme($sm-theme);
@include mat-select-theme($light-theme);
.mat-checkbox-frame,
.mat-radio-outer-circle {
border-color: $purple;
}
.mat-radio-button.mat-accent.disabled, .mat-radio-button.mat-accent.disabled.mat-radio-checked {
.mat-radio-outer-circle, .mat-radio-inner-circle {
border-color: $blue-300;
}
.mat-radio-inner-circle{
background-color: $blue-300;
}
}
.light-theme .mat-radio-button.mat-radio-disabled .mat-radio-outer-circle,
.light-theme .mat-radio-button.mat-radio-disabled .mat-radio-label-content {
border-color: $blue-200 !important;
@@ -126,6 +141,11 @@ h5.al-header {
cursor: pointer;
}
.pointer-events-none {
pointer-events: none;
}
.grab {
cursor: grab;
}
@@ -141,8 +161,10 @@ h5.al-header {
background: #ff7272 !important;
}
}
mat-expansion-panel{
mat-expansion-panel {
box-shadow: unset;
.mat-expansion-panel-header {
margin-bottom: 0;
}
@@ -214,6 +236,12 @@ button {
.pointer {
cursor: pointer;
}
.grabbing{
cursor: grab;
&:active {
cursor: grabbing;
}
}
.flex-middle {
display: flex;
@@ -401,12 +429,21 @@ html {
max-width: none;
min-width: 114px;
min-height: 32px;
&.custom-columns {
width: 370px;
}
}
}
.sm-menu-header {
text-align: center;
padding: 15px 15px;
background: $blue-25;
color: $blue-400;
font-weight: 500;
}
.mat-menu-item {
font-size: 14px;
font-weight: 500;
@@ -459,10 +496,10 @@ as-split {
}
$type-colors: (
string: #ff8400,
number: $neon-yellow-betterinchrome,
boolean: #b938a4,
date: #05668D,
string: #ff8400,
number: $neon-yellow-betterinchrome,
boolean: #b938a4,
date: #05668D,
);
.dark-theme {
@@ -547,7 +584,6 @@ body .mat-menu-content:not(:empty) {
padding-bottom: 0px;
.search-results {
height: 200px;
overflow: auto;
max-width: 222px;
}
@@ -563,6 +599,7 @@ body .mat-menu-content:not(:empty) {
body .mat-menu-panel .mat-form-field.tags-menu-input {
.mat-form-field-wrapper {
padding: 0;
.mat-form-field-flex {
padding: 0 14px;
align-items: center;
@@ -621,7 +658,7 @@ button.btn.button-outline-dark {
.sm-card-list-layout {
display: grid;
grid-template-columns: repeat(auto-fit, 352px);
grid-gap: 24px;
grid-gap: 32px 24px;
padding: 0 24px 24px;
justify-content: center;
@@ -648,7 +685,7 @@ button.btn.button-outline-dark {
align-items: center;
font-size: 12px;
&>.menu {
& > .menu {
color: $blue-300;
font-weight: 500;
cursor: pointer;

View File

@@ -60,9 +60,33 @@ export const ICONS = {
DOWNLOAD: 'al-ico-download',
WORKER: 'al-ico-workers',
TAG: 'al-ico-tag',
SHARE: 'al-ico-shared-item'
SHARE: 'al-ico-shared-item',
ARROW_DOWN: 'al-ico-ico-chevron-down',
ARROW_UP: 'al-ico-ico-chevron-up',
};
export type IconNames = keyof typeof ICONS;
export type ObjectKeysRecord<T extends object> = { [P in keyof T]: P };
export type IconsList = ObjectKeysRecord<typeof ICONS>;
export type IconsValues = typeof ICONS[keyof typeof ICONS];
export const PALLET = {
blue25: '#f9fafb',
blue50: '#f2f4fc', //242,244,252
blue100: '#dce0ee', //220,224,238
blue200: '#c3cdf0', //195,205,240
blue250: '#A4ADCD',
blue280: '#a7b2d8',
blue300: '#8492c2', //132,146,19
blue400: '#5a658e', //90,101,142
blue450: '#657099',
blue480: '#707ba3',
blue500: '#384161', //56,65,97
blue550: '#47527A',
blue570: '#323a56',
blue600: '#2c3246', //44,50,70
blue650: '#24293c',
blue700: '#202432', //32,36,50
blue800: '#1a1e2c', //26,30,44
blue900: '#141722', //20,23,34
blue950: '#0d0e15', //20,23,34
};

View File

@@ -1,14 +1,14 @@
import {HTTP_ACTIONS} from '../../../app.constants';
import {ISmAction} from '../models/actions';
import {HTTP_PREFIX} from '../../../app.constants';
import {omit} from 'lodash/fp';
import {HttpErrorResponse} from '@angular/common/http';
import {createAction, props} from '@ngrx/store';
export class RequestFailed implements ISmAction {
public type = HTTP_ACTIONS.REQUEST_FAILED;
public payload: { err: any };
constructor(err: HttpErrorResponse) {
this.payload = { err: omit(['headers'], err) };
}
}
export const requestFailed = createAction(
HTTP_PREFIX + 'REQUEST_FAILED',
(err: HttpErrorResponse) => ({err: omit(['headers'], err)})
);
export const apiRequest = createAction(
HTTP_PREFIX + 'API Request',
props<{method: string; endpoint: string; success: string}>()
);

View File

@@ -1,119 +1,79 @@
import {MessageSeverityEnum, VIEW_ACTIONS, VIEW_PREFIX} from '../../../app.constants';
import {Action, createAction, props} from '@ngrx/store';
import {MessageSeverityEnum, VIEW_PREFIX} from '../../../app.constants';
import {createAction, props} from '@ngrx/store';
import {omit} from 'lodash/fp';
import {HttpErrorResponse} from '@angular/common/http';
export class SetAutoRefresh {
public type = VIEW_ACTIONS.SET_AUTO_REFRESH;
public payload: { autoRefresh: boolean };
export const setAutoRefresh = createAction(
VIEW_PREFIX + '[set auto refresh]',
props<{autoRefresh: boolean}>()
);
constructor(autoRefresh: boolean) {
this.payload = {autoRefresh};
}
}
export const setCompareAutoRefresh = createAction(
VIEW_PREFIX + '[set compare auto refresh]',
props<{autoRefresh: boolean}>()
);
export class SetCompareAutoRefresh {
public type = VIEW_ACTIONS.SET_COMPARE_AUTO_REFRESH;
public payload: { autoRefresh: boolean };
export const setServerError = createAction(
VIEW_PREFIX + '[set server error]',
(serverError: HttpErrorResponse, contextSubCode?: number, customMessage?: string, aggregateSimilar = false) => ({
serverError: omit(['headers'], serverError) as Omit<HttpErrorResponse, 'headers'>,
contextSubCode,
customMessage,
aggregateSimilar
})
);
constructor(autoRefresh: boolean) {
this.payload = {autoRefresh};
}
}
export const setNotificationDialog = createAction(
VIEW_PREFIX + '[set notification dialog]',
props<{message: string; title: string}>()
);
export class SetServerError {
public type = VIEW_ACTIONS.SET_SERVER_ERROR;
public payload: {
serverError: Omit<HttpErrorResponse, 'headers'>;
contextSubCode?: number;
customMessage?: string;
aggregateSimilar: boolean;
};
export const resetLoader = createAction(VIEW_PREFIX + '[reset loader]');
constructor(serverError: HttpErrorResponse, contextSubCode?: number, customMessage?: string, aggregateSimilar = false) {
this.payload = {
serverError: omit(['headers'], serverError) as Omit<HttpErrorResponse, 'headers'>,
contextSubCode,
customMessage,
aggregateSimilar};
}
}
export const setBackdrop = createAction(
VIEW_PREFIX + '[set backdrop]',
props<{payload: boolean}>()
);
export class SetNotificationDialog {
public type = VIEW_ACTIONS.SET_NOTIFICATION_DIALOG;
public payload: { message: string; title: string };
export const activeLoader = createAction(
VIEW_PREFIX + '[activate loader]',
(endpoint: string) => ({endpoint})
);
constructor(payload: { message: string; title: string }) {
this.payload = payload;
}
}
export const deactivateLoader = createAction(
VIEW_PREFIX + '[deactivate loader]',
(endpoint: string) => ({endpoint})
);
export class ResetLoader implements Action {
readonly type = VIEW_ACTIONS.RESET_LOADER;
constructor() {
}
}
export const visibilityChanged = createAction(
VIEW_PREFIX + '[visibility changed]',
props<{visible: boolean}>()
);
export class SetBackdrop implements Action {
readonly type = VIEW_ACTIONS.SET_BACKDROP;
export const addMessage = createAction(
VIEW_PREFIX + '[add message]',
(severity: MessageSeverityEnum, msg: string, userActions?: {actions: any[]; name: string}[]) =>
({severity, msg, userActions})
);
constructor(public payload: boolean) {
}
}
export class ActiveLoader {
type = VIEW_ACTIONS.ACTIVE_LOADER;
public payload: any;
constructor(endpoint = 'default') {
this.payload = {endpoint};
}
}
export class DeactiveLoader {
type = VIEW_ACTIONS.DEACTIVE_LOADER;
public payload: any;
constructor(endpoint = 'default') {
this.payload = {endpoint};
}
}
export class VisibilityChanged {
type = VIEW_ACTIONS.VISIBILITY_CHANGED;
constructor(public visible: boolean) {}
}
export class AddMessage implements Action {
readonly type = VIEW_ACTIONS.ADD_MESSAGE;
public payload: {
severity: string;
msg: string;
userActions?: {actions: any[]; name: string}[];
};
constructor(severity: MessageSeverityEnum, msg: string, userActions?: {actions: any[]; name: string}[]) {
this.payload = {severity, msg, userActions};
}
}
export const removeMessage = createAction(VIEW_PREFIX + '[remove message]');
export const setServerUpdatesAvailable = createAction(
VIEW_ACTIONS.SET_SERVER_UPDATES_AVAILABLE,
VIEW_PREFIX + '[set server updates available]',
props<{availableUpdates}>()
);
export const setScaleFactor = createAction(
VIEW_ACTIONS + '[set scale]',
VIEW_PREFIX + '[set scale]',
props<{scale: number}>()
);
export const firstLogin = createAction(
VIEW_ACTIONS + '[set first Login]',
VIEW_PREFIX + '[set first Login]',
props<{first: boolean}>()
);
export const neverShowPopupAgain = createAction(VIEW_PREFIX + 'NEVER_SHOW_POPUP_AGAIN', props<{ popupId: string; reset?: boolean }>());
export const plotlyReady = createAction(VIEW_ACTIONS + '[plotly ready]');
export const plotlyReady = createAction(VIEW_PREFIX + '[plotly ready]');

View File

@@ -12,11 +12,12 @@ import {TasksArchiveManyResponse} from '../../../business-logic/model/tasks/task
import {TasksPublishManyResponse} from '../../../business-logic/model/tasks/tasksPublishManyResponse';
import {TasksStopManyResponse} from '../../../business-logic/model/tasks/tasksStopManyResponse';
import {EntityTypeEnum} from '../../../shared/constants/non-common-consts';
import {MetricColumn} from '@common/shared/utils/tableParamEncode';
import {ProjectStatsGraphData} from '@common/core/reducers/projects.reducer';
export const PROJECTS_PREFIX = '[ROOT_PROJECTS] ';
export const GET_PROJECTS = PROJECTS_PREFIX + 'GET_PROJECTS';
export const SET_PROJECTS = PROJECTS_PREFIX + 'SET_PROJECTS';
export const SET_SELECTED_PROJECT = PROJECTS_PREFIX + 'SET_SELECTED_PROJECT';
export const SET_SELECTED_PROJECT_ID = PROJECTS_PREFIX + 'SET_SELECTED_PROJECT_ID';
export const RESET_SELECTED_PROJECT = PROJECTS_PREFIX + 'RESET_SELECTED_PROJECT';
export const RESET_PROJECT_SELECTION = PROJECTS_PREFIX + 'RESET_PROJECT_SELECTION';
@@ -28,7 +29,7 @@ export interface TagColor {
background: string;
}
export class GetAllProjects implements ISmAction {
export class GetAllSystemProjects implements ISmAction {
readonly type = GET_PROJECTS;
}
@@ -59,14 +60,10 @@ export class SetSelectedProjectId implements ISmAction {
}
}
export class SetSelectedProject implements ISmAction {
readonly type = SET_SELECTED_PROJECT;
public payload: { project: Project };
constructor(project: Project) {
this.payload = {project};
}
}
export const setSelectedProject = createAction(
PROJECTS_PREFIX + 'SET_SELECTED_PROJECT',
props<{project: Project}>()
);
export class ResetSelectedProject implements ISmAction {
readonly type = RESET_SELECTED_PROJECT;
@@ -103,3 +100,15 @@ export const openMoreInfoPopup = createAction(
PROJECTS_PREFIX + '[open more info popup]',
props<{ parentAction: ReturnType<typeof archivedSelectedModels>; operationName: string; entityType: EntityTypeEnum; res: ModelsPublishManyResponse | ModelsArchiveManyResponse | ModelsDeleteManyResponse | TasksResetManyResponse | TasksEnqueueManyResponse | TasksArchiveManyResponse | TasksPublishManyResponse | TasksStopManyResponse }>()
);
export const setMetricVariant = createAction(
PROJECTS_PREFIX + '[set selected metric variant for graph]',
props<{projectId: string; col: MetricColumn}>()
);
export const fetchGraphData = createAction(PROJECTS_PREFIX + '[fetch stats for project graph]');
export const setGraphData = createAction(
PROJECTS_PREFIX + '[set project stats]',
props<{stats: ProjectStatsGraphData[]}>()
);

View File

@@ -1,7 +1,6 @@
import {TASKS_ACTIONS} from '../../../app.constants';
import {ISmAction} from '../models/actions';
import {Action} from '@ngrx/store';
import {Task} from '../../../business-logic/model/tasks/task';
export class StopTask implements ISmAction {
type = TASKS_ACTIONS.STOP_TASK;
@@ -23,58 +22,3 @@ export class SetTaskInListAndInSelectedTask implements Action {
};
}
}
// new actions
export class SetTasksData implements ISmAction {
type = TASKS_ACTIONS.SET_TASKS_DATA;
public payload: { tasksData: Array<Task> };
constructor(tasksData: Array<Task>) {
this.payload = {tasksData};
}
}
export class GetTaskForMetricsPage implements ISmAction {
public type = TASKS_ACTIONS.GET_TASK_FOR_METRICS;
public payload: { taskId: string };
constructor(taskId: string) {
this.payload = {taskId};
}
}
export class SetSelectedTask implements ISmAction {
public type = TASKS_ACTIONS.SET_SELECTED_TASK;
public payload: { task: Task };
constructor(task: Task) {
this.payload = {task};
}
}
export class SetTaskInTable implements ISmAction {
public type = TASKS_ACTIONS.SET_TASK_IN_TABLE;
public payload: { task: Task };
constructor(task: Task) {
this.payload = {task};
}
}
export class SetTaskMetric implements ISmAction {
public type = TASKS_ACTIONS.SET_TASK_FOR_METRICS;
public payload: { task: Task };
constructor(task: Task) {
this.payload = {task};
}
}
export class UpdateTask implements ISmAction {
public type = TASKS_ACTIONS.UPDATE_TASK;
public payload: { id: string; changes: Partial<Task> };
constructor(id: string, changes: Partial<Task>) {
this.payload = {id, changes};
}
}

View File

@@ -1,88 +1,61 @@
import {Action, createAction, props} from '@ngrx/store';
import {USERS_ACTIONS, USERS_PREFIX} from '../../../app.constants';
import {User} from '../../../business-logic/model/users/user';
import {OrganizationCreateInviteResponse} from "../../../business-logic/model/organization/organizationCreateInviteResponse";
import {createAction, props} from '@ngrx/store';
import {USERS_PREFIX} from '../../../app.constants';
import {OrganizationCreateInviteResponse} from '../../../business-logic/model/organization/organizationCreateInviteResponse';
import {GetCurrentUserResponseUserObjectCompany} from '../../../business-logic/model/users/getCurrentUserResponseUserObjectCompany';
import {LoginGetSettingsResponse} from "../../../business-logic/model/login/loginGetSettingsResponse";
export const fetchCurrentUser = createAction(USERS_PREFIX +'FETCH_USER');
export class FetchCurrentUser implements Action {
type = USERS_ACTIONS.FETCH_CURRENT_USER;
}
export const setCurrentUser = createAction(
USERS_ACTIONS.SET_CURRENT_USER,
props<{user: User; terms_of_use?: any}>()
export const setCurrentUserName = createAction(USERS_PREFIX + 'SET_CURRENT_USER_NAME',
props<{name: string}>()
);
export const termsOfUseAccepted = createAction(USERS_PREFIX + '[TOS accepted]');
export const logout = createAction(USERS_ACTIONS.LOGOUT, props<{provider?: string}>());
export const logoutSuccess = createAction(USERS_ACTIONS.LOGOUT_SUCCESS);
export class SetPreferences implements Action {
type = USERS_ACTIONS.SET_PREF;
constructor(public payload) {}
}
export const logout = createAction(USERS_PREFIX + 'LOGOUT', props<{provider?: string}>());
export const logoutSuccess = createAction(USERS_PREFIX + 'LOGOUT_SUCCESS');
export const setPreferences = createAction(USERS_PREFIX +'SET_PREF', props<{payload: any}>());
export const getInviteUserLink = createAction(USERS_PREFIX +'GET_INVITE_USER_LINK');
export const setInviteUserLink = createAction(USERS_PREFIX +'SET_INVITE_USER_LINK',
props<OrganizationCreateInviteResponse>()
);
export const addWorkspace = createAction(
'[login] add workspace',
props<{inviteId: string}>()
);
export const addWorkspace = createAction(USERS_PREFIX + ' add workspace', props<{inviteId: string}>());
export const setFilterByUser = createAction(USERS_PREFIX +'SET_FILTERED_BY_USER', props<{showOnlyUserWork: boolean}>());
export const leaveWorkspace = createAction(
'[login] leave workspace',
USERS_PREFIX + ' leave workspace',
props<{workspace: GetCurrentUserResponseUserObjectCompany}>()
);
export const setWorkspace = createAction(
'[users] set workspace',
USERS_PREFIX + ' set workspace',
props<{workspace: GetCurrentUserResponseUserObjectCompany}>()
);
export const setActiveWorkspace = createAction(
'[users] set active workspace',
USERS_PREFIX + ' set active workspace',
props<{workspace: GetCurrentUserResponseUserObjectCompany}>()
);
export const setSelectedWorkspaceTab = createAction(
'[users] set selected workspace tab',
USERS_PREFIX + ' set selected workspace tab',
props<{workspace: GetCurrentUserResponseUserObjectCompany}>()
);
export const removeWorkspace = createAction(
'[users] set selected workspace',
USERS_PREFIX + ' set selected workspace',
props<{workspaceId: string}>()
);
export const getUserWorkspaces = createAction('[login] get user workspaces');
export const getUserWorkspaces = createAction(USERS_PREFIX + ' get user workspaces');
export const setUserWorkspaces = createAction(
'[users] set user workspaces',
USERS_PREFIX + ' set user workspaces',
props<{workspaces: any[]}>()
);
export const setUserWorkspacesFromUser = createAction('[users] set user workspaces from current user');
export const setUserWorkspacesFromUser = createAction(USERS_PREFIX + ' set user workspaces from current user');
export const setAccountAdministrationPage = createAction(`${USERS_PREFIX} route to account-administration` );
export const getWhitelistEntries = createAction(`${USERS_PREFIX} get invited users`);
export const setWhitelistEntries = createAction(`${USERS_PREFIX} set invited users`,
props<{whitelistEntries: LoginGetSettingsResponse }>());
export const removeWhitelistEntry = createAction(`${USERS_PREFIX} remove invited user`,
props<{remove: string[]}>());
export const deleteWhitelistEntry = createAction(`${USERS_PREFIX} delete invited user`,
props<{user: User}>());
export const addWhitelistEntries = createAction(`${USERS_PREFIX} add invited user`,
props<{whitelistEntries: string[]}>());
export const setAddWhitelistEntries = createAction(`${USERS_PREFIX} set add invited user`,
props<{whitelistEntries: string[]}>());
export const setRemoveWhitelistEntry = createAction(`${USERS_PREFIX} set remove invited user`,
props<{removed: string[]}>());
export const getApiVersion = createAction(`${USERS_PREFIX} get api version` );
export const setApiVersion = createAction(`${USERS_PREFIX} set api version`, props<{serverVersions: {server: string; api: string}}>());

View File

@@ -2,8 +2,8 @@ import {Injectable} from '@angular/core';
import {act, Actions, Effect, ofType} from '@ngrx/effects';
import {ApiAuthService} from '../../../business-logic/api-services/auth.service';
import * as authActions from '../actions/common-auth.actions';
import {RequestFailed} from '../actions/http.actions';
import {ActiveLoader, DeactiveLoader, SetServerError} from '../actions/layout.actions';
import {requestFailed} from '../actions/http.actions';
import {activeLoader, deactivateLoader, setServerError} from '../actions/layout.actions';
import {catchError, map, mergeMap, switchMap, withLatestFrom} from 'rxjs/operators';
import {AuthGetCredentialsResponse} from '../../../business-logic/model/auth/authGetCredentialsResponse';
import {Store} from '@ngrx/store';
@@ -19,7 +19,7 @@ export class CommonAuthEffectsService {
@Effect()
activeLoader = this.actions.pipe(
ofType(authActions.getAllCredentials, authActions.createCredential),
map(action => new ActiveLoader(action.type))
map(action => activeLoader(action.type))
);
@Effect()
@@ -29,9 +29,9 @@ export class CommonAuthEffectsService {
withLatestFrom(this.store.select(selectCurrentUser)),
mergeMap(([res, user]: [AuthGetCredentialsResponse, GetCurrentUserResponseUserObject]) => [
authActions.updateAllCredentials({credentials: res.credentials, extra: res.additional_credentials, workspace: user.company.id}),
new DeactiveLoader(action.type)
deactivateLoader(action.type)
]),
catchError(error => [new RequestFailed(error), new DeactiveLoader(action.type)])
catchError(error => [requestFailed(error), deactivateLoader(action.type)])
))
);
@@ -39,11 +39,11 @@ export class CommonAuthEffectsService {
revokeCredential = this.actions.pipe(
ofType(authActions.credentialRevoked),
mergeMap(action => this.credentialsApi.authRevokeCredentials({access_key: action.accessKey}).pipe(
mergeMap(() => [authActions.removeCredential(action), new DeactiveLoader(action.type)]),
mergeMap(() => [authActions.removeCredential(action), deactivateLoader(action.type)]),
catchError(error => [
new RequestFailed(error),
new DeactiveLoader(action.type),
new SetServerError(error, null, 'Can\'t delete this credentials.')
requestFailed(error),
deactivateLoader(action.type),
setServerError(error, null, 'Can\'t delete this credentials.')
])
))
);
@@ -54,12 +54,12 @@ export class CommonAuthEffectsService {
mergeMap(action => this.credentialsApi.authCreateCredentials({}).pipe(
mergeMap(res => [
authActions.addCredential({newCredential: res.credentials, workspaceId: action.workspaceId}),
new DeactiveLoader(action.type)]),
deactivateLoader(action.type)]),
catchError(error => [
new RequestFailed(error),
new SetServerError(error, null, 'Unable to create credentials'),
requestFailed(error),
setServerError(error, null, 'Unable to create credentials'),
authActions.addCredential({newCredential: {}, workspaceId: action.workspaceId}),
new DeactiveLoader(action.type)])
deactivateLoader(action.type)])
))
);
}

View File

@@ -1,6 +1,6 @@
import {Injectable} from '@angular/core';
import {Actions, Effect, ofType} from '@ngrx/effects';
import {EmptyAction, VIEW_ACTIONS} from '../../../app.constants';
import {EmptyAction} from '../../../app.constants';
import * as layoutActions from '../actions/layout.actions';
import {filter, map, switchMap, take, mergeMap} from 'rxjs/operators';
import {get} from 'lodash/fp';
@@ -28,9 +28,9 @@ export class LayoutEffects {
) {}
@Effect({dispatch: false})
serverErrorMoreInfo = this.actions.pipe(ofType<layoutActions.SetServerError>(VIEW_ACTIONS.SET_SERVER_ERROR),
filter(action => !!action.payload.serverError),
map(action => this.parseError(get('error.meta.result_msg', action.payload.serverError))),
serverErrorMoreInfo = this.actions.pipe(ofType(layoutActions.setServerError),
filter(action => !!action.serverError),
map(action => this.parseError(get('error.meta.result_msg', action.serverError))),
filter(([ids]) => !!ids),
switchMap(([ids, entity]) => this.getAllEntity(ids, entity).pipe(
filter(res => !!res),
@@ -45,15 +45,15 @@ export class LayoutEffects {
@Effect({dispatch: false})
popupError = this.actions.pipe(
ofType<layoutActions.SetServerError>(VIEW_ACTIONS.SET_SERVER_ERROR),
filter(action => action.payload.serverError?.status !== 401),
ofType(layoutActions.setServerError),
filter(action => action.serverError?.status !== 401),
map((action) => {
if (action.payload?.serverError?.status === 502) {
console.log('Gateway Error', action.payload.serverError);
if (action.serverError?.status === 502) {
console.log('Gateway Error', action.serverError);
return;
}
const customMessage: string = action.payload.customMessage;
if (action.payload.aggregateSimilar) {
const customMessage: string = action.customMessage;
if (action.aggregateSimilar) {
const lastTS = this.errors[customMessage];
const now = (new Date()).getTime();
if (lastTS && lastTS + ERROR_AGGREGATION > now) {
@@ -62,9 +62,9 @@ export class LayoutEffects {
this.errors[customMessage] = now;
}
let resultMessage: string;
const subcode = get('error.meta.result_subcode', action.payload.serverError);
const subcode = get('error.meta.result_subcode', action.serverError);
if (subcode) {
resultMessage = `Error ${subcode} : ${get('error.meta.result_msg', action.payload.serverError)}`;
resultMessage = `Error ${subcode} : ${get('error.meta.result_msg', action.serverError)}`;
}
this.alertDialogRef = this.dialog.open(AlertDialogComponent, {
data: {alertMessage: 'Error', alertSubMessage: customMessage, resultMessage}
@@ -77,8 +77,7 @@ export class LayoutEffects {
@Effect()
addMessage: Observable<any> = this.actions.pipe(
ofType<layoutActions.AddMessage>(VIEW_ACTIONS.ADD_MESSAGE),
map((action: layoutActions.AddMessage) => action.payload),
ofType(layoutActions.addMessage),
mergeMap(payload => this.notifierService.show({type: payload.severity, message: payload.msg, actions: payload.userActions})),
mergeMap(actions => actions.length > 0? actions : [new EmptyAction()])
);

View File

@@ -1,23 +1,29 @@
import {Injectable} from '@angular/core';
import {Store} from '@ngrx/store';
import {act, Actions, createEffect, Effect, ofType} from '@ngrx/effects';
import {Actions, createEffect, Effect, ofType} from '@ngrx/effects';
import {ApiProjectsService} from '../../../business-logic/api-services/projects.service';
import * as actions from '../actions/projects.actions';
import {
ResetSelectedProject,
SetSelectedProjectId,
UpdateProject,
ResetProjectSelection,
fetchGraphData,
GetAllSystemProjects,
getCompanyTags,
getTags,
openMoreInfoPopup,
openTagColorsMenu,
RESET_PROJECT_SELECTION,
setTags, openTagColorsMenu, getTags, setCompanyTags, getCompanyTags, openMoreInfoPopup
ResetProjectSelection,
ResetSelectedProject,
setCompanyTags,
setGraphData,
SetSelectedProjectId,
setTags,
UpdateProject
} from '../actions/projects.actions';
import {GetAllProjects} from '../actions/projects.actions';
import {catchError, filter, finalize, map, mergeMap, switchMap, withLatestFrom} from 'rxjs/operators';
import {RequestFailed} from '../actions/http.actions';
import {ActiveLoader, DeactiveLoader, SetServerError} from '../actions/layout.actions';
import {SetSelectedExperiments} from '../../experiments/actions/common-experiments-view.actions';
import {SetSelectedModels} from '../../models/actions/models-view.actions';
import {requestFailed} from '../actions/http.actions';
import {activeLoader, deactivateLoader, setServerError} from '../actions/layout.actions';
import {setSelectedModels} from '../../models/actions/models-view.actions';
import {TagColorMenuComponent} from '../../shared/ui-components/tags/tag-color-menu/tag-color-menu.component';
import {MatDialog} from '@angular/material/dialog';
import {ApiOrganizationService} from '../../../business-logic/api-services/organization.service';
@@ -26,9 +32,15 @@ import {selectRouterParams} from '../reducers/router-reducer';
import {forkJoin} from 'rxjs';
import {ProjectsGetTaskTagsResponse} from '../../../business-logic/model/projects/projectsGetTaskTagsResponse';
import {ProjectsGetModelTagsResponse} from '../../../business-logic/model/projects/projectsGetModelTagsResponse';
import {selectSelectedProjectId} from '../reducers/projects.reducer';
import {selectSelectedMetricVariantForCurrProject, selectSelectedProjectId} from '../reducers/projects.reducer';
import {OperationErrorDialogComponent} from '@common/shared/ui-components/overlay/operation-error-dialog/operation-error-dialog.component';
import {EmptyAction} from '../../../app.constants';
import {ApiTasksService} from '../../../business-logic/api-services/tasks.service';
import {createMetricColumn} from '@common/shared/utils/tableParamEncode';
import {get} from 'lodash/fp';
import {ITask} from '../../../business-logic/model/al-task';
import {TasksGetAllExRequest} from '../../../business-logic/model/tasks/tasksGetAllExRequest';
import {setSelectedExperiments} from '../../experiments/actions/common-experiments-view.actions';
import {selectShowHidden} from "@common/projects/common-projects.reducer";
const ALL_PROJECTS_OBJECT = {id: '*', name: 'All Experiments'};
@@ -38,7 +50,7 @@ export class ProjectsEffects {
constructor(
private actions$: Actions, private projectsApi: ApiProjectsService, private orgApi: ApiOrganizationService,
private store: Store<any>, private dialog: MatDialog
private store: Store<any>, private dialog: MatDialog, private tasksApi: ApiTasksService
) {
}
@@ -46,28 +58,29 @@ export class ProjectsEffects {
activeLoader = this.actions$.pipe(
ofType(actions.SET_SELECTED_PROJECT_ID),
filter((action: any) => !!action.payload?.projectId),
map(action => new ActiveLoader(action.type))
map(action => activeLoader(action.type))
);
@Effect()
getProjects$ = this.actions$.pipe(
ofType<GetAllProjects>(actions.GET_PROJECTS),
switchMap(() =>
this.projectsApi.projectsGetAllEx({only_fields: ['name', 'company', 'parent']})
ofType<GetAllSystemProjects>(actions.GET_PROJECTS),
withLatestFrom(this.store.select(selectShowHidden)),
switchMap(([action, showHidden]) =>
this.projectsApi.projectsGetAllEx({only_fields: ['name', 'company', 'parent'], search_hidden: showHidden})
.pipe(map(res => new actions.SetAllProjects(res.projects)))
)
);
@Effect()
ResetProjects$ = this.actions$.pipe(
resetProjects$ = this.actions$.pipe(
ofType<ResetSelectedProject>(actions.RESET_SELECTED_PROJECT),
mergeMap(() => [new ResetProjectSelection()])
);
@Effect()
ResetProjectSelections$ = this.actions$.pipe(
resetProjectSelections$ = this.actions$.pipe(
ofType<ResetProjectSelection>(RESET_PROJECT_SELECTION),
mergeMap(() => [new SetSelectedExperiments([]), new SetSelectedModels([])])
mergeMap(() => [setSelectedExperiments({experiments: []}), setSelectedModels({models: []})])
);
@Effect()
@@ -76,12 +89,12 @@ export class ProjectsEffects {
switchMap((action) =>
this.projectsApi.projectsUpdate({project: action.payload.id, ...action.payload.changes})
.pipe(
mergeMap((res) => [
mergeMap(() => [
new actions.UpdateProjectCompleted()
]),
catchError(err => [
new RequestFailed(err),
new SetServerError(err, null, 'Update project failed'),
requestFailed(err),
setServerError(err, null, 'Update project failed'),
new actions.SetSelectedProjectId(action.payload.id)
])
)
@@ -92,16 +105,16 @@ export class ProjectsEffects {
ofType<SetSelectedProjectId>(actions.SET_SELECTED_PROJECT_ID),
withLatestFrom(this.store.select(selectSelectedProjectId)),
switchMap(([action, selectedProjectId]) => {
if(!action.payload.projectId){
return [new actions.SetSelectedProject(null)];
if (!action.payload.projectId) {
return [actions.setSelectedProject({project: null})];
}
if (action.payload.projectId === selectedProjectId) {
return [new DeactiveLoader(action.type)];
return [deactivateLoader(action.type)];
}
if (action.payload.projectId === '*') {
return [
new actions.SetSelectedProject(ALL_PROJECTS_OBJECT),
new DeactiveLoader(action.type)];
actions.setSelectedProject({project: ALL_PROJECTS_OBJECT}),
deactivateLoader(action.type)];
} else {
this.fetchingExampleExperiment = action.payload.example && action.payload.projectId;
return this.projectsApi.projectsGetAllEx({
@@ -112,13 +125,13 @@ export class ProjectsEffects {
.pipe(
finalize(() => this.fetchingExampleExperiment = null),
mergeMap(res => [
new actions.SetSelectedProject(res.projects[0]),
new DeactiveLoader(action.type),
]
actions.setSelectedProject({project: res.projects[0]}),
deactivateLoader(action.type),
]
),
catchError(error => [
new RequestFailed(error),
new DeactiveLoader(action.type)
requestFailed(error),
deactivateLoader(action.type)
])
);
}
@@ -128,7 +141,7 @@ export class ProjectsEffects {
@Effect({dispatch: false})
openTagColor = this.actions$.pipe(
ofType(openTagColorsMenu),
map(action => {
map(() => {
this.dialog.open(TagColorMenuComponent);
})
);
@@ -139,7 +152,7 @@ export class ProjectsEffects {
switchMap(() => this.orgApi.organizationGetTags({include_system: true})
.pipe(
map((res: OrganizationGetTagsResponse) => setCompanyTags({tags: res.tags, systemTags: res.system_tags})),
catchError(error => [new RequestFailed(error)])
catchError(error => [requestFailed(error)])
)
)
);
@@ -157,12 +170,12 @@ export class ProjectsEffects {
Array.from(new Set(res[0].tags.concat(res[1].tags))).sort()),
mergeMap((tags: string[]) => [
setTags({tags}),
new DeactiveLoader(action.type)
deactivateLoader(action.type)
]),
catchError(error => [
new RequestFailed(error),
new DeactiveLoader(action.type),
new SetServerError(error, null, 'Fetch tags failed')]
requestFailed(error),
deactivateLoader(action.type),
setServerError(error, null, 'Fetch tags failed')]
)
))
);
@@ -170,15 +183,54 @@ export class ProjectsEffects {
openMoreInfoPopupEffect = createEffect(() => this.actions$.pipe(
ofType(openMoreInfoPopup),
switchMap(action => this.dialog.open(OperationErrorDialogComponent, {
data: {
title: `${action.operationName} ${action.entityType}`,
action,
iconClass: `d-block al-ico-${action.operationName} al-icon w-auto`,
}
}).afterClosed()
data: {
title: `${action.operationName} ${action.entityType}`,
action,
iconClass: `d-block al-ico-${action.operationName} al-icon w-auto`,
}
}).afterClosed()
)
), {dispatch: false});
fetchProjectStats = createEffect(() => this.actions$.pipe(
ofType(fetchGraphData),
withLatestFrom(
this.store.select(selectSelectedProjectId),
this.store.select(selectSelectedMetricVariantForCurrProject)
),
filter(([, , variant]) => !!variant),
switchMap(([, projectId, variant]) => {
const col = createMetricColumn(variant, projectId);
return this.tasksApi.tasksGetAllEx({
project: [projectId],
only_fields: ['started', 'last_iteration', 'user.name', 'type', 'name', 'status', 'active_duration', col.id],
[col.id]: [0, null],
started: ['2000-01-01T00:00:00', null],
status: ['completed', 'published', 'failed', 'stopped', 'closed'],
order_by: ['-started'],
type: ['__$not', 'annotation_manual', '__$not', 'annotation', '__$not', 'dataset_import'],
system_tags: ['-archived'],
page_size: 1000
} as unknown as TasksGetAllExRequest).pipe(
map((res) =>
setGraphData({
stats: res.tasks.map((task: ITask) => {
const started = new Date(task.started).getTime();
const end = started + (task.active_duration ?? 0) * 1000;
return {
id: task.id,
y: get(col.id, task),
x: end,
name: task.name,
status: task.status,
type: task.type,
user: task.user.name,
};
})
}))
);
})
));
}

View File

@@ -1,45 +1,27 @@
import {Injectable} from '@angular/core';
import {Router} from '@angular/router';
import {Actions, createEffect, Effect, ofType} from '@ngrx/effects';
import {ActionType, Store} from '@ngrx/store';
import {MESSAGES_SEVERITY, USERS_ACTIONS} from '../../../app.constants';
import {ApiUsersService} from '../../../business-logic/api-services/users.service';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {Store} from '@ngrx/store';
import {MESSAGES_SEVERITY} from '~/app.constants';
import {ApiUsersService} from '~/business-logic/api-services/users.service';
import {
addWhitelistEntries,
FetchCurrentUser,
getWhitelistEntries,
getInviteUserLink,
getUserWorkspaces,
leaveWorkspace,
logout,
removeWorkspace,
setActiveWorkspace,
setCurrentUser,
setAddWhitelistEntries,
setWhitelistEntries,
setInviteUserLink,
setUserWorkspaces,
setUserWorkspacesFromUser,
removeWhitelistEntry,
setRemoveWhitelistEntry,
deleteWhitelistEntry,
setAccountAdministrationPage, logoutSuccess
fetchCurrentUser, getApiVersion, getInviteUserLink, getUserWorkspaces, leaveWorkspace, logout, logoutSuccess, removeWorkspace, setAccountAdministrationPage, setActiveWorkspace, setApiVersion,
setInviteUserLink, setUserWorkspaces, setUserWorkspacesFromUser
} from '../actions/users.actions';
import {catchError, map, mergeMap, switchMap, withLatestFrom} from 'rxjs/operators';
import {RequestFailed} from '../actions/http.actions';
import {ApiAuthService} from '../../../business-logic/api-services/auth.service';
import {ApiOrganizationService} from '../../../business-logic/api-services/organization.service';
import {ActiveLoader, AddMessage, DeactiveLoader, SetServerError} from '../actions/layout.actions';
import {OrganizationCreateInviteResponse} from '../../../business-logic/model/organization/organizationCreateInviteResponse';
import {ApiLoginService} from '../../../business-logic/api-services/login.service';
import {OrganizationGetUserCompaniesResponse} from '../../../business-logic/model/organization/organizationGetUserCompaniesResponse';
import {requestFailed} from '../actions/http.actions';
import {ApiAuthService} from '~/business-logic/api-services/auth.service';
import {ApiOrganizationService} from '~/business-logic/api-services/organization.service';
import {addMessage, deactivateLoader, setServerError} from '../actions/layout.actions';
import {OrganizationCreateInviteResponse} from '~/business-logic/model/organization/organizationCreateInviteResponse';
import {ApiLoginService} from '~/business-logic/api-services/login.service';
import {OrganizationGetUserCompaniesResponse} from '~/business-logic/model/organization/organizationGetUserCompaniesResponse';
import {selectRouterUrl} from '../reducers/router-reducer';
import {LoginLogoutResponse} from '../../../business-logic/model/login/loginLogoutResponse';
import {LoginLogoutResponse} from '~/business-logic/model/login/loginLogoutResponse';
import {ErrorService} from '../../shared/services/error.service';
import {AuthDeleteUserResponse} from '../../../business-logic/model/auth/authDeleteUserResponse';
import {selectCurrentUser} from '../reducers/users-reducer';
import {LoginRemoveWhitelistEntriesResponse} from '../../../business-logic/model/login/loginRemoveWhitelistEntriesResponse';
import {LoginAddWhitelistEntriesResponse} from '../../../business-logic/model/login/loginAddWhitelistEntriesResponse';
import {ApiServerService} from '~/business-logic/api-services/server.service';
import {ServerInfoResponse} from '~/business-logic/model/server/serverInfoResponse';
import {setCurrentUser} from '~/core/actions/users.action';
@Injectable()
@@ -47,25 +29,23 @@ export class CommonUserEffects {
constructor(
private actions: Actions, private userService: ApiUsersService, private organizationService: ApiOrganizationService,
private router: Router, private authService: ApiAuthService, private loginApi: ApiLoginService, private store: Store<any>,
private errorService: ErrorService
private router: Router, private authService: ApiAuthService, private loginApi: ApiLoginService, private serverService: ApiServerService,
private store: Store<any>, private errorService: ErrorService
) {
}
@Effect()
fetchUser$ = this.actions.pipe(
ofType<FetchCurrentUser>(USERS_ACTIONS.FETCH_CURRENT_USER),
mergeMap(() => this.userService.usersGetCurrentUser({})
fetchUser$ = createEffect(() => this.actions.pipe(
ofType(fetchCurrentUser),
mergeMap(() => this.userService.usersGetCurrentUser({get_supported_features: true})
.pipe(
switchMap((res) => [setCurrentUser(res)]),
catchError(error => [new RequestFailed(error)])
catchError(error => [requestFailed(error)])
)
)
);
));
@Effect()
getUserInviteLink$ = this.actions.pipe(
ofType(getInviteUserLink.type),
getUserInviteLink$ = createEffect( () => this.actions.pipe(
ofType(getInviteUserLink),
switchMap((action) =>
this.organizationService.organizationCreateInvite({})
.pipe(
@@ -73,16 +53,15 @@ export class CommonUserEffects {
setInviteUserLink(res)
]),
catchError(error => [
new RequestFailed(error),
new DeactiveLoader(action.type),
new SetServerError(error, null, 'Fetch invite link failed')
requestFailed(error),
deactivateLoader(action.type),
setServerError(error, null, 'Fetch invite link failed')
])
)
)
);
));
@Effect()
logoutFlow = this.actions.pipe(
logoutFlow = createEffect( () => this.actions.pipe(
ofType(logout),
mergeMap(action => this.loginApi.loginLogout({
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -97,10 +76,10 @@ export class CommonUserEffects {
}
return logoutSuccess();
}),
catchError(err => [new AddMessage(MESSAGES_SEVERITY.ERROR, `Logout Failed
catchError(err => [addMessage(MESSAGES_SEVERITY.ERROR, `Logout Failed
${this.errorService.getErrorMsg(err?.error)}`)])
)),
);
));
setUserManagementPage = createEffect(() =>
this.actions.pipe(
@@ -117,7 +96,7 @@ ${this.errorService.getErrorMsg(err?.error)}`)])
// eslint-disable-next-line @typescript-eslint/naming-convention
mergeMap((action) => this.loginApi.loginLeaveCompany({company_id: action.workspace.id}).pipe(
mergeMap(() => [removeWorkspace({workspaceId: action.workspace.id})]),
catchError(() => [new AddMessage(MESSAGES_SEVERITY.ERROR, 'Failed to Leave Workspace')])
catchError(() => [addMessage(MESSAGES_SEVERITY.ERROR, 'Failed to Leave Workspace')])
)),
));
@@ -140,78 +119,13 @@ ${this.errorService.getErrorMsg(err?.error)}`)])
}
)), {dispatch: false});
userLoader = createEffect(() =>
this.actions.pipe(
ofType(removeWhitelistEntry, deleteWhitelistEntry, addWhitelistEntries),
mergeMap(action => [new ActiveLoader(action.type)])
)
);
getInvitedUsers = createEffect(() => this.actions.pipe(
ofType(getWhitelistEntries),
// eslint-disable-next-line @typescript-eslint/naming-convention
switchMap( () => this.loginApi.loginGetSettings({include_auth: true})),
withLatestFrom(this.store.select(selectCurrentUser)),
mergeMap( ([whitelistEntries, currentUser]) => {
// make the current user to be first!
const currentUserPosition = whitelistEntries.whitelist_entries.findIndex( whitelistEntry => whitelistEntry.user?.id === currentUser.id);
if (currentUserPosition > -1) {
const inviteCurrentUser = whitelistEntries.whitelist_entries.splice(currentUserPosition, 1);
whitelistEntries.whitelist_entries = [inviteCurrentUser[0], ...whitelistEntries.whitelist_entries];
}
return [setWhitelistEntries({whitelistEntries})];
})
getApiVersion = createEffect(() => this.actions.pipe(
ofType(getApiVersion),
mergeMap(() => this.serverService.serverInfo({})),
map((res: ServerInfoResponse) =>
setApiVersion({serverVersions: {server: res.version, api: res.api_version}})
),
catchError(() => [setUserWorkspacesFromUser()])
));
addInviteUsers = createEffect(() =>
this.actions.pipe(
ofType(addWhitelistEntries),
switchMap( (action) =>
this.loginApi.loginAddWhitelistEntries({emails: action.whitelistEntries})
.pipe(
mergeMap( (invitedEmails: LoginAddWhitelistEntriesResponse) => {
const notAddedByServer = action.whitelistEntries.filter( email => !invitedEmails.added.includes(email));
const actions: ActionType<any>[] = [new DeactiveLoader(action.type), setAddWhitelistEntries({whitelistEntries: invitedEmails.added})];
if (notAddedByServer.length) {
actions.push(new AddMessage(MESSAGES_SEVERITY.ERROR, `User addition failed: ${notAddedByServer.join(', ')} `));
}
return actions;
}),
catchError(err =>
[new DeactiveLoader(action.type), new AddMessage(MESSAGES_SEVERITY.ERROR, err.error.meta.result_msg)]
)
)
),
));
removeWhitelistEntryUser = createEffect(() =>
this.actions.pipe(
ofType(removeWhitelistEntry),
switchMap( (action) => this.loginApi.loginRemoveWhitelistEntries({emails: action.remove}).pipe(
mergeMap( (removedEmails: LoginRemoveWhitelistEntriesResponse) => [
setRemoveWhitelistEntry({removed: removedEmails.removed}),
new DeactiveLoader(action.type)
]
)
)),
));
deleteWhitelistEntryUser = createEffect(() =>
this.actions.pipe(
ofType(deleteWhitelistEntry),
switchMap( (action) =>
this.authService.authDeleteUser({user: action.user.id}).pipe(
mergeMap( (isRemovedUser: AuthDeleteUserResponse) => {
if (isRemovedUser.deleted) {
return [setRemoveWhitelistEntry({removed: [action.user.email]}), new DeactiveLoader(action.type)];
}
return [new AddMessage(MESSAGES_SEVERITY.ERROR, `User ${action.user.name} did not delete`), new DeactiveLoader(action.type)];
}),
catchError(err =>
[new DeactiveLoader(action.type), new AddMessage(MESSAGES_SEVERITY.ERROR, err.error.meta.result_msg)]
)
)
),
));
}

View File

@@ -6,13 +6,13 @@ import {catchError} from 'rxjs/operators';
import { Router } from '@angular/router';
import {selectCurrentUser, selectActiveWorkspace, selectSelectedWorkspaceTab} from '../reducers/users-reducer';
import {Store} from '@ngrx/store';
import { setCurrentUser } from '../actions/users.actions';
import {GetCurrentUserResponseUserObject} from '../../../business-logic/model/users/getCurrentUserResponseUserObject';
import {GetCurrentUserResponseUserObjectCompany} from '../../../business-logic/model/users/getCurrentUserResponseUserObjectCompany';
import {GetCurrentUserResponseUserObject} from '~/business-logic/model/users/getCurrentUserResponseUserObject';
import {GetCurrentUserResponseUserObjectCompany} from '~/business-logic/model/users/getCurrentUserResponseUserObjectCompany';
import {ConfigurationService} from '../../shared/services/configuration.service';
import {setCurrentUser} from '~/core/actions/users.action';
@Injectable()
export class WebappIntercptor implements HttpInterceptor {
export class WebappInterceptor implements HttpInterceptor {
private user: GetCurrentUserResponseUserObject;
private workspace: GetCurrentUserResponseUserObjectCompany;
private selectedWorkspaceTab: GetCurrentUserResponseUserObjectCompany;

View File

@@ -1,8 +1,7 @@
import {ActionReducer, Action} from '@ngrx/store';
import {merge, pick} from 'lodash/fp';
import {userPreferences} from '../../user-preferences';
import {USERS_ACTIONS} from '../../../app.constants';
import {SetPreferences} from '../actions/users.actions';
import {setPreferences} from '../actions/users.actions';
const firstRun = {};
@@ -22,8 +21,8 @@ export function createLocalStorageReducer(key: string, syncedKeys: string[], act
const savedState = userPreferences.getPreferences(key);
nextState = merge(nextState, savedState);
}
if (action.type === USERS_ACTIONS.SET_PREF) {
const savedState = (action as SetPreferences).payload[key];
if (action.type === setPreferences.type) {
const savedState = (action as ReturnType<typeof setPreferences>).payload[key];
nextState = merge(nextState, savedState);
}

View File

@@ -1,4 +1,4 @@
import {VIEW_ACTIONS} from '../../../app.constants';
import * as actions from '../actions/layout.actions';
const initialState = {
visible: false,
@@ -10,13 +10,13 @@ const initialState = {
export function messagesReducer(state = initialState, action) {
switch (action.type) {
case VIEW_ACTIONS.ADD_MESSAGE:
case actions.addMessage:
return {
visible: true,
severity: action.payload.severity,
message : action.payload.msg
};
case VIEW_ACTIONS.REMOVE_MESSAGE:
case actions.removeMessage:
return {
visible: false
};

View File

@@ -1,13 +1,31 @@
import {createSelector} from '@ngrx/store';
import * as projectsActions from '../actions/projects.actions';
import {setCompanyTags, setTagColors, setTags, setTagsFilterByProject, TagColor} from '../actions/projects.actions';
import {
setCompanyTags,
setGraphData,
setMetricVariant,
setTagColors,
setTags,
setTagsFilterByProject,
TagColor
} from '../actions/projects.actions';
import {Project} from '../../../business-logic/model/projects/project';
import {getSystemTags} from '../../../features/experiments/shared/experiments.utils';
import {experimentsView} from '../../../features/experiments/reducers';
import {ITableExperiment} from '../../experiments/shared/common-experiment-model.model';
import {MetricColumn} from '@common/shared/utils/tableParamEncode';
export const SYSTEM_TAGS_BLACK_LIST = ['archived'];
export interface ProjectStatsGraphData {
id: string;
x: number;
y: number;
name: string;
type: string;
status: string;
user: string;
}
export interface RootProjects {
projects: Project[];
selectedProject: Project;
@@ -18,6 +36,8 @@ export interface RootProjects {
systemTags: string[];
tagsColors: { [tag: string]: TagColor };
tagsFilterByProject: boolean;
graphVariant: {[project: string]: MetricColumn};
graphData: ProjectStatsGraphData[];
}
const initRootProjects: RootProjects = {
@@ -29,11 +49,13 @@ const initRootProjects: RootProjects = {
companyTags: [],
systemTags: [],
tagsColors: {},
tagsFilterByProject: true
tagsFilterByProject: true,
graphVariant: {},
graphData: null
};
export const projects = state => state.rootProjects as RootProjects;
export const selectProjects = createSelector(projects, (state): Project[] => state.projects);
export const selectRootProjects = createSelector(projects, (state): Project[] => state.projects);
export const selectSelectedProject = createSelector(projects, state => state.selectedProject);
export const selectSelectedProjectDescription = createSelector(projects, state => state.selectedProject?.description);
export const selectSelectedProjectId = createSelector(selectSelectedProject, (selectedProject): string => selectedProject ? selectedProject.id : '');
@@ -46,9 +68,19 @@ export const selectProjectSystemTags = createSelector(projects, state => getSyst
export const selectTagsColors = createSelector(projects, state => state.tagsColors);
export const selectTagColors = createSelector(selectTagsColors,
(tagsColors, props: { tag: string }) => tagsColors[props.tag]);
const selectSelectedProjectsMetricVariant = createSelector(projects, state => state.graphVariant);
export const selectSelectedMetricVariant = createSelector(selectSelectedProjectsMetricVariant,
(projectsVariant, projectId: string) => projectsVariant[projectId]);
export const selectSelectedMetricVariantForCurrProject = createSelector(
selectSelectedProjectsMetricVariant, selectSelectedProjectId,
(projectsVariant, projectId) => projectsVariant[projectId]);
export const selectGraphData = createSelector(projects, state => state.graphData);
export function projectsReducer(state: RootProjects = initRootProjects, action) {
export const projectsReducer = (state: RootProjects = initRootProjects, action) => {
switch (action.type) {
case projectsActions.SET_PROJECTS:
return {...state, projects: action.payload};
@@ -57,10 +89,11 @@ export function projectsReducer(state: RootProjects = initRootProjects, action)
return {
...state,
...(state.selectedProject?.id !== projectId && {archive: initRootProjects.archive}),
graphData: initRootProjects.graphData,
};
}
case projectsActions.SET_SELECTED_PROJECT:
return {...state, selectedProject: action.payload.project};
case projectsActions.setSelectedProject.type:
return {...state, selectedProject: action.project};
case projectsActions.RESET_SELECTED_PROJECT:
return {...state, selectedProject: initRootProjects.selectedProject};
case projectsActions.UPDATE_PROJECT: {
@@ -78,7 +111,13 @@ export function projectsReducer(state: RootProjects = initRootProjects, action)
return {...state, companyTags: action.tags, systemTags: action.systemTags};
case setTagColors.type:
return {...state, tagsColors: {...state.tagsColors, [action.tag]: action.colors}};
case setMetricVariant.type: {
const payload = action as ReturnType<typeof setMetricVariant>;
return {...state, graphVariant: {...state.graphVariant, [payload.projectId]: payload.col}};
}
case setGraphData.type:
return {...state, graphData: (action as ReturnType<typeof setGraphData>).stats};
default:
return state;
}
}
};

View File

@@ -1,156 +1,74 @@
import {USERS_ACTIONS} from '../../../app.constants';
import {createSelector} from '@ngrx/store';
import {createSelector, on, ReducerTypes} from '@ngrx/store';
import {
removeWorkspace,
setCurrentUser,
setInviteUserLink,
setActiveWorkspace,
setWorkspace,
setSelectedWorkspaceTab,
setUserWorkspaces,
setUserWorkspacesFromUser,
logout,
setWhitelistEntries,
setAddWhitelistEntries,
setRemoveWhitelistEntry,
setFilterByUser,
termsOfUseAccepted
termsOfUseAccepted,
setApiVersion,
fetchCurrentUser,
setCurrentUserName
} from '../actions/users.actions';
import {GetCurrentUserResponseUserObject} from '../../../business-logic/model/users/getCurrentUserResponseUserObject';
import {OrganizationCreateInviteResponse} from '../../../business-logic/model/organization/organizationCreateInviteResponse';
import {UsersGetCurrentUserResponseTermsOfUse} from '../../../business-logic/model/users/usersGetCurrentUserResponseTermsOfUse';
import {GetCurrentUserResponseUserObjectCompany} from '../../../business-logic/model/users/getCurrentUserResponseUserObjectCompany';
import {OrganizationGetUserCompaniesResponseCompanies} from '../../../business-logic/model/organization/organizationGetUserCompaniesResponseCompanies';
import {UsersGetCurrentUserResponseTermsOfUse} from '../../../business-logic/model/users/usersGetCurrentUserResponseTermsOfUse';
import {LoginGetSettingsResponse} from '../../../business-logic/model/login/loginGetSettingsResponse';
import {Invite} from '../../../business-logic/model/login/invite';
import {WhitelistEntry} from '../../../business-logic/model/login/whitelistEntry';
export interface UsersState {
currentUser: GetCurrentUserResponseUserObject;
termsOfUse: UsersGetCurrentUserResponseTermsOfUse;
inviteLink: OrganizationCreateInviteResponse;
whitelistEntries: LoginGetSettingsResponse;
inviteLinkId: string;
activeWorkspace: GetCurrentUserResponseUserObjectCompany;
userWorkspaces: OrganizationGetUserCompaniesResponseCompanies[];
selectedWorkspaceTab: GetCurrentUserResponseUserObjectCompany;
workspaces: GetCurrentUserResponseUserObjectCompany[];
userWorkspaces: OrganizationGetUserCompaniesResponseCompanies[];
termsOfUse: UsersGetCurrentUserResponseTermsOfUse;
inviteLink: OrganizationCreateInviteResponse;
inviteLinkId: string;
showOnlyUserWork: boolean;
serverVersions: { server: string; api: string };
}
const initUsers: UsersState = {
export const initUsers: UsersState = {
currentUser: null,
termsOfUse: null,
inviteLink: null,
whitelistEntries: null,
inviteLinkId: null,
activeWorkspace: null,
userWorkspaces: [],
selectedWorkspaceTab: null,
workspaces: [],
userWorkspaces: [],
showOnlyUserWork: false
termsOfUse: null,
inviteLink: null,
inviteLinkId: null,
showOnlyUserWork: false,
serverVersions: null
};
export const users = state => state.users as UsersState;
export const selectCurrentUser = createSelector(users, (state): GetCurrentUserResponseUserObject => state.currentUser);
export const selectWorkspaces = createSelector(users, (state): GetCurrentUserResponseUserObjectCompany[] => state.workspaces);
export const selectActiveWorkspace = createSelector(users, (state): GetCurrentUserResponseUserObjectCompany => state.activeWorkspace);
export const selectUserWorkspaces = createSelector(users, (state) => state.userWorkspaces);
export const selectSelectedWorkspaceTab = createSelector(users, (state) => state.selectedWorkspaceTab);
export const selectWorkspaces = createSelector(users, (state): GetCurrentUserResponseUserObjectCompany[] => state.workspaces);
export const selectTermsOfUse = createSelector(users, state => state.termsOfUse);
export const selectInviteLink = createSelector(users, state => state.inviteLink);
export const selectShowOnlyUserWork = createSelector(users, (state): boolean => state.showOnlyUserWork);
export const selectWhitelistEntries = createSelector(users, state => state.whitelistEntries?.whitelist_entries);
export const selectServerVersions = createSelector(users, (state): { server: string; api: string } => state.serverVersions);
export function usersReducer(state = initUsers, action) {
switch (action.type) {
case USERS_ACTIONS.FETCH_CURRENT_USER:
return {...state};
case setInviteUserLink.type:
return {...state, inviteLink: {id: action.id, allocated_users: action.allocated_users, allowed_users: action.allowed_users}};
case setCurrentUser.type: {
const lastWorkspace = window.localStorage.getItem('lastWorkspace');
window.localStorage.removeItem('lastWorkspace');
const workspaces = action.user ? [action.user.company, ...(action.user?.companies || [])] : [];
const activeWorkspace = workspaces.find(workspace => workspace.id === lastWorkspace);
const altWorkspace = workspaces.find(workspace => workspace.id === state.activeWorkspace?.id);
return {
...state,
currentUser: action.user,
termsOfUse: action.terms_of_use,
workspaces,
activeWorkspace: activeWorkspace || altWorkspace || action.user?.company
};
}
case termsOfUseAccepted.type:
return {...state, termsOfUse: {...state.termsOfUse, accept_required: false}};
case logout.type:
return {
...state,
currentUser: null
};
case setWorkspace.type: {
const workspace = (action as ReturnType<typeof setWorkspace>).workspace;
return {
...state,
workspaces: [...state.workspaces, ...(!state.workspaces.find(ws => ws.id === workspace.id) ? [action.workspace] : [])],
activeWorkspace: action.workspace
};
}
case setActiveWorkspace.type:
return {...state, activeWorkspace: action.workspace};
case setSelectedWorkspaceTab.type:
return {...state, selectedWorkspaceTab: action.workspace};
case removeWorkspace.type: {
const workspaceId = (action as ReturnType<typeof removeWorkspace>).workspaceId;
const workspaces = state.workspaces.filter(ws => ws.id !== workspaceId);
return {
...state,
...(workspaceId === state.activeWorkspace.id && {activeWorkspace: workspaces[0]}),
workspaces,
userWorkspaces: state.userWorkspaces.filter(ws => ws.id !== workspaceId)
};
}
case setUserWorkspaces.type:
return {...state, userWorkspaces: (action as ReturnType<typeof setUserWorkspaces>).workspaces};
case setUserWorkspacesFromUser.type:
return {...state, ...(state.currentUser?.company && {userWorkspaces: [state.currentUser?.company]})};
case setFilterByUser.type:
return {...state, showOnlyUserWork: action.showOnlyUserWork};
case setWhitelistEntries.type:
return {
...state,
whitelistEntries: (action as ReturnType<typeof setWhitelistEntries>).whitelistEntries
};
case setAddWhitelistEntries.type: {
const newWhitelistEntry: WhitelistEntry[] = (action as ReturnType<typeof setAddWhitelistEntries>).whitelistEntries.map(email => {
return {
email,
status: Invite.StatusEnum.Pending
} as WhitelistEntry;
});
const newWhitelistEntries = [...state.whitelistEntries.whitelist_entries, ...newWhitelistEntry];
const whitelistEntries = {...state.whitelistEntries, whitelist_entries: newWhitelistEntries};
return {
...state,
whitelistEntries
};
}
case setRemoveWhitelistEntry.type: {
const filteredWhitelistEntries = state.whitelistEntries.whitelist_entries
.filter( (whitelistEntry: WhitelistEntry) => {
return !(action as ReturnType<typeof setRemoveWhitelistEntry>).removed.includes(whitelistEntry.email);
});
const whitelistEntries = {...state.whitelistEntries, whitelist_entries: filteredWhitelistEntries};
return {
...state,
whitelistEntries
};
}
default:
return state;
}
}
export const usersReducerFunctions = [
on(fetchCurrentUser, state => ({...state})),
on(setInviteUserLink, (state, action) => ({
...state,
// eslint-disable-next-line @typescript-eslint/naming-convention
inviteLink: {id: action.id, allocated_users: action.allocated_users, allowed_users: action.allowed_users}
})),
on(setCurrentUserName, (state, action) => ({
...state,
currentUser: {...state.currentUser, name: action.name}
})),
// eslint-disable-next-line @typescript-eslint/naming-convention
on(termsOfUseAccepted, state => ({...state, termsOfUse: {...state.termsOfUse, accept_required: false}})),
on(logout, state => ({
...state,
currentUser: null
})),
on(setFilterByUser, (state, action) => ({...state, showOnlyUserWork: action.showOnlyUserWork})),
on(setApiVersion, (state, action) => ({...state, serverVersions: action.serverVersions}))
] as ReducerTypes<UsersState, any>[];

View File

@@ -1,7 +1,6 @@
import {HTTP, HTTP_ACTIONS, VIEW_ACTIONS} from '../../../app.constants';
import {createSelector} from '@ngrx/store';
import {get} from 'lodash/fp';
import {firstLogin, neverShowPopupAgain, plotlyReady, setScaleFactor} from '../actions/layout.actions';
import {createReducer, createSelector, on} from '@ngrx/store';
import * as layoutActions from '../actions/layout.actions';
import {apiRequest, requestFailed} from '@common/core/actions/http.actions';
export interface ViewState {
loading: {[endpoint: string]: boolean};
@@ -56,56 +55,33 @@ export const selectPlotlyReady = createSelector(views, state => state.plotlyRead
export const selectNeverShowPopups = createSelector(views, (state): string[] => state.neverShowPopupAgain);
export function viewReducer(viewState: ViewState = initViewState, action) {
switch (action.type) {
case HTTP_ACTIONS.REQUEST_FAILED: {
const isLoggedOut = action.payload.err && action.payload.err.status === 401;
return {...viewState, loggedOut: isLoggedOut};
}
case VIEW_ACTIONS.DEACTIVE_LOADER:
return {
...viewState,
loading: {...viewState.loading, [action.payload.endpoint]: false}
};
case VIEW_ACTIONS.ACTIVE_LOADER:
return {
...viewState,
loading: {...viewState.loading, [action.payload.endpoint]: true}
};
case VIEW_ACTIONS.VISIBILITY_CHANGED:
return {...viewState, applicationVisible: action.visible};
case setScaleFactor.type:
return {...viewState, scaleFactor: action.scale};
case firstLogin.type:
return {...viewState, firstLogin: (action as ReturnType<typeof firstLogin>).first, firstLoginAt: new Date().getTime()};
case plotlyReady.type:
return {...viewState, plotlyReady: true};
case VIEW_ACTIONS.RESET_LOADER:
return {...viewState, loading: {}};
case HTTP.API_REQUEST_SUCCESS:
return {
...viewState,
loading: {...viewState.loading, [get('payload.endpoint', action) ? action.payload.endpoint : 'default']: false}
};
case HTTP.API_REQUEST:
return {
...viewState,
loading: {...viewState.loading, [get('payload.endpoint', action) ? action.payload.endpoint : 'default']: true}
};
case VIEW_ACTIONS.SET_NOTIFICATION_DIALOG:
return {...viewState, notification: action.payload};
case VIEW_ACTIONS.SET_BACKDROP:
return {...viewState, backdropActive: action.payload};
case VIEW_ACTIONS.SET_AUTO_REFRESH:
return {...viewState, autoRefresh: action.payload.autoRefresh};
case VIEW_ACTIONS.SET_COMPARE_AUTO_REFRESH:
return {...viewState, compareAutoRefresh: action.payload.autoRefresh};
case neverShowPopupAgain.type:
return {...viewState, neverShowPopupAgain: action.reset? viewState.neverShowPopupAgain.filter( popups => popups !== action.popupId) : [...viewState.neverShowPopupAgain, action.popupId]};
default:
return viewState;
}
}
export const viewReducer = createReducer(
initViewState,
on(requestFailed, (state, action) => {
const isLoggedOut = action.err && action.err.status === 401;
return {...state, loggedOut: isLoggedOut};
}),
on(layoutActions.deactivateLoader, (state, action) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {[action.endpoint]: removed, ...loading} = state.loading;
return {...state, loading};
}),
on(layoutActions.activeLoader, (state, action) => ({
...state,
loading: {...state.loading, [action.endpoint]: true}
})),
on(layoutActions.visibilityChanged, (state, action) => ({...state, applicationVisible: action.visible})),
on(layoutActions.setScaleFactor, (state, action) => ({...state, scaleFactor: action.scale})),
on(layoutActions.firstLogin, (state, action) => ({...state, firstLogin: action.first, firstLoginAt: new Date().getTime()})),
on(layoutActions.plotlyReady, (state) => ({...state, plotlyReady: true})),
on(layoutActions.resetLoader, (state) => ({...state, loading: {}})),
on(apiRequest, (state, action) => ({
...state,
loading: {...state.loading, [action?.endpoint || 'default']: true}
})),
on(layoutActions.setNotificationDialog, (state, action) => ({...state, notification: action})),
on(layoutActions.setBackdrop, (state, action) => ({...state, backdropActive: action.payload})),
on(layoutActions.setAutoRefresh, (state, action) => ({...state, autoRefresh: action.autoRefresh})),
on(layoutActions.setCompareAutoRefresh, (state, action) => ({...state, compareAutoRefresh: action.autoRefresh})),
on(layoutActions.neverShowPopupAgain, (state, action) => ({...state, neverShowPopupAgain: action.reset? state.neverShowPopupAgain.filter( popups => popups !== action.popupId) : Array.from(new Set([...state.neverShowPopupAgain, action.popupId]))}))
);

View File

@@ -1,10 +1,10 @@
import {Injectable} from '@angular/core';
import {act, Actions, Effect, ofType} from '@ngrx/effects';
import {ActiveLoader, DeactiveLoader} from '../core/actions/layout.actions';
import {activeLoader, deactivateLoader} from '../core/actions/layout.actions';
import {SearchActivate, SearchClear, searchExperiments, searchModels, searchProjects, searchSetTerm, searchStart, SetExperimentsResults, SetModelsResults, SetProjectsResults} from './dashboard-search.actions';
import {EXPERIMENT_SEARCH_ONLY_FIELDS, SEARCH_ACTIONS, SEARCH_PAGE_SIZE} from './dashboard-search.consts';
import {ApiProjectsService} from '../../business-logic/api-services/projects.service';
import {RequestFailed} from '../core/actions/http.actions';
import {requestFailed} from '../core/actions/http.actions';
import {Store} from '@ngrx/store';
import {selectActiveSearch} from './dashboard-search.reducer';
import {ProjectsGetAllExRequest} from '../../business-logic/model/projects/projectsGetAllExRequest';
@@ -29,7 +29,7 @@ export class DashboardSearchEffects {
@Effect()
activeLoader = this.actions.pipe(
ofType(SEARCH_ACTIONS.SEARCH_PROJECTS, SEARCH_ACTIONS.SEARCH_MODELS, SEARCH_ACTIONS.SEARCH_EXPERIMENTS),
map(action => new ActiveLoader(action.type))
map(action => activeLoader(action.type))
);
// add actions for each search
@Effect()
@@ -64,8 +64,8 @@ export class DashboardSearchEffects {
include_stats : true,
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination']
}).pipe(
mergeMap(res => [new SetProjectsResults(res.projects), new DeactiveLoader(action.type)]),
catchError(error => [new DeactiveLoader(action.type), new RequestFailed(error)])))
mergeMap(res => [new SetProjectsResults(res.projects), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])))
);
@Effect()
@@ -81,8 +81,8 @@ export class DashboardSearchEffects {
system_tags: ['-archived'],
only_fields: ['labels', 'ready', 'created', 'framework', 'user.name', 'name', 'parent.name', 'task.name', 'id', 'company']
}).pipe(
mergeMap(res => [new SetModelsResults(res.models), new DeactiveLoader(action.type)]),
catchError(error => [new DeactiveLoader(action.type), new RequestFailed(error)])))
mergeMap(res => [new SetModelsResults(res.models), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])))
);
@Effect()
@@ -99,7 +99,7 @@ export class DashboardSearchEffects {
type : ['__$not', 'annotation_manual', '__$not', 'annotation', '__$not', 'dataset_import'],
system_tags: ['-archived']
}).pipe(
mergeMap(res => [new SetExperimentsResults(res.tasks), new DeactiveLoader(action.type)]),
catchError(error => [new DeactiveLoader(action.type), new RequestFailed(error)])))
mergeMap(res => [new SetExperimentsResults(res.tasks), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])))
);
}

View File

@@ -1,5 +1,5 @@
export type TaskTableColFieldsEnum = 'output.result' | 'comment' | 'id' | 'project.name' | 'name' | 'type' | 'status' | 'started' | 'last_update' | 'user.name' | 'queue.name' | 'worker.name';
export const CARDS_IN_ROW = 4;
export const CARDS_IN_ROW = 6;
export const RECENT_TASKS_TABLE_COL_FIELDS = {
RESULT : 'output.result' as TaskTableColFieldsEnum,
COMMENT : 'comment' as TaskTableColFieldsEnum,

View File

@@ -1,8 +1,8 @@
import {Injectable} from '@angular/core';
import {ApiProjectsService} from '../../business-logic/api-services/projects.service';
import {Actions, createEffect, Effect, ofType} from '@ngrx/effects';
import {RequestFailed} from '../core/actions/http.actions';
import {ActiveLoader, AddMessage, DeactiveLoader} from '../core/actions/layout.actions';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {requestFailed} from '../core/actions/http.actions';
import {activeLoader, addMessage, deactivateLoader} from '../core/actions/layout.actions';
import {CreateProjectFromDashboard, getInviteInfo, GetRecentProjects, setInviteInfo, SetRecentProjects, SetRecentTasks} from './common-dashboard.actions';
import {CARDS_IN_ROW, DASHBOARD_ACTIONS} from './common-dashboard.const';
import {MESSAGES_SEVERITY} from '../../app.constants';
@@ -16,7 +16,7 @@ import {MatDialog} from '@angular/material/dialog';
import {ConfirmDialogComponent} from '../shared/ui-components/overlay/confirm-dialog/confirm-dialog.component';
import {Store} from '@ngrx/store';
import {ErrorService} from '../shared/services/error.service';
import {selectCurrentUser, selectShowOnlyUserWork} from "../core/reducers/users-reducer";
import {selectCurrentUser, selectShowOnlyUserWork} from '../core/reducers/users-reducer';
@Injectable()
export class CommonDashboardEffects {
@@ -26,14 +26,12 @@ export class CommonDashboardEffects {
private dialog: MatDialog) {
}
@Effect()
activeLoader = this.actions.pipe(
activeLoader = createEffect(() => this.actions.pipe(
ofType(DASHBOARD_ACTIONS.GET_RECENT_PROJECTS, DASHBOARD_ACTIONS.GET_RECENT_PROJECTS, DASHBOARD_ACTIONS.GET_RECENT_TASKS, DASHBOARD_ACTIONS.CREATE_PROJECT),
map(action => new ActiveLoader(action.type))
);
map(action => activeLoader(action.type))
));
@Effect()
getRecentProjects = this.actions.pipe(
getRecentProjects = createEffect(() => this.actions.pipe(
ofType<GetRecentProjects>(DASHBOARD_ACTIONS.GET_RECENT_PROJECTS),
withLatestFrom(this.store.select(selectCurrentUser), this.store.select(selectShowOnlyUserWork)),
mergeMap(([action, user, showOnlyUserWork]) =>
@@ -42,14 +40,13 @@ export class CommonDashboardEffects {
active_users: (showOnlyUserWork ? [user.id] : null),
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination']
}).pipe(
mergeMap(res => [new SetRecentProjects(res.projects), new DeactiveLoader(action.type)]),
catchError(error => [new DeactiveLoader(action.type), new RequestFailed(error)])
mergeMap(res => [new SetRecentProjects(res.projects), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])
)
)
);
));
@Effect()
getRecentTasks = this.actions.pipe(
getRecentTasks = createEffect(() => this.actions.pipe(
ofType(DASHBOARD_ACTIONS.GET_RECENT_TASKS ),
withLatestFrom(this.store.select(selectCurrentUser), this.store.select(selectShowOnlyUserWork)),
switchMap(([action, user, showOnlyUserWork]) => this.tasksApi.tasksGetAllEx({
@@ -63,26 +60,25 @@ export class CommonDashboardEffects {
user: showOnlyUserWork ? [user.id] : null,
})
.pipe(
mergeMap(res => [new SetRecentTasks(res.tasks as Array<IRecentTask>), new DeactiveLoader(action.type)]),
catchError(err => [new RequestFailed(err), new DeactiveLoader(action.type)])
mergeMap(res => [new SetRecentTasks(res.tasks as Array<IRecentTask>), deactivateLoader(action.type)]),
catchError(err => [requestFailed(err), deactivateLoader(action.type)])
)
)
);
));
@Effect()
createProject = this.actions.pipe(
createProject = createEffect(() => this.actions.pipe(
ofType<CreateProjectFromDashboard>(DASHBOARD_ACTIONS.CREATE_PROJECT),
mergeMap((action) => this.projectsApi.projectsCreate(action.payload.project)
.pipe(
mergeMap(res => [
new GetRecentProjects({stats_for_state: ProjectsGetAllExRequest.StatsForStateEnum.Active, order_by: ['-last_update'], page: 0, page_size: CARDS_IN_ROW}),
new DeactiveLoader(action.type),
new AddMessage(MESSAGES_SEVERITY.SUCCESS, 'Project Created Successfully')]),
catchError(error => [new DeactiveLoader(action.type), new RequestFailed(error),
new AddMessage(MESSAGES_SEVERITY.ERROR, 'Project Creation Failed')])
deactivateLoader(action.type),
addMessage(MESSAGES_SEVERITY.SUCCESS, 'Project Created Successfully')]),
catchError(error => [deactivateLoader(action.type), requestFailed(error),
addMessage(MESSAGES_SEVERITY.ERROR, 'Project Creation Failed')])
)
)
);
));
addWorkspace = createEffect(() => this.actions.pipe(
ofType(addWorkspace),

View File

@@ -1,13 +1,9 @@
<div class="row h-100">
<div *ngIf="(recentTasks$ | async).length >= 0" class="col-24 h-100">
<div class="recent-header">
<div class="recent-title">RECENT EXPERIMENTS</div>
<ng-content select="[header-buttons]"></ng-content>
</div>
<div class="table-container">
<sm-recent-tasks-table [tasks]="recentTasks$ | async"
(taskSelected)="taskSelected($event)">
</sm-recent-tasks-table>
</div>
</div>
<div class="recent-header">
<div class="recent-title">RECENT EXPERIMENTS</div>
<ng-content select="[header-buttons]"></ng-content>
</div>
<div class="table-container">
<sm-recent-tasks-table [tasks]="recentTasks"
(taskSelected)="taskSelected($event)">
</sm-recent-tasks-table>
</div>

View File

@@ -1,8 +1,7 @@
import { Component, OnInit } from '@angular/core';
import {Component, Input, OnInit} from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { Store } from '@ngrx/store';
import { IRecentTask, selectRecentTasks } from '../../common-dashboard.reducer';
import { IRecentTask} from '../../common-dashboard.reducer';
import {GetRecentTasks} from '../../common-dashboard.actions';
import { ITask } from '../../../../business-logic/model/al-task';
import {selectCurrentUser} from '../../../core/reducers/users-reducer';
@@ -14,10 +13,9 @@ import {filter, take} from 'rxjs/operators';
styleUrls: ['./dashboard-experiments.component.scss']
})
export class DashboardExperimentsComponent implements OnInit {
public recentTasks$: Observable<Array<IRecentTask>>;
@Input() recentTasks: IRecentTask[];
constructor(private store: Store<any>, private router: Router) {
this.recentTasks$ = this.store.select(selectRecentTasks);
}
ngOnInit() {
@@ -29,7 +27,7 @@ export class DashboardExperimentsComponent implements OnInit {
public taskSelected(task: IRecentTask | ITask) {
// TODO ADD task.id to route
const projectId = task.project ? task.project.id : '*';
this.router.navigateByUrl('projects/' + projectId + '/experiments/' + task.id);
return this.router.navigateByUrl('projects/' + projectId + '/experiments/' + task.id);
}
}

View File

@@ -1,25 +1,23 @@
<div class="row recent-header">
<div class="col-24 d-flex justify-content-between">
<div class="sm-card-list-layout">
<div #header class="sm-card-list-header">
<div class="recent-title">RECENT PROJECTS
<button class="btn btn-link view-all" (click)="router.navigateByUrl('/projects')">VIEW ALL</button>
</div>
<div *smCheckPermission="true">
<button *ngIf="(recentProjectsList$ | async).length >= CARDS_IN_ROW"
<div>
<button *ngIf="(recentProjectsList$ | async).length >= cardsInRow"
class="btn btn-primary d-flex align-items-center"
(click)="openCreateProjectDialog()">
<i class="al-icon al-color sm blue-400 al-ico-add mr-2"></i>NEW PROJECT
</button>
</div>
</div>
</div>
<div class="projects">
<sm-project-card
*ngFor="let project of recentProjectsList$ | async | slice:0:CARDS_IN_ROW"
*ngFor="let project of recentProjectsList$ | async"
[project]="project" (projectCardClicked)="projectCardClicked($event)"
[hideMenu]="true"
></sm-project-card>
<sm-plus-card
*smCheckPermission="(recentProjectsList$ | async).length < CARDS_IN_ROW"
*ngIf="(recentProjectsList$ | async).length < cardsInRow"
[folder]="true"
(plusCardClick)="openCreateProjectDialog()"
></sm-plus-card>

View File

@@ -6,24 +6,17 @@
font-weight: 500;
}
.sm-card-list-layout {
height: 324px;
overflow: hidden;
grid-gap: 16px 24px;
padding-top: 24px;
}
.sm-card-list-header {
grid-column: 1 / -1;
padding: 0;
}
.recent-title {
@include recent-title();
}
.recent-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 24px;
padding-bottom: 12px;
}
.projects {
display: flex;
flex-wrap: wrap;
overflow: hidden;
height: 250px;
sm-project-card {
margin: 0 15px;
}
}

View File

@@ -1,33 +1,37 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, Output, EventEmitter, AfterViewInit, ViewChild, ElementRef, OnDestroy} from '@angular/core';
import {Router} from '@angular/router';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
import {Observable} from 'rxjs';
import {fromEvent, Observable, Subscription} from 'rxjs';
import {Store} from '@ngrx/store';
import {Project} from '../../../../business-logic/model/projects/project';
import {selectRecentProjects} from '../../common-dashboard.reducer';
import {GetRecentProjects} from '../../common-dashboard.actions';
import {CARDS_IN_ROW} from '../../common-dashboard.const';
import {ProjectDialogComponent} from '../../../shared/project-dialog/project-dialog.component';
import {ResetSelectedProject, SetSelectedProjectId} from '../../../core/actions/projects.actions';
import {selectCurrentUser} from '../../../core/reducers/users-reducer';
import {filter, take} from 'rxjs/operators';
import {filter, take, throttleTime} from 'rxjs/operators';
import {isExample} from '../../../shared/utils/shared-utils';
import { CARDS_IN_ROW } from '../../common-dashboard.const';
@Component({
selector : 'sm-dashboard-projects',
templateUrl: './dashboard-projects.component.html',
styleUrls : ['./dashboard-projects.component.scss']
})
export class DashboardProjectsComponent implements OnInit {
export class DashboardProjectsComponent implements OnInit, AfterViewInit, OnDestroy {
public recentProjectsList$: Observable<Array<Project>>;
public CARDS_IN_ROW = CARDS_IN_ROW;
private dialog: MatDialogRef<ProjectDialogComponent>;
readonly cardsInRow = CARDS_IN_ROW;
@Output() width = new EventEmitter<number>();
private sub: Subscription;
constructor(private store: Store<any>, public router: Router,
private matDialog: MatDialog) {
this.recentProjectsList$ = this.store.select(selectRecentProjects);
}
@ViewChild('header') header: ElementRef<HTMLDivElement>;
ngOnInit() {
this.store.dispatch(new ResetSelectedProject());
this.store.select(selectCurrentUser)
@@ -35,6 +39,12 @@ export class DashboardProjectsComponent implements OnInit {
.subscribe(() => this.store.dispatch(new GetRecentProjects()));
}
ngAfterViewInit() {
window.setTimeout(() => this.width.emit(this.header.nativeElement.getBoundingClientRect().width));
this.sub = fromEvent(window, 'resize')
.pipe(throttleTime(50))
.subscribe(() => this.width.emit(this.header.nativeElement.getBoundingClientRect().width));
}
public projectCardClicked(project: Project) {
this.router.navigateByUrl(`projects/${project.id}`);
this.store.dispatch(new SetSelectedProjectId(project.id, isExample(project)));
@@ -52,4 +62,8 @@ export class DashboardProjectsComponent implements OnInit {
}
});
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
}

View File

@@ -50,7 +50,7 @@ export abstract class DashboardSearchComponentBase {
this.store.dispatch(new InitSearch('Search for all'));
this.searchSubs = this.searchQuery$
.pipe(skip(1), filter(query => !!query?.query))
.pipe(skip(1))
.subscribe(query => {
this.searchTermChanged(query.query, query.regExp);
});
@@ -87,7 +87,7 @@ export abstract class DashboardSearchComponentBase {
this.activeLink = activeLink;
}
setFirstActiveLink(allResults, tabsIndexes) {
setFirstActiveLink(allResults, tabsIndexes: string[]) {
if (!(allResults[tabsIndexes.indexOf(this.activeLink)].length > 0)) {
const firstTabIndex = allResults.findIndex(list => list.length > 0);
if (firstTabIndex > -1) {

View File

@@ -30,7 +30,7 @@
<ng-template #configStep>
<div class="steps-content">
<div class="text" *ngIf="queue">
Tasks have been enqueued on the {{queue?.name}} queue, which is currently not serviced by any worker. They will remain in the 'pending' state until a ClearML worker services this queue.
Tasks have been enqueued on the <b>{{queue?.name}}</b> queue, which is currently not serviced by any worker. They will remain in the 'pending' state until a ClearML worker services this queue.
</div>
<div *ngFor="let step of steps" class="step-container">
<div class="step-header">{{step.header}}</div>
@@ -79,7 +79,7 @@
<div class="step">3. Integrate</div>
<div class="step sub-note">Add the following lines to your code</div>
<div class="code">
<div #content class="content"><span class="variable">from</span> clearml <span class="variable">import</span> Task
<div #content class="content"><span class="variable">from</span> {{gettingStartedContext?.agentName || 'clearml'}} <span class="variable">import</span> Task
task <span class="operation">=</span> Task.<span class="variable">init</span>(project_name<span class="operation">=</span>"my project", task_name<span class="operation">=</span>"my task")</div>
<sm-copy-clipboard
[hideBackground]="true"

View File

@@ -89,13 +89,13 @@
.code {
position: relative;
padding: 16px;
margin: 1px 0 2px;
width: 530px;
color: $blue-100;
background-color: $blue-900;
border-radius: 4px;
overflow: auto;
&:hover {
sm-copy-clipboard {
@@ -104,6 +104,9 @@
}
.content {
padding: 16px;
overflow: auto;
margin-right: 16px;
white-space: pre;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
@@ -131,6 +134,7 @@
position: absolute;
right: 10px;
top: 10px;
background-color: rgba(0, 0, 0, 0.2);
}
}

View File

@@ -12,6 +12,7 @@ import {AdminService} from '../../../../features/admin/admin.service';
import {ConfigurationService} from '../../../shared/services/configuration.service';
import {GetCurrentUserResponseUserObjectCompany} from '../../../../business-logic/model/users/getCurrentUserResponseUserObjectCompany';
import {Queue} from "../../../../business-logic/model/queues/queue";
import {GettingStartedContext} from '../../../../../environments/base';
type StepObject = { header?: string; title?: string; code?: string; subNote?: string };
@@ -25,6 +26,7 @@ export class WelcomeMessageComponent implements OnInit, OnDestroy {
accessKey: string;
secretKey: string;
creatingCredentials = false;
private workspacesSub: Subscription;
public workspace: GetCurrentUserResponseUserObjectCompany;
private newCredentialSub: Subscription;
@@ -50,7 +52,7 @@ export class WelcomeMessageComponent implements OnInit, OnDestroy {
code: 'pip install clearml-agent',
}, {
title: '2. Configure',
code: 'clear-agent init'
code: 'clearml-agent init'
}
];
host: string;
@@ -58,6 +60,8 @@ export class WelcomeMessageComponent implements OnInit, OnDestroy {
public queue: Queue;
steps: StepObject[];
doNotShowAgain: boolean;
private agentName: string;
public gettingStartedContext: GettingStartedContext;
constructor(
private store: Store<any>,
@@ -69,10 +73,15 @@ export class WelcomeMessageComponent implements OnInit, OnDestroy {
this.dialogRef.beforeClosed().subscribe(() => this.dialogRef.close(this.doNotShowAgain));
this.step = data?.step || this.step;
this.queue = data?.queue;
this.gettingStartedContext = this.configService.getStaticEnvironment().gettingStartedContext;
this.steps = this.queue ? this.ORPHANED_QUEUE_STEPS : this.GETTING_STARTED_STEPS;
if (this.queue) {
this.steps[0].code = `clearml-agent daemon -queue ${this.queue.name}`;
this.steps[0].code = `clearml-agent daemon --queue ${this.queue.name}`;
this.steps[0].header = `To assign a worker to the ${this.queue.name} queue, run:`;
} else if (this.gettingStartedContext) {
this.steps[0].code = this.gettingStartedContext.install;
this.steps[1].code = this.gettingStartedContext.configure;
}
this.host = `${window.location.protocol}//${window.location.hostname}`;
if (this.API_BASE_URL === '/api') {

View File

@@ -2,7 +2,7 @@ import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} f
import {isHtmlPage, isTextFileURL} from '../../shared/utils/shared-utils';
import {IsAudioPipe} from '../../shared/pipes/is-audio.pipe';
import {IsVideoPipe} from '../../shared/pipes/is-video.pipe';
import {AddMessage} from '../../core/actions/layout.actions';
import {addMessage} from '../../core/actions/layout.actions';
import {MESSAGES_SEVERITY} from '../../../app.constants';
import {Store} from '@ngrx/store';
import {ThemeEnum} from '../../experiments/shared/common-experiments.const';
@@ -55,7 +55,7 @@ export class DebugImageSnippetComponent implements OnInit {
}
copyToClipboardSuccess(success: boolean) {
this.store.dispatch(new AddMessage(
this.store.dispatch(addMessage(
success ? MESSAGES_SEVERITY.SUCCESS : MESSAGES_SEVERITY.ERROR,
success ? 'Path copied to clipboard' : 'No path to copy'
));

View File

@@ -2,13 +2,13 @@ import {Injectable} from '@angular/core';
import {Actions, Effect, ofType} from '@ngrx/effects';
import {catchError, mergeMap, map, switchMap, withLatestFrom} from 'rxjs/operators';
import * as debugActions from './debug-images-actions';
import {ActiveLoader, DeactiveLoader} from '../core/actions/layout.actions';
import {activeLoader, deactivateLoader} from '../core/actions/layout.actions';
import {ApiTasksService} from '../../business-logic/api-services/tasks.service';
import {ApiEventsService} from '../../business-logic/api-services/events.service';
import {RequestFailed} from '../core/actions/http.actions';
import {REFRESH_EXPERIMENTS} from '../experiments/actions/common-experiments-view.actions';
import {requestFailed} from '../core/actions/http.actions';
import {refreshExperiments} from '../experiments/actions/common-experiments-view.actions';
import {setRefreshing} from '../experiments-compare/actions/compare-header.actions';
import {Store} from '@ngrx/store';
import {Action, Store} from '@ngrx/store';
import {selectDebugImages, selectImageViewerScrollId} from './debug-images-reducer';
import {
setCurrentDebugImage,
@@ -42,7 +42,7 @@ export class DebugImagesEffects {
@Effect()
activeLoader = this.actions$.pipe(
ofType(debugActions.FETCH_EXPERIMENTS),
map(action => new ActiveLoader(action.type))
map(action => activeLoader(action.type))
);
@@ -61,7 +61,7 @@ export class DebugImagesEffects {
})
.pipe(
mergeMap((res: any) => {
const actionsToShoot = [new DeactiveLoader(action.type), setRefreshing({payload: false})];
const actionsToShoot = [deactivateLoader(action.type), setRefreshing({payload: false})] as Action[];
if (res.metrics[0].iterations && res.metrics[0].iterations.length > 0) {
actionsToShoot.push(new debugActions.SetDebugImages({res, task: action.payload.task}));
switch (action.type) {
@@ -94,10 +94,10 @@ export class DebugImagesEffects {
return actionsToShoot;
}),
catchError(error => [
new RequestFailed(error),
requestFailed(error),
setRefreshing({payload: false}),
new DeactiveLoader(action.type),
new DeactiveLoader(REFRESH_EXPERIMENTS)
deactivateLoader(action.type),
deactivateLoader(refreshExperiments.type)
])
)
)
@@ -108,8 +108,8 @@ export class DebugImagesEffects {
ofType<debugActions.FetchExperiments>(debugActions.FETCH_EXPERIMENTS),
switchMap((action) => this.apiTasks.tasksGetAllEx({id: action.payload, only_fields: ['id', 'name', 'status']})
.pipe(
mergeMap(res => [new debugActions.SetExperimentsNames(res), new DeactiveLoader(action.type)]),
catchError(error => [new RequestFailed(error), new DeactiveLoader(action.type)])
mergeMap(res => [new debugActions.SetExperimentsNames(res), deactivateLoader(action.type)]),
catchError(error => [requestFailed(error), deactivateLoader(action.type)])
)
)
);
@@ -123,8 +123,8 @@ export class DebugImagesEffects {
event_type: 'training_debug_image'
})
.pipe(
mergeMap(res => [new debugActions.SetMetrics(res), new DeactiveLoader(action.type)]),
catchError(error => [new RequestFailed(error), new DeactiveLoader(action.type)])
mergeMap(res => [new debugActions.SetMetrics(res), deactivateLoader(action.type)]),
catchError(error => [requestFailed(error), deactivateLoader(action.type)])
)
)
);
@@ -144,10 +144,10 @@ export class DebugImagesEffects {
.pipe(
mergeMap(res => [
setDebugImageIterations({min_iteration: res.min_iteration, max_iteration: res.max_iteration}),
setCurrentDebugImage({event: res.event}), new DeactiveLoader(action.type),
setCurrentDebugImage({event: res.event}), deactivateLoader(action.type),
setDebugImageViewerScrollId({scrollId: res.scroll_id}),
]),
catchError(error => [new RequestFailed(error), new DeactiveLoader(action.type)])
catchError(error => [requestFailed(error), deactivateLoader(action.type)])
)
)
);
@@ -169,13 +169,13 @@ export class DebugImagesEffects {
} else {
return [
setDebugImageIterations({min_iteration: res.min_iteration, max_iteration: res.max_iteration}),
setCurrentDebugImage({event: res.event}), new DeactiveLoader(action.type),
setCurrentDebugImage({event: res.event}), deactivateLoader(action.type),
setDebugImageViewerScrollId({scrollId: res.scroll_id}),
action.navigateEarlier ? setDisplayerBeginningOfTime({beginningOfTime: false}) : setDisplayerEndOfTime({endOfTime: false})
];
}
}),
catchError(error => [new RequestFailed(error), new DeactiveLoader(action.type)])
catchError(error => [requestFailed(error), deactivateLoader(action.type)])
)
)
);

View File

@@ -1,14 +1,15 @@
<mat-expansion-panel *ngFor="let iteration of iterations; let i = index; trackBy:trackKey"
class="images-section" togglePosition="before" [expanded]="i===0">
class="images-section" [class.dark-theme]="isDarkTheme" togglePosition="before" [expanded]="i===0">
<mat-expansion-panel-header class="debug-header" [collapsedHeight]="null">
<mat-panel-title> {{iteration.iter}}</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="d-flex justify-content flex-wrap">
<sm-debug-image-snippet [frame]="frame"
*ngFor="let frame of iteration.events; let i = index; trackBy:trackFrame"
(imageError)="imageUrlError({frame: frame, experimentId: experimentId})"
(imageClicked)="imageClicked.emit({frame: frame, snippetKey: frame.key, frames: allIterationsEvents})">
<sm-debug-image-snippet
*ngFor="let frame of iteration.events; let i = index; trackBy:trackFrame"
[frame]="frame"
(imageError)="imageUrlError({frame: frame, experimentId: experimentId})"
(imageClicked)="imageClicked.emit({frame: frame, snippetKey: frame.key, frames: allIterationsEvents})">
</sm-debug-image-snippet>
</div>
</ng-template>

View File

@@ -50,6 +50,7 @@
background:transparent;
}
::ng-deep .mat-expansion-panel-content {
.mat-expansion-panel-body {
padding: 0 12px 48px 12px;
@@ -78,7 +79,7 @@
&:hover {
.mat-expansion-panel-header-title {
color: rgba(56, 65, 97, 1)
color: rgba(56, 65, 97, 1);
}
}
@@ -94,4 +95,9 @@
}
}
.mat-expansion-panel {
&.dark-theme {
background: transparent;
}
}
}

View File

@@ -15,6 +15,7 @@ export class DebugImagesViewComponent {
@Input() isMergeIterations;
@Input() title;
@Input() iterations;
@Input() isDarkTheme = false;
@Output() imageClicked = new EventEmitter();
@Output() refreshClicked = new EventEmitter();
@Output() urlError = new EventEmitter();
@@ -22,7 +23,7 @@ export class DebugImagesViewComponent {
public imageUrlError(data: { frame: string; experimentId: string }) {
this.urlError.emit(data);
}
get allIterationsEvents (){
get allIterationsEvents(){
const iterationEvents = [];
this.iterations.forEach(iteration=> iterationEvents.push(iteration.events));
return iterationEvents;

View File

@@ -9,8 +9,8 @@
<div class="d-flex">
<div class="metric-bar" [class.minimized]="minimized" *ngIf="!thereAreNoMetrics(experimentId)">
<label>Metric:</label>
<mat-form-field appearance="outline">
<mat-select #metricSelect (selectionChange)="selectMetric($event, experimentId)" panelClass="light-theme"
<mat-form-field appearance="outline" [ngClass]="{'dark thin': isDarkTheme}">
<mat-select #metricSelect (selectionChange)="selectMetric($event, experimentId)" [panelClass]="isDarkTheme ? 'dark black dark-theme': 'light-theme'"
[value]="selectedMetrics[experimentId]" name="selectedMetric">
<mat-option *ngIf="selectedMetrics[experimentId]" [value]="ALL_IMAGES">{{ALL_IMAGES}}</mat-option>
<mat-option *ngFor="let metric of optionalMetrics[experimentId]" [value]="metric">
@@ -26,7 +26,7 @@
smTooltip="Older images"></div>
<b
class="text-right">{{debugImages && debugImages[experimentId] && debugImages[experimentId][0][debugImages[experimentId].length - 1].iter}}</b>
class="text-right">{{debugImages && debugImages[experimentId] && debugImages[experimentId][0][debugImages[experimentId][0].length - 1].iter}}</b>
<div class="al-icon al-ico-between al-color light-blue-grey"></div>
@@ -43,14 +43,18 @@
smTooltip="Newest images"></div>
</div>
</div>
<div class="no-images icon i-no-debug-samples-with-text"
*ngIf="shouldShowNoImagesForExperiment(experimentId)"></div>
<div class="no-images no-output" [class.dark]="isDarkTheme" *ngIf="shouldShowNoImagesForExperiment(experimentId)">
<svg class="mb-3 w-100" xmlns="http://www.w3.org/2000/svg" width="300" height="150" viewBox="0 0 300 150">
<path opacity="0.1" d="M72.67,79.36a5.39,5.39,0,0,1-1.45,4.32,1.17,1.17,0,0,1-.6.34,1.13,1.13,0,0,1-1.31-.88,1.11,1.11,0,0,1,.28-1,3.19,3.19,0,0,0,.89-2.36c-.22-1.09-1.66-1.73-1.68-1.74A1.12,1.12,0,0,1,69.65,76C69.76,76,72.22,77.07,72.67,79.36ZM46.19,78.1a1.38,1.38,0,0,0-1.06,1.61L47.3,90.38a1.38,1.38,0,0,0,1.61,1.06l4-.82L50.19,77.29Zm30.87.39c-.91-4.54-5.23-7.07-5.41-7.18a1.13,1.13,0,1,0-1.16,1.94s3.63,2.13,4.34,5.68-1.8,7-1.82,7a1.12,1.12,0,0,0,.25,1.56,1.11,1.11,0,0,0,.86.2,1.12,1.12,0,0,0,.68-.43C75,87.09,78,83,77.06,78.49Zm-3.57-12a1.12,1.12,0,0,0-1,2,12.48,12.48,0,0,1,2.91,2.24,13.87,13.87,0,0,1,3.88,7.28,14.19,14.19,0,0,1-.78,8.27,13,13,0,0,1-1.83,3.22,1.11,1.11,0,0,0,1.06,1.82h0a1,1,0,0,0,.63-.36,16.47,16.47,0,0,0,3.13-13.38A16.18,16.18,0,0,0,73.49,66.53Zm-9.28,1a1.35,1.35,0,0,0-1.6-1.06h0a1.45,1.45,0,0,0-.71.4L52.2,76.93l2.7,13.24,12.82,5.52a1.36,1.36,0,0,0,1.79-.7,1.33,1.33,0,0,0,.09-.81ZM261.4,76.66l-3.77,21.62-8.28-8.4-.47,2.7a4.13,4.13,0,0,1-4.76,3.35l-25.67-4.48a4.14,4.14,0,0,1-3.35-4.76l2.36-13.51c0-.12,0-.24.08-.35a6.17,6.17,0,1,1,10-3.71,5.62,5.62,0,0,1-.5,1.55l4.49.79a6.37,6.37,0,0,1,.06-1.63,6.15,6.15,0,1,1,11.63,3.65l4.63.81a4,4,0,0,1,3.4,4.53c0,.08,0,.15-.05.23l-.48,2.7ZM225.28,68.1a3.7,3.7,0,0,0-3.52-3.87h-.19a3.79,3.79,0,1,0,3.71,3.87Zm16.29,3A3.7,3.7,0,0,0,238,67.24h-.19a3.79,3.79,0,1,0,3.72,3.86Zm53.2-20.19L281,129.18a17.25,17.25,0,0,1-20,14l-78.27-13.81a17,17,0,0,1-11.13-7.08,17.86,17.86,0,0,1-1-1.69H130.44a17.86,17.86,0,0,1-1,1.69,17,17,0,0,1-11.13,7.08L40.09,143.16a17.25,17.25,0,0,1-20-14L6.26,50.86a17.26,17.26,0,0,1,14-20h0L94.63,17.77A17.24,17.24,0,0,1,110.76,6.58h79.47a17.24,17.24,0,0,1,16.13,11.19l74.38,13.11a17.25,17.25,0,0,1,14,20ZM120.22,119.58h-9.47a17.21,17.21,0,0,1-17.19-17.19V25.1L21.81,37.75a9.55,9.55,0,0,0-7.74,11.06l13.76,78.06a9.57,9.57,0,0,0,11.06,7.71L117,120.81A9.68,9.68,0,0,0,120.22,119.58Zm69.82-7a9.54,9.54,0,0,0,9.52-9.52V24.13a9.53,9.53,0,0,0-9.5-9.55h-79a9.51,9.51,0,0,0-9.51,9.51v79a9.53,9.53,0,0,0,9.51,9.52Zm95.52-70.94a9.49,9.49,0,0,0-6.17-3.93L207.56,25.1v77.33a17.23,17.23,0,0,1-17.21,17.15h-9.49a9.66,9.66,0,0,0,3.28,1.23l78.15,13.77a9.58,9.58,0,0,0,11.07-7.75l13.78-78.06a9.44,9.44,0,0,0-1.58-7.09Zm-109,.43c1.09.64,1,2.34,1,2.34l-.14,37.87a3.33,3.33,0,0,1-3.36,3.3h0l-48.13-.16a3.37,3.37,0,0,1-3.37-3.35l.2-37.39a3.3,3.3,0,0,1,2.11-3l.18-.08h49.68s1.76-.15,1.83.53ZM160.49,53.34a3.8,3.8,0,0,0,7.59,0,3.7,3.7,0,0,0-3.67-3.76h-.12A3.79,3.79,0,0,0,160.49,53.34ZM171.6,74.93l-8.55-9.46-.19-.19a2.49,2.49,0,0,0-3.52.16v0l-4.2,4.63-9.19-10.63a3.05,3.05,0,0,0-1.59-1,2.75,2.75,0,0,0-2.66.95h0l-13.13,15v4.13l43,.14Z"/></svg>
<h3>NO DEBUG SAMPLES</h3>
</div>
<sm-debug-images-view
*ngFor="let debugImages of debugImages[experimentId]"
[iterations]="debugImages"
[experimentId]="experimentId"
[title]="taskNames && taskNames[experimentId]"
[isMergeIterations]="mergeIterations"
[isDarkTheme]="isDarkTheme"
(imageClicked)="imageClicked($event)"
(urlError)="urlError($event)"
>

View File

@@ -15,12 +15,25 @@ sm-debug-images-view {
}
.no-images {
height: 100%;
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
display: block;
color: $cloudy-blue-two;
width: 100%;
background-repeat: repeat-y;
background-position: center calc(50vh - 300px);
background-size: unset;
font-size: 1.75rem;
font-weight: 500;
line-height: 1.2;
&.dark {
color: $blue-250;
font-size: 12px;
path {
opacity: 1;
fill: $dark-grey-blue;
}
}
}
.metric-bar {
@@ -56,6 +69,7 @@ sm-debug-images-view {
.single-debug-images-container {
display: block;
overflow: hidden;
position: relative;
flex: 1;
min-width: 640px;
min-height: 100%;

View File

@@ -1,4 +1,4 @@
import {ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core';
import {ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {combineLatest, Observable, Subscription} from 'rxjs';
import {select, Store} from '@ngrx/store';
import {IExperimentInfoState} from '../../features/experiments/reducers/experiment-info.reducer';
@@ -35,6 +35,7 @@ import {MatSelectChange} from '@angular/material/select';
})
export class DebugImagesComponent implements OnInit, OnDestroy {
@Input() isDarkTheme = false;
private debugImagesSubscription: Subscription;
private taskNamesSubscription: Subscription;
private selectedExperimentSubscription: Subscription;
@@ -91,10 +92,8 @@ export class DebugImagesComponent implements OnInit, OnDestroy {
const debugImagesP = Object.entries(debugImages).reduce(((previousValue, currentValue: any) => {
previousValue[currentValue[0]] = currentValue[1].metrics.map(metric => metric.iterations.map(iteration => {
const events = iteration.events.map(event => {
const signedUrl = this.adminService.signUrlIfNeeded(event.url);
const parsed = new URL(signedUrl);
parsed.searchParams.append('X-Amz-Date', event.timestamp);
return {...event, oldSrc: event.url, url: parsed.toString(), variantAndMetric: this.selectedMetric === ALL_IMAGES ? `${event.metric}/${event.variant}` : ''};
const url = this.adminService.signUrlIfNeeded(event.url, {disableCache: event.timestamp});
return {...event, oldSrc: event.url, url, variantAndMetric: this.selectedMetric === ALL_IMAGES ? `${event.metric}/${event.variant}` : ''};
});
return {...iteration, events};
}));

View File

@@ -13,6 +13,7 @@ import { debugImagesReducer } from './debug-images-reducer';
import { DebugImagesViewComponent } from './debug-images-view/debug-images-view.component';
import { DebugImagesComponent } from './debug-images.component';
import {MatSliderModule} from "@angular/material/slider";
import {ExperimentGraphsModule} from "../shared/experiment-graphs/experiment-graphs.module";
const declerations = [DebugImagesComponent, DebugImagesViewComponent, ImageDisplayerComponent, DebugImageSnippetComponent];
@@ -27,7 +28,8 @@ const declerations = [DebugImagesComponent, DebugImagesViewComponent, ImageDispl
ScrollingModule,
StoreModule.forFeature('debugImages', debugImagesReducer),
EffectsModule.forFeature([DebugImagesEffects]),
MatSliderModule
MatSliderModule,
ExperimentGraphsModule
]
})
export class DebugImagesModule {

View File

@@ -6,7 +6,7 @@ import {treeBuilderService} from '../services/tree-builder.service';
import {createDiffObjectDetails} from '../jsonToDiffConvertor';
import {ExperimentParams, TreeNode, TreeNodeMetadata} from '../shared/experiments-compare-details.model';
import {MatTreeFlatDataSource, MatTreeFlattener} from '@angular/material/tree';
import {ActiveLoader, AddMessage, DeactiveLoader} from '../../core/actions/layout.actions';
import {activeLoader, addMessage, deactivateLoader} from '../../core/actions/layout.actions';
import { ChangeDetectorRef, OnDestroy, QueryList, ViewChildren, Directive } from '@angular/core';
import {ExperimentCompareDetailsBase} from '../../../features/experiments-compare/experiments-compare-details.base';
import {ActivatedRoute, Router} from '@angular/router';
@@ -20,8 +20,9 @@ import {distinctUntilChanged, filter, map, tap} from 'rxjs/operators';
import {selectHideIdenticalFields, selectRefreshing} from '../reducers';
import {refetchExperimentRequested} from '../actions/compare-header.actions';
import {RENAME_MAP} from '../experiments-compare.constants';
import {selectHasDataFeature} from '../../../core/reducers/users.reducer';
export type nextDiffDirectionEnum = 'down' | 'up';
export type NextDiffDirectionEnum = 'down' | 'up';
export interface FlatNode {
data: any;
@@ -33,10 +34,10 @@ export interface FlatNode {
@Directive()
export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase implements OnDestroy {
public hasDataFeature$: Observable<boolean>;
private hasDataFeature: boolean;
abstract buildCompareTree(experiments);
public RENAME_MAP = RENAME_MAP;
public renameMap = RENAME_MAP;
public experiments$: Observable<any[]>;
public taskIds$: Observable<string>;
@@ -81,6 +82,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
public changeDetection: ChangeDetectorRef,
public activeRoute: ActivatedRoute) {
super();
this.hasDataFeature$ = this.store.pipe(select(selectHasDataFeature));
this.taskIds$ = this.store.pipe(
select(selectRouterParams),
@@ -111,6 +113,9 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
this.refreshingSubscription = this.store.pipe(select(selectRefreshing))
.pipe(filter(({refreshing}) => refreshing))
.subscribe(({autoRefresh}) => this.store.dispatch(refetchExperimentRequested({autoRefresh})));
this.hideIdenticalFieldsSub.add(this.hasDataFeature$.subscribe( hasData => this.hasDataFeature = hasData));
}
ngOnDestroy(): void {
@@ -124,13 +129,13 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
calculateTree(experiments) {
this.calculatingTree = true;
this.store.dispatch(new ActiveLoader('CALCULATING_DIFF_TREE'));
this.store.dispatch(activeLoader('CALCULATING_DIFF_TREE'));
this.changeDetection.detectChanges();
setTimeout(() => {
const experimentTrees = this.buildCompareTree(experiments);
const experimentTrees = this.buildCompareTree(experiments, this.hasDataFeature);
this.tree = experimentTrees;
this.ClearRemovedExperiment(experiments);
this.clearRemovedExperiment(experiments);
const treeFlattener = new MatTreeFlattener<TreeNode<any>, FlatNode>(
this.nodeTransformer,
@@ -157,7 +162,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
this.selectedPath && window.setTimeout(() => this.exapndAndScrollToPath());
});
this.calculatingTree = false;
this.store.dispatch(new DeactiveLoader('CALCULATING_DIFF_TREE'));
this.store.dispatch(deactivateLoader('CALCULATING_DIFF_TREE'));
if (!this.changeDetection['destroyed']) {
this.changeDetection.detectChanges();
}
@@ -186,13 +191,13 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
toggleNode(node) {
Object.keys(this.experimentsDataControl).forEach(id => {
const [dataSource, treeControl] = this.experimentsDataControl[id];
const n = treeControl.dataNodes.filter(n => n.hasChildren).find(n => n.data.path === node.data.path);
const [, treeControl] = this.experimentsDataControl[id];
const n = treeControl.dataNodes.filter(n1 => n1.hasChildren).find(n2 => n2.data.path === node.data.path);
treeControl.toggle(n);
});
}
ClearRemovedExperiment(experiments) {
clearRemovedExperiment(experiments) {
const expIds = experiments.map(exp => exp.id);
Object.keys(this.experimentsDataControl).forEach(expId => {
if (!expIds.includes(expId)) {
@@ -235,7 +240,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
);
}
goToNextDiff(direction: nextDiffDirectionEnum) {
goToNextDiff(direction: NextDiffDirectionEnum) {
if (direction === 'down') {
this.selectedPathIndex = this.onlyDiffsPaths.length - 1 > this.selectedPathIndex ? this.selectedPathIndex + 1 : 0;
} else if (this.selectedPathIndex > 0) {
@@ -262,7 +267,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
let nodeGotExpanded = false;
if (!isEqual(openPaths.slice(0, openPaths.length - 1), this.previousOpenPaths)) {
Object.keys(this.experimentsDataControl).forEach(id => {
const [dataSource, treeControl] = this.experimentsDataControl[id];
const [, treeControl] = this.experimentsDataControl[id];
const nodesToOpen = treeControl.dataNodes.filter(node => node.hasChildren).filter(n => {
const currentPath = n.data.path;
return !treeControl.isExpanded(n) && openPaths.includes(currentPath);
@@ -274,7 +279,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
});
}
this.previousOpenPaths = openPaths.slice(0, openPaths.length - 1);
const [dataSource, treeControl] = Object.values(this.experimentsDataControl)[0];
const [dataSource,] = Object.values(this.experimentsDataControl)[0];
const selectedNodeIndex = this.findRealIndex(dataSource);
const scrollToInPixels = (selectedNodeIndex + 1) * 28 - this.virtualScrollRef.first.getViewportSize() / 2;
if (nodeGotExpanded) {
@@ -346,7 +351,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
{[section]: mergedExperiment[section]},
this.dataTransformer,
this.metaDataTransformer,
{experiment: experiment}
{experiment}
);
}
@@ -428,7 +433,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
}
}
keyClicked(data, event: MouseEvent) {
keyClicked(data) {
const path = data.path;
this.selectedPathClicked(path);
}
@@ -467,7 +472,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
}
copyIdToClipboard() {
this.store.dispatch(new AddMessage('success', 'Copied to clipboard'));
this.store.dispatch(addMessage('success', 'Copied to clipboard'));
}
public resetComponentState(experiments) {

View File

@@ -34,11 +34,11 @@
class="title-key"
[class.ellipsis]="showEllipsis"
[style.width.px]="showEllipsis ? nativeWidth - 45 - node.level * 20 : null"
>{{(RENAME_MAP[node.data.key] || node.data.key) | hideHashTitle}}</span>
>{{(renameMap[node.data.key] || node.data.key) | hideHashTitle}}</span>
<i *ngIf="node.metaData.tooltip" class="al-icon sm al-ico-description node-icon" customClass="hyper-parameters-tooltip" [smTooltip]="node.metaData.tooltip"></i>
</div>
</div>
<div *ngIf="!node.hasChildren" class="section" (click)="keyClicked(node.data, $event)">
<div *ngIf="!node.hasChildren" class="section" (click)="keyClicked(node.data)">
<div [style.padding-left.px]="2 + node.level * 20" [ngClass]="{
'node-item-container': true,
'identical-row': checkIfIdenticalRow(node.data),

View File

@@ -5,7 +5,6 @@ import {
Component,
ElementRef, HostListener,
OnInit, QueryList,
ViewChild,
ViewChildren
} from '@angular/core';
import {select, Store} from '@ngrx/store';
@@ -59,7 +58,6 @@ export class ExperimentCompareDetailsComponent extends ExperimentCompareBase imp
ngOnInit() {
this.onInit();
this.routerParamsSubscription = this.taskIds$.subscribe((experimentIds: string) => this.store.dispatch(experimentListUpdated({ids: experimentIds.split(',')})));
this.experimentsSubscription = this.experiments$.pipe(
@@ -94,11 +92,11 @@ export class ExperimentCompareDetailsComponent extends ExperimentCompareBase imp
});
}
buildCompareTree(experiments: Array<IExperimentDetail>): ExperimentCompareTree {
buildCompareTree(experiments: Array<IExperimentDetail>, hasDataFeature?: boolean): ExperimentCompareTree {
const mergedExperiment = getAllKeysEmptyObject(experiments);
return experiments
.reduce((acc, cur) => {
acc[cur.id] = this.buildExperimentTree(cur, this.baseExperiment, mergedExperiment);
acc[cur.id] = this.buildExperimentTree(cur, this.baseExperiment, mergedExperiment, hasDataFeature);
return acc;
}, {} as ExperimentCompareTree);

View File

@@ -76,7 +76,7 @@
></sm-parallel-coordinates-graph>
<ng-template #no_data>
<div class="d-flex align-items-center justify-content-center flex-column h-100 no-data">
<div class="icon i-no-data-graph"></div>
<div class="al-icon al-ico-no-data-graph"></div>
<div class="no-data-title">No data to show</div>
<div>Please select parameters & metric</div>
</div>

View File

@@ -165,9 +165,11 @@
}
}
.i-no-data-graph {
width: 300px;
height: 300px;
.al-ico-no-data-graph {
font-size: 240px;
width: 240px;
height: 240px;
color: #e5e5e5;
}
sm-grouped-checked-filter-list {

View File

@@ -1,17 +1,17 @@
import {Component, HostListener, OnDestroy, OnInit, ElementRef, ViewChild} from '@angular/core';
import {combineLatest, Observable, Subscription} from 'rxjs';
import {ExperimentGraph} from '../../../tasks/tasks.model';
import {select, Store} from '@ngrx/store';
import {IExperimentInfoState} from '../../../../features/experiments/reducers/experiment-info.reducer';
import {distinctUntilChanged, distinctUntilKeyChanged, filter, map} from 'rxjs/operators';
import {distinctUntilChanged, filter, map} from 'rxjs/operators';
import {selectRouterParams} from '../../../core/reducers/router-reducer';
import {get, has} from 'lodash/fp';
import {SetExperimentSettings, SetSelectedExperiments} from '../../actions/experiments-compare-charts.actions';
import {selectRefreshing, selectScalarsGraphHyperParams, selectScalarsGraphMetrics, selectScalarsGraphShowIdenticalHyperParams, selectScalarsGraphTasks, selectMetricValueType, selectSelectedSettigsHyperParams, selectSelectedSettigsMetric} from '../../reducers';
import {getExperimentsHyperParams, setShowIdenticalHyperParams, setvalueType} from '../../actions/experiments-compare-scalars-graph.actions';
import {GroupedHyperParams, HyperParams, MetricOption, MetricValueType, SelectedMetric, VariantOption} from '../../reducers/experiments-compare-charts.reducer';
import {GroupedHyperParams, MetricOption, MetricValueType, SelectedMetric, VariantOption} from '../../reducers/experiments-compare-charts.reducer';
import {MatRadioChange} from '@angular/material/radio';
import {selectPlotlyReady} from '../../../core/reducers/view-reducer';
import {ExtFrame} from '../../../shared/experiment-graphs/single-graph/plotly-graph-base';
export const _filter = (opt: VariantOption[], value: string): VariantOption[] => {
@@ -40,7 +40,7 @@ export class ExperimentCompareHyperParamsGraphComponent implements OnInit, OnDes
private selectRefreshing$: Observable<{ refreshing: boolean, autoRefresh: boolean }>;
public experiments$: Observable<any[]>;
public graphs: { [key: string]: ExperimentGraph };
public graphs: { [key: string]: ExtFrame };
public selectedHyperParams: string[];
public selectedMetric: SelectedMetric;
public hyperParams: { [section: string]: any};

View File

@@ -1,6 +1,5 @@
import {ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core';
import {Observable, Subscription} from 'rxjs';
import {ExperimentGraph} from '../../../tasks/tasks.model';
import {select, Store} from '@ngrx/store';
import {IExperimentInfoState} from '../../../../features/experiments/reducers/experiment-info.reducer';
import {distinctUntilChanged, filter, map, tap} from 'rxjs/operators';
@@ -14,6 +13,7 @@ import {ScalarKeyEnum} from '../../../../business-logic/model/events/scalarKeyEn
import {toggleShowScalarOptions} from '../../actions/compare-header.actions';
import {GroupByCharts} from '../../../experiments/reducers/common-experiment-output.reducer';
import {GroupedList} from '../../../shared/ui-components/data/selectable-grouped-filter-list/selectable-grouped-filter-list.component';
import {ExtFrame} from '../../../shared/experiment-graphs/single-graph/plotly-graph-base';
@Component({
@@ -44,7 +44,7 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
public graphList: GroupedList = {};
public selectedGraph: string = null;
private taskIds: Array<string>;
public graphs: { [key: string]: ExperimentGraph };
public graphs: { [key: string]: ExtFrame[] };
public refreshDisabled = false;
public showSettingsBar: boolean = false;
public groupBy: GroupByCharts;
@@ -138,7 +138,7 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
}
}
private buildNestedListWithoutChildren(merged: { [p: string]: ExperimentGraph }) {
private buildNestedListWithoutChildren(merged: { [p: string]: ExtFrame[] }) {
return Object.keys(merged).reduce((acc, metric) => {
acc[metric] = {};
return acc;

View File

@@ -7,7 +7,7 @@ import {Observable, Subscription} from 'rxjs';
import * as metricsValuesActions from '../../actions/experiments-compare-metrics-values.actions';
import {selectCompareMetricsValuesExperiments, selectCompareMetricsValuesSortConfig, selectRefreshing} from '../../reducers';
import {Router} from '@angular/router';
import {AddMessage} from '../../../core/actions/layout.actions';
import {addMessage} from '../../../core/actions/layout.actions';
import {TreeNode} from '../../shared/experiments-compare-details.model';
import {createDiffObjectScalars, getAllKeysEmptyObject} from '../../jsonToDiffConvertor';
@@ -187,6 +187,6 @@ export class ExperimentCompareMetricValuesComponent implements OnInit, OnDestroy
}
copyIdToClipboard() {
this.store.dispatch(new AddMessage('success', 'Copied to clipboard'));
this.store.dispatch(addMessage('success', 'Copied to clipboard'));
}
}

View File

@@ -29,10 +29,10 @@
[class.selected-diff]="checkIfSelectedPath(node.data)"
[class.identical-row]="!allPaths[node.data.path]">
<i class="fas" [style.margin-left.px]="2 + node.level * 20" [ngClass]="treeControl.isExpanded(node) ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
<span class="title-key" [class.ellipsis]="showEllipsis">{{(RENAME_MAP[node.data.key] || node.data.key)}}</span>
<span class="title-key" [class.ellipsis]="showEllipsis">{{(renameMap[node.data.key] || node.data.key)}}</span>
</div>
</div>
<div *ngIf="!node.hasChildren" class="section" (click)="keyClicked(node.data, $event)">
<div *ngIf="!node.hasChildren" class="section" (click)="keyClicked(node.data)">
<div [style.padding-left.px]="2 + node.level * 20" [ngClass]="{
'node-item-container': true,
'identical-row': checkIfIdenticalRow(node.data),

View File

@@ -1,7 +1,6 @@
import {ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core';
import {SelectableListItem} from '../../../shared/ui-components/data/selectable-list/selectable-list.model';
import {Observable, Subscription} from 'rxjs';
import {ExperimentGraph} from '../../../tasks/tasks.model';
import {select, Store} from '@ngrx/store';
import {IExperimentInfoState} from '../../../../features/experiments/reducers/experiment-info.reducer';
import {distinctUntilChanged, filter, map, tap} from 'rxjs/operators';
@@ -11,6 +10,7 @@ import {isEqual} from 'lodash/fp';
import {scrollToElement} from '../../../shared/utils/shared-utils';
import {GetMultiPlotCharts, ResetExperimentMetrics, SetExperimentMetricsSearchTerm, SetExperimentSettings, SetSelectedExperiments} from '../../actions/experiments-compare-charts.actions';
import {selectCompareTasksPlotCharts, selectExperimentMetricsSearchTerm, selectRefreshing, selectSelectedExperimentSettings, selectSelectedSettingsHiddenPlot} from '../../reducers';
import {ExtFrame} from '../../../shared/experiment-graphs/single-graph/plotly-graph-base';
@Component({
selector: 'sm-experiment-compare-plots',
@@ -31,11 +31,11 @@ export class ExperimentComparePlotsComponent implements OnInit, OnDestroy {
private routerParamsSubscription: Subscription;
private refreshingSubscription: Subscription;
public graphList: Array<SelectableListItem> = [];
public graphList: SelectableListItem[] = [];
public selectedGraph: string = null;
private experimentId: string;
private taskIds: Array<string>;
public graphs: { [key: string]: ExperimentGraph };
public graphs: { [key: string]: ExtFrame[] };
public refreshDisabled: boolean;

Some files were not shown because too many files have changed in this diff Show More