Release v1.13 (#64)

Co-authored-by: shallegro <shay@allego.ai>
This commit is contained in:
shyallegro
2023-11-17 10:24:49 +02:00
committed by GitHub
parent aa038f4f82
commit 34f2167598
537 changed files with 18795 additions and 13775 deletions

View File

@@ -8,28 +8,16 @@
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/ng-cli-compat",
"plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
"plugin:@angular-eslint/template/process-inline-templates"
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates",
"plugin:@ngrx/recommended-requiring-type-checking"
],
"rules": {
"no-console": "error",
"no-debugger": "error",
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "sm",
"style": "kebab-case"
}
],
"@angular-eslint/directive-selector": [
"error",
{
@@ -38,49 +26,15 @@
"style": "camelCase"
}
],
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/member-ordering": "off",
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/no-inferrable-types": [
"off",
{
"ignoreParameters": true
}
],
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-use-before-define": "error",
"brace-style": [
"error",
"1tbs"
],
"eqeqeq": [
"off",
"smart"
],
"id-blacklist": "off",
"id-match": "off",
"max-len": [
"@angular-eslint/component-selector": [
"error",
{
"code": 2000
"type": "element",
"prefix": "sm",
"style": "kebab-case"
}
],
"no-bitwise": "off",
"no-shadow": [
"off",
{
"hoist": "all"
}
],
"no-underscore-dangle": "off",
"valid-typeof": "error"
"@ngrx/prefer-effect-callback-in-block-statement": "off"
}
},
{
@@ -88,7 +42,8 @@
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
"plugin:@angular-eslint/template/recommended",
"plugin:@angular-eslint/template/accessibility"
],
"rules": {
"@angular-eslint/template/use-track-by-function": "warn"

View File

@@ -2,16 +2,22 @@
## Building the UI from source
### Prerequisite
* a linux machine
* Node 16 with latest npm
* clone the project to your local machine
* Node 18 with latest npm
* use git to clone the project to your local machine
### build
* `cd` to the root of the project
### Build
* `cd clearml-web` to the root of the project
* run `npm ci` to install required node modules
* run `npm run build`
## Application Structure
### Development
During development, the development server will need to proxy an API server. to achieve that:
* in [proxy.config.js](proxy.config.js) update the list of targets in line 3 with a working API server URI.
* Angular is already configured to use this proxy configuration
* If more than 1 API server is configured `apiBaseUrl` should be updated with the server enumeration in [environment.ts](src%2Fenvironments%2Fenvironment.ts)
Start the development server: `npm run start`
#### Business Logic module
Contains ClearML logic. api calls and ClearML objects (e.g tasks, models) and ClearML logic function (e.g isTaskHidden)

View File

@@ -43,8 +43,7 @@
"src/fonts.scss"
],
"scripts": [
"node_modules/ngx-markdown-editor/assets/highlight.js/highlight.min.js",
"node_modules/ngx-markdown-editor/assets/marked.min.js"
"node_modules/ngx-markdown-editor/assets/highlight.js/highlight.min.js"
],
"allowedCommonJsDependencies": [
"ansi-to-html",
@@ -65,7 +64,9 @@
"dom-to-image",
"ace-builds",
"hocon-parser",
"taira"
"taira",
"base-64",
"export-to-csv"
],
"vendorChunk": true,
"extractLicenses": false,
@@ -319,5 +320,8 @@
"@angular-eslint/schematics:library": {
"setParserOptionsProject": true
}
},
"cli": {
"analytics": false
}
}

20374
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "clearml-webapp",
"version": "1.12.0",
"version": "1.13.0",
"license": "",
"scripts": {
"ng": "ng",
@@ -19,34 +19,33 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.8",
"@angular/cdk": "^15.2.8",
"@angular/common": "^15.2.8",
"@angular/compiler": "^15.2.8",
"@angular/core": "^15.2.8",
"@angular/forms": "^15.2.8",
"@angular/material": "^15.2.8",
"@angular/platform-browser": "^15.2.8",
"@angular/platform-browser-dynamic": "^15.2.8",
"@angular/platform-server": "^15.2.8",
"@angular/router": "^15.2.8",
"@angular/service-worker": "^15.2.8",
"@angular/youtube-player": "^15.2.8",
"@aws-sdk/client-s3": "^3.360.0",
"@aws-sdk/s3-request-presigner": "^3.360.0",
"@ctrl/ngx-github-buttons": "^8.0.0",
"@angular/animations": "^16.1.5",
"@angular/cdk": "16.1.5",
"@angular/common": "^16.1.5",
"@angular/compiler": "^16.1.5",
"@angular/core": "^16.1.5",
"@angular/forms": "^16.1.5",
"@angular/material": "16.1.5",
"@angular/platform-browser": "^16.1.5",
"@angular/platform-browser-dynamic": "^16.1.5",
"@angular/platform-server": "^16.1.5",
"@angular/router": "^16.1.5",
"@angular/service-worker": "^16.1.5",
"@angular/youtube-player": "^16.1.5",
"@aws-sdk/client-s3": "^3.370.0",
"@aws-sdk/s3-request-presigner": "^3.370.0",
"@ctrl/ngx-github-buttons": "^9.0.0",
"@ctrl/tinycolor": "^4.0.1",
"@ngneat/dag": "^2.0.0",
"@ngrx/effects": "^15.4.0",
"@ngrx/entity": "^15.4.0",
"@ngrx/router-store": "^15.4.0",
"@ngrx/store": "^15.4.0",
"ace-builds": "^1.18.0",
"angular-google-tag-manager": "^1.7.0",
"@ngrx/effects": "^16.1.0",
"@ngrx/entity": "^16.1.0",
"@ngrx/router-store": "^16.1.0",
"@ngrx/store": "^16.1.0",
"ace-builds": "^1.23.4",
"angular-resizable-element": "^7.0.2",
"angular-split": "^15.0.0",
"angular2-csv": "^0.2.9",
"ansi-to-html": "^0.7.2",
"bootstrap": "^5.2.3",
"bootstrap": "^5.3.0",
"britecharts": "^3.0.0-alpha-6.1.6",
"curved-arrows": "^0.1.0",
"d3-selection": "^3.0.0",
@@ -54,6 +53,8 @@
"d3-zoom": "^3.0.0",
"diff": "^5.1.0",
"dom-to-image": "^2.6.0",
"dompurify": "^3.0.6",
"export-to-csv": "^0.2.1",
"filesize": "^10.0.7",
"has-ansi": "^5.0.1",
"hocon-parser": "^1.0.1",
@@ -61,53 +62,54 @@
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"lucene": "^2.1.1",
"marked": "^7.0.5",
"ngx-clipboard": "^16.0.0",
"ngx-color-picker": "^14.0.0",
"ngx-device-detector": "^5.0.1",
"ngx-device-detector": "^6.0.2",
"ngx-markdown-editor": "^5.3.1",
"ngx-print": "^1.3.1",
"ngx-window-token": "^7.0.0",
"object-hash": "^3.0.0",
"primeicons": "^6.0.1",
"primeng": "^15.4.1",
"primeng": "^16.2.0",
"process": "^0.11.10",
"rxjs": "^7.8.0",
"rxjs": "^7.8.1",
"string-to-color": "^2.2.2",
"taira": "^3.2.2",
"tinycolor2": "^1.6.0",
"tslib": "^2.5.0",
"url": "^0.11.0",
"tslib": "^2.6.0",
"url": "^0.11.1",
"uuid": "^9.0.0",
"zone.js": "^0.12.0"
"zone.js": "~0.13.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.6",
"@angular-devkit/core": "^15.2.6",
"@angular-devkit/schematics": "^15.2.6",
"@angular-devkit/schematics-cli": "^15.2.5",
"@angular-eslint/builder": "^15.2.1",
"@angular-eslint/eslint-plugin": "^15.2.1",
"@angular-eslint/eslint-plugin-template": "^15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "^15.2.1",
"@angular/cli": "^15.2.6",
"@angular/compiler-cli": "^15.2.8",
"@angular/language-service": "^15.2.8",
"@angular-devkit/build-angular": "^16.1.4",
"@angular-devkit/core": "^16.1.4",
"@angular-devkit/schematics": "^16.1.4",
"@angular-devkit/schematics-cli": "^16.1.4",
"@angular-eslint/builder": "16.1.2",
"@angular-eslint/eslint-plugin": "16.1.2",
"@angular-eslint/eslint-plugin-template": "16.1.2",
"@angular-eslint/schematics": "16.1.2",
"@angular-eslint/template-parser": "^16.1.2",
"@angular/cli": "^16.1.4",
"@angular/compiler-cli": "^16.1.5",
"@angular/language-service": "^16.1.5",
"@fortawesome/fontawesome-free": "^6.4.0",
"@ngrx/schematics": "^15.4.0",
"@ngrx/store-devtools": "^15.4.0",
"@ngrx/eslint-plugin": "^16.2.0",
"@ngrx/schematics": "^16.1.0",
"@ngrx/store-devtools": "^16.1.0",
"@types/d3-selection": "^3.0.5",
"@types/lodash-es": "^4.17.7",
"@types/lodash-es": "^4.17.8",
"@types/node": "^18.16.0",
"@types/plotly.js": "^2.12.18",
"@types/plotly.js": "^2.12.24",
"@types/tinycolor2": "^1.4.3",
"@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"codelyzer": "^6.0.2",
"eslint": "^8.39.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsdoc": "41.1.2",
"eslint-plugin-prefer-arrow": "1.2.3",
"typescript": "~4.9.5"
"@typescript-eslint/eslint-plugin": "6.7.0",
"@typescript-eslint/parser": "6.7.0",
"eslint": "^8.49.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsdoc": "^46.6.0",
"eslint-plugin-prefer-arrow": "^1.2.3",
"typescript": "~5.1.6"
}
}

View File

@@ -1,7 +1,7 @@
import {ApiUsersService} from './business-logic/api-services/users.service';
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 {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
import {Title} from '@angular/platform-browser';
import {selectBreadcrumbs, selectLoggedOut} from '@common/core/reducers/view.reducer';
import {Store} from '@ngrx/store';
@@ -9,11 +9,11 @@ import {selectRouterParams, selectRouterUrl} from '@common/core/reducers/router-
import {ApiProjectsService} from './business-logic/api-services/projects.service';
import {Project} from './business-logic/model/projects/project';
import {getAllSystemProjects, setSelectedProjectId, updateProject} from '@common/core/actions/projects.actions';
import {selectSelectedProject} from '@common/core/reducers/projects.reducer';
import {selectRouterProjectId, selectSelectedProject} from '@common/core/reducers/projects.reducer';
import {MatDialog} from '@angular/material/dialog';
import {getTutorialBucketCredentials} from '@common/core/actions/common-auth.actions';
import {termsOfUseAccepted} from '@common/core/actions/users.actions';
import {distinctUntilChanged, filter, map, tap} from 'rxjs/operators';
import {distinctUntilChanged, filter, tap} from 'rxjs/operators';
import * as routerActions from './webapp-common/core/actions/router.actions';
import {combineLatest, Observable, Subscription} from 'rxjs';
import {ServerUpdatesService} from '@common/shared/services/server-updates.service';
@@ -24,14 +24,13 @@ 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 '@common/shared/utils/shared-utils';
import {User} from './business-logic/model/users/user';
import {ConfigurationService} from '@common/shared/services/configuration.service';
import {GoogleTagManagerService} from 'angular-google-tag-manager';
import {selectIsSharedAndNotOwner} from './features/experiments/reducers';
import {TipsService} from '@common/shared/services/tips.service';
import {USER_PREFERENCES_KEY} from '@common/user-preferences';
import {Environment} from '../environments/base';
import {loadExternalLibrary} from '@common/shared/utils/load-external-library';
import {User} from '~/business-logic/model/users/user';
@Component({
selector: 'sm-root',
@@ -40,17 +39,14 @@ import {loadExternalLibrary} from '@common/shared/utils/load-external-library';
encapsulation: ViewEncapsulation.None
})
export class AppComponent implements OnInit, OnDestroy {
public loggedOut$: Observable<any>;
public activeFeature: string;
public loggedOut$: Observable<boolean>;
private urlSubscription: Subscription;
public selectedProject$: Observable<Project>;
public projectId: string;
public isWorkersContext: boolean;
public updatesAvailable$: Observable<any>;
private selectedProjectFromUrl$: Observable<string>;
public updatesAvailable$: Observable<string>;
private breadcrumbsSubscription: Subscription;
private selectedCurrentUserSubscription: Subscription;
private selectedCurrentUser$: Observable<any>;
public showNotification: boolean = true;
public demo = ConfigurationService.globalEnvironment.demo;
public isLoginContext: boolean;
@@ -77,7 +73,7 @@ export class AppComponent implements OnInit, OnDestroy {
private router: Router,
private route: ActivatedRoute,
private titleService: Title,
private store: Store<any>,
private store: Store,
private projectsApi: ApiProjectsService,
private userService: ApiUsersService,
public serverUpdatesService: ServerUpdatesService,
@@ -93,16 +89,6 @@ export class AppComponent implements OnInit, OnDestroy {
this.isSharedAndNotOwner$ = this.store.select(selectIsSharedAndNotOwner);
this.selectedProject$ = this.store.select(selectSelectedProject);
this.updatesAvailable$ = this.store.select(selectAvailableUpdates);
this.selectedCurrentUser$ = this.store.select(selectCurrentUser);
this.selectedProjectFromUrl$ = this.store.select(selectRouterParams)
.pipe(
filter((params: Params) => !!params),
map(params => params?.projectId || null)
);
if (ConfigurationService.globalEnvironment.GTM_ID) {
this.gtmService = injector.get(GoogleTagManagerService);
}
}
ngOnInit(): void {
@@ -129,7 +115,7 @@ export class AppComponent implements OnInit, OnDestroy {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(
(item: RouterEvent) => {
(item: NavigationEnd) => {
const gtmTag = {
event: 'page',
pageName: item.url
@@ -138,8 +124,8 @@ export class AppComponent implements OnInit, OnDestroy {
this.store.dispatch(routerActions.navigationEnd());
});
this.selectedCurrentUserSubscription = this.selectedCurrentUser$.pipe(
tap(user => this.currentUser = user), // should not be filtered
this.selectedCurrentUserSubscription = this.store.select(selectCurrentUser).pipe(
tap(user => this.currentUser = user as unknown as User),
filter(user => !!user?.id),
distinctUntilChanged((prev, next) => prev?.id === next?.id)
)
@@ -158,27 +144,23 @@ export class AppComponent implements OnInit, OnDestroy {
}
});
this.selectedProjectFromUrl$.subscribe((projectId: string) => {
this.store.select(selectRouterProjectId).subscribe((projectId: string) => {
this.store.dispatch(setSelectedProjectId({projectId}));
});
this.urlSubscription = combineLatest([this.store.select(selectRouterUrl), this.store.select(selectRouterParams)])
this.urlSubscription = combineLatest([
this.store.select(selectRouterUrl),
this.store.select(selectRouterParams)
])
.subscribe(([url, params]) => {
this.projectId = params?.projectId;
this.isLoginContext = url && url.includes('login');
this.isWorkersContext = url && url.includes('workers-and-queues');
if (this.projectId) {
try { // TODO: refactor to a better solution after all navbar are declared...
this.activeFeature = url.split(this.projectId)[1].split('/')[1];
} catch (e) {
}
}
});
this.breadcrumbsSubscription = this.store.select(selectBreadcrumbs).subscribe(breadcrumbs => {
const crumbs = breadcrumbs.flat().map(breadcrumb=> breadcrumb.name);
this.titleService.setTitle(`${this.title ? this.title + '-' : ''} ${crumbs.join(' / ')}`);
const crumbs = breadcrumbs.flat().filter((breadcrumb => !!breadcrumb?.name)).map(breadcrumb => breadcrumb.name);
crumbs.length> 0 && this.titleService.setTitle(`${this.title ? this.title + '-' : ''} ${crumbs.join(' / ')}`);
});
if (window.localStorage.getItem('disableHidpi') !== 'true') {

View File

@@ -12,7 +12,7 @@ import {CommonLayoutModule} from '@common/layout/layout.module';
import {HTTP_INTERCEPTORS} from '@angular/common/http';
import {WebappInterceptor} from '@common/core/interceptors/webapp-interceptor';
import {CustomReuseStrategy} from '@common/core/router-reuse-strategy';
import {loadUserAndPreferences, UserPreferences} from '@common/user-preferences';
import {UserPreferences} from '@common/user-preferences';
import {AngularSplitModule} from 'angular-split';
import {NotifierModule} from '@common/angular-notifier';
import {LayoutModule} from './layout/layout.module';
@@ -23,6 +23,8 @@ import {ProjectsSharedModule} from './features/projects/shared/projects-shared.m
import {MAT_FORM_FIELD_DEFAULT_OPTIONS} from '@angular/material/form-field';
import {LoginService} from '~/shared/services/login.service';
import {ExperimentSharedModule} from '~/features/experiments/shared/experiment-shared.module';
import {loadUserAndPreferences} from '~/core/app-init';
import {MAT_TOOLTIP_DEFAULT_OPTIONS, MatTooltipDefaultOptions} from '@angular/material/tooltip';
@NgModule({
declarations : [AppComponent],
@@ -69,11 +71,9 @@ import {ExperimentSharedModule} from '~/features/experiments/shared/experiment-s
{provide: HTTP_INTERCEPTORS, useClass: WebappInterceptor, multi: true},
{provide: RouteReuseStrategy, useClass: CustomReuseStrategy},
{
provide: 'googleTagManagerId',
deps: [ConfigurationService],
useFactory: (confService: ConfigurationService) =>
confService.getStaticEnvironment().GTM_ID
},
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {position: 'above'} as MatTooltipDefaultOptions
}
],
bootstrap : [AppComponent],
exports : []

View File

@@ -66,6 +66,8 @@ import { ModelsUpdateResponse } from '../model/models/modelsUpdateResponse';
import { BASE_PATH, COLLECTION_FORMATS } from '../variables';
import { Configuration } from '../configuration';
import {ModelsUpdateTagsRequest} from '~/business-logic/model/models/modelsUpdateTagsRequest';
import {ModelsUpdateTagsResponse} from '~/business-logic/model/models/modelsUpdateTagsResponse';
@Injectable()
@@ -101,7 +103,7 @@ export class ApiModelsService {
/**
*
*
* Add or update model metadata
* @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.
@@ -146,7 +148,7 @@ export class ApiModelsService {
}
/**
*
*
* Archive models
* @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.
@@ -191,7 +193,7 @@ export class ApiModelsService {
}
/**
*
*
* Create a new model not associated with a task
* @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.
@@ -236,7 +238,7 @@ export class ApiModelsService {
}
/**
*
*
* Delete a model.
* @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.
@@ -281,7 +283,7 @@ export class ApiModelsService {
}
/**
*
*
* Delete models
* @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.
@@ -326,7 +328,7 @@ export class ApiModelsService {
}
/**
*
*
* Delete metadata from model
* @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.
@@ -371,7 +373,7 @@ export class ApiModelsService {
}
/**
*
*
* Edit an existing model
* @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.
@@ -416,7 +418,7 @@ export class ApiModelsService {
}
/**
*
*
* Get all models
* @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.
@@ -461,7 +463,7 @@ export class ApiModelsService {
}
/**
*
*
* Get all models
* @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.
@@ -506,7 +508,7 @@ export class ApiModelsService {
}
/**
*
*
* Gets model information
* @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.
@@ -551,7 +553,7 @@ export class ApiModelsService {
}
/**
*
*
* Get all models
* @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.
@@ -596,7 +598,7 @@ export class ApiModelsService {
}
/**
*
*
* Gets model information
* @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.
@@ -641,7 +643,7 @@ export class ApiModelsService {
}
/**
*
*
* Get the list of frameworks used in the company models
* @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.
@@ -686,7 +688,7 @@ export class ApiModelsService {
}
/**
*
*
* Convert public models to private
* @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.
@@ -731,7 +733,7 @@ export class ApiModelsService {
}
/**
*
*
* Convert company models to public
* @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.
@@ -776,7 +778,7 @@ export class ApiModelsService {
}
/**
*
*
* Move models to a project
* @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.
@@ -821,7 +823,7 @@ export class ApiModelsService {
}
/**
*
*
* Publish models
* @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.
@@ -866,7 +868,7 @@ export class ApiModelsService {
}
/**
*
*
* Set the model ready flag to True. If the model is an output model of a task then try to publish the task.
* @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.
@@ -911,7 +913,7 @@ export class ApiModelsService {
}
/**
*
*
* Unarchive models
* @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.
@@ -956,7 +958,7 @@ export class ApiModelsService {
}
/**
*
*
* Update a model
* @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.
@@ -1001,7 +1003,7 @@ export class ApiModelsService {
}
/**
*
*
* Create or update a new model for a task
* @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.
@@ -1045,4 +1047,48 @@ export class ApiModelsService {
);
}
/**
*
* Add/Remove multiple tags from multiple models
* @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 modelsUpdateTags(request: ModelsUpdateTagsRequest, 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 modelsUpdateTags.');
}
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<ModelsUpdateTagsResponse>(`${this.basePath}/models.update_tags`,
request,
{
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
}

View File

@@ -115,6 +115,8 @@ import { TasksValidateRequest } from '../model/tasks/tasksValidateRequest';
import { BASE_PATH, COLLECTION_FORMATS } from '../variables';
import { Configuration } from '../configuration';
import {TasksUpdateTagsResponse} from '~/business-logic/model/tasks/tasksUpdateTagsResponse';
import {TasksUpdateTagsRequest} from '~/business-logic/model/tasks/tasksUpdateTagsRequest';
@Injectable()
@@ -150,7 +152,7 @@ export class ApiTasksService {
/**
*
*
* Update existing artifacts (search by key/mode) and add new ones
* @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.
@@ -195,7 +197,7 @@ export class ApiTasksService {
}
/**
*
*
* Add or update task model
* @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.
@@ -240,7 +242,7 @@ export class ApiTasksService {
}
/**
*
*
* Archive tasks. If a task is queued it will first be dequeued and then archived.
* @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.
@@ -285,7 +287,7 @@ export class ApiTasksService {
}
/**
*
*
* Archive tasks
* @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.
@@ -330,7 +332,7 @@ export class ApiTasksService {
}
/**
*
*
* Clone an existing task
* @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.
@@ -375,7 +377,7 @@ export class ApiTasksService {
}
/**
*
*
* Indicates that task is closed
* @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.
@@ -420,7 +422,7 @@ export class ApiTasksService {
}
/**
*
*
* Signal a task has completed
* @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.
@@ -465,7 +467,7 @@ export class ApiTasksService {
}
/**
*
*
* Create a new task
* @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.
@@ -510,7 +512,7 @@ export class ApiTasksService {
}
/**
*
*
* Delete a task along with any information stored for it (statistics, frame updates etc.) Unless Force flag is provided, operation will fail if task has objects associated with it - i.e. children tasks and projects. Models that refer to the deleted task will be updated with a task ID indicating a deleted task.
* @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.
@@ -555,7 +557,7 @@ export class ApiTasksService {
}
/**
*
*
* Delete existing artifacts (search by key/mode)
* @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.
@@ -600,7 +602,7 @@ export class ApiTasksService {
}
/**
*
*
* Delete task configuration items
* @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.
@@ -645,7 +647,7 @@ export class ApiTasksService {
}
/**
*
*
* Delete task hyper parameters
* @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.
@@ -690,7 +692,7 @@ export class ApiTasksService {
}
/**
*
*
* Delete tasks
* @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.
@@ -735,7 +737,7 @@ export class ApiTasksService {
}
/**
*
*
* Delete models from task
* @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.
@@ -780,7 +782,7 @@ export class ApiTasksService {
}
/**
*
*
* Remove a task from its queue. Fails if task status is not queued.
* @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.
@@ -825,7 +827,7 @@ export class ApiTasksService {
}
/**
*
*
* Dequeue tasks
* @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.
@@ -870,7 +872,7 @@ export class ApiTasksService {
}
/**
*
*
* Edit task\&#39;s details.
* @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.
@@ -915,7 +917,7 @@ export class ApiTasksService {
}
/**
*
*
* Add or update task configuration
* @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.
@@ -960,7 +962,7 @@ export class ApiTasksService {
}
/**
*
*
* Add or update task hyper parameters
* @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.
@@ -1005,7 +1007,7 @@ export class ApiTasksService {
}
/**
*
*
* Adds a task into a queue. Fails if task state is not \&#39;created\&#39;. Fails if the following parameters in the task were not filled: * execution.script.repository * execution.script.entrypoint
* @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.
@@ -1050,7 +1052,7 @@ export class ApiTasksService {
}
/**
*
*
* Enqueue tasks
* @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.
@@ -1095,7 +1097,7 @@ export class ApiTasksService {
}
/**
*
*
* Indicates that task has failed
* @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.
@@ -1140,7 +1142,7 @@ export class ApiTasksService {
}
/**
*
*
* Get all the company\&#39;s tasks and all public tasks
* @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.
@@ -1185,7 +1187,7 @@ export class ApiTasksService {
}
/**
*
*
* Get all the company\&#39;s tasks and all public tasks
* @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.
@@ -1230,7 +1232,7 @@ export class ApiTasksService {
}
/**
*
*
* Gets task information
* @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.
@@ -1275,7 +1277,7 @@ export class ApiTasksService {
}
/**
*
*
* Get all the company\&#39;s tasks and all public tasks
* @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.
@@ -1320,7 +1322,7 @@ export class ApiTasksService {
}
/**
*
*
* Get the list of task configuration items names
* @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.
@@ -1365,7 +1367,7 @@ export class ApiTasksService {
}
/**
*
*
* Get the list of task configurations
* @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.
@@ -1410,7 +1412,7 @@ export class ApiTasksService {
}
/**
*
*
* Get the list of task hyper parameters
* @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.
@@ -1455,7 +1457,7 @@ export class ApiTasksService {
}
/**
*
*
* Get the list of task types used in the specified projects
* @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.
@@ -1500,7 +1502,7 @@ export class ApiTasksService {
}
/**
*
*
* Convert public tasks to private
* @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.
@@ -1545,7 +1547,7 @@ export class ApiTasksService {
}
/**
*
*
* Convert company tasks to public
* @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.
@@ -1590,7 +1592,7 @@ export class ApiTasksService {
}
/**
*
*
* Move tasks to a project
* @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.
@@ -1635,7 +1637,7 @@ export class ApiTasksService {
}
/**
*
*
* Refresh the task\&#39;s last update time
* @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.
@@ -1680,7 +1682,7 @@ export class ApiTasksService {
}
/**
*
*
* Mark a task status as published. For Annotation tasks - if any changes were committed by this task, a new version in the dataset together with an output view are created. For Training tasks - if a model was created, it should be set to ready.
* @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.
@@ -1725,7 +1727,7 @@ export class ApiTasksService {
}
/**
*
*
* Publish tasks
* @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.
@@ -1770,7 +1772,7 @@ export class ApiTasksService {
}
/**
*
*
* Reset a task to its initial state, along with any information stored for it (statistics, frame updates etc.).
* @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.
@@ -1815,7 +1817,7 @@ export class ApiTasksService {
}
/**
*
*
* Reset tasks
* @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.
@@ -1860,7 +1862,7 @@ export class ApiTasksService {
}
/**
*
*
* Set the script requirements for a task
* @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.
@@ -1905,7 +1907,7 @@ export class ApiTasksService {
}
/**
*
*
* Mark a task status as in_progress. Optionally allows to set the task\&#39;s execution progress.
* @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.
@@ -1950,7 +1952,7 @@ export class ApiTasksService {
}
/**
*
*
* Request to stop a running task
* @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.
@@ -1995,7 +1997,7 @@ export class ApiTasksService {
}
/**
*
*
* Request to stop running tasks
* @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.
@@ -2040,7 +2042,7 @@ export class ApiTasksService {
}
/**
*
*
* Signal a task has stopped
* @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.
@@ -2085,7 +2087,7 @@ export class ApiTasksService {
}
/**
*
*
* Unarchive tasks
* @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.
@@ -2130,7 +2132,7 @@ export class ApiTasksService {
}
/**
*
*
* Update task\&#39;s runtime parameters
* @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.
@@ -2175,7 +2177,7 @@ export class ApiTasksService {
}
/**
*
*
* Updates a batch of tasks. Headers Content type should be \&#39;application/json- lines\&#39;.
* @param requests Json encoded newline-terminated lines, each representing an event in the batch and uses the same parameters used for tasks.update
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@@ -2220,7 +2222,7 @@ export class ApiTasksService {
}
/**
*
*
* Validate task properties (before create)
* @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.
@@ -2264,4 +2266,48 @@ export class ApiTasksService {
);
}
/**
*
* Add/Remove multiple tags from multiple tasks
* @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 tasksUpdateTags(request: TasksUpdateTagsRequest, 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 tasksUpdateTags.');
}
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<TasksUpdateTagsResponse>(`${this.basePath}/tasks.update_tags`,
request,
{
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
}

View File

@@ -6,11 +6,12 @@
* See: https://github.com/angular/angular/issues/11058#issuecomment-247367318
*/
export class CustomHttpUrlEncodingCodec extends HttpUrlEncodingCodec {
encodeKey(k: string): string {
override encodeKey(k: string): string {
k = super.encodeKey(k);
return k.replace(/\+/gi, '%2B');
}
encodeValue(v: string): string {
override encodeValue(v: string): string {
v = super.encodeValue(v);
return v.replace(/\+/gi, '%2B');
}

View File

@@ -17,5 +17,5 @@ export interface DebugImagesResponseIterations {
* Iteration number
*/
iter?: number;
events?: Array<object>;
events?: Array<any>;
}

View File

@@ -17,4 +17,5 @@ export interface EventsGetTaskSingleValueMetricsRequest {
* List of task Task IDs
*/
tasks: Array<string>;
model_events: boolean;
}

View File

@@ -18,5 +18,6 @@ export interface EventsGetTaskSingleValueMetricsResponseTasks {
* Task ID
*/
task?: string;
values?: Array<EventsGetTaskSingleValueMetricsResponseValues>;
task_name?: string;
values?: Array<EventsGetTaskSingleValueMetricsResponseValues>;
}

View File

@@ -39,7 +39,8 @@ export interface Model {
/**
* Model last update time
*/
last_update?: string;
last_update?: Date;
last_change?: Date;
/**
* Task ID of task in which the model was created
*/

View File

@@ -0,0 +1,28 @@
/**
* models
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* 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 ModelsUpdateTagsRequest {
/**
* IDs of the models to update
*/
ids?: Array<string>;
/**
* User tags to add
*/
add_tags?: Array<string>;
/**
* User tags to remove
*/
remove_tags?: Array<string>;
}

View File

@@ -0,0 +1,20 @@
/**
* models
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* 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 ModelsUpdateTagsResponse {
/**
* The number of updated models
*/
updated?: number;
}

View File

@@ -33,4 +33,13 @@ export interface ProjectsGetHyperparamValuesRequest {
* If set to \'true\' and the project field is set then the result includes hyper parameters values from the subproject tasks
*/
include_subprojects?: boolean;
/**
* Page number
*/
page?: number;
/**
* Page size
*/
page_size?: number;
pattern?: string;
}

View File

@@ -0,0 +1,21 @@
/**
* tasks
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* 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.
*/
/**
* All the terms in \'all\' condition are combined with \'and\' operation
*/
export interface TasksGetAllExRequestAll {
include?: Array<string>;
exclude?: Array<string>;
}

View File

@@ -0,0 +1,21 @@
/**
* tasks
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* 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.
*/
/**
* All the terms in \'any\' condition are combined with \'or\' operation
*/
export interface TasksGetAllExRequestAny {
include?: Array<string>;
exclude?: Array<string>;
}

View File

@@ -0,0 +1,34 @@
/**
* tasks
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* 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.
*/
import { TasksGetAllExRequestAll } from '././tasksGetAllExRequestAll';
import { TasksGetAllExRequestAny } from '././tasksGetAllExRequestAny';
/**
* Filter on a field that includes combination of \'any\' or \'all\' included and excluded terms
*/
export interface TasksGetAllExRequestFilters {
any?: TasksGetAllExRequestAny;
all?: TasksGetAllExRequestAll;
/**
* The operation between \'any\' and \'all\' parts of the filter if both are provided
*/
op?: TasksGetAllExRequestFilters.OpEnum;
}
export namespace TasksGetAllExRequestFilters {
export type OpEnum = 'and' | 'or';
export const OpEnum = {
And: 'and' as OpEnum,
Or: 'or' as OpEnum
}
}

View File

@@ -0,0 +1,28 @@
/**
* tasks
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* 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 TasksUpdateTagsRequest {
/**
* IDs of the tasks to update
*/
ids?: Array<string>;
/**
* User tags to add
*/
add_tags?: Array<string>;
/**
* User tags to remove
*/
remove_tags?: Array<string>;
}

View File

@@ -0,0 +1,20 @@
/**
* tasks
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* 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 TasksUpdateTagsResponse {
/**
* The number of updated tasks
*/
updated?: number;
}

View File

@@ -15,4 +15,6 @@
export interface GetCurrentUserResponseUserObjectCompany {
id?: string;
name?: string;
tier?: any;
}

View File

@@ -0,0 +1,40 @@
/**
* users
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* 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 UsersGetInvitesResponseInvites {
/**
* Invite id
*/
id?: string;
/**
* Company id
*/
company?: string;
/**
* Company name
*/
company_name?: string;
/**
* Invite creation time (UTC)
*/
created?: string;
/**
* Invite expiration time (UTC)
*/
expires?: string;
/**
* Invited emails
*/
emails?: Array<string>;
}

11
src/app/core/app-init.ts Normal file
View File

@@ -0,0 +1,11 @@
import {LoginService} from '~/shared/services/login.service';
import {ConfigurationService} from '@common/shared/services/configuration.service';
export const loadUserAndPreferences = (
loginService: LoginService,
confService: ConfigurationService,
): () => Promise<any> => (): Promise<any> => new Promise((resolve) => {
confService.initConfigurationService().subscribe(() =>
loginService.initCredentials().subscribe(() => loginService.loginFlow(resolve))
);
});

View File

@@ -61,6 +61,8 @@ const syncedKeys = [
'projects.selectedProject',
'rootProjects.showHidden',
'rootProjects.hideExamples',
'rootProjects.mainPageTagsFilter',
'rootProjects.mainPageTagsFilterMatchMode',
'rootProjects.defaultNestedModeForFeature',
'views.availableUpdates',
'views.showSurvey',
@@ -92,7 +94,7 @@ export const localStorageReducer = (reducer: ActionReducer<any>): ActionReducer<
return nextState;
};
const userPrefMetaFactory = (userPreferences: UserPreferences): MetaReducer<any>[] => [
const userPrefMetaFactory = (userPreferences: UserPreferences): MetaReducer[] => [
(reducer: ActionReducer<any>) =>
createUserPrefReducer('users', ['activeWorkspace', 'showOnlyUserWork'], [USERS_PREFIX], userPreferences, reducer),
(reducer: ActionReducer<any>) =>

View File

@@ -1,9 +1,9 @@
import {Injectable} from '@angular/core';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {Actions, concatLatestFrom, createEffect, ofType} from '@ngrx/effects';
import * as actions from '../../webapp-common/core/actions/projects.actions';
import {Store} from '@ngrx/store';
import {selectSelectedProjectId, selectShowHidden} from '@common/core/reducers/projects.reducer';
import {catchError, finalize, mergeMap, switchMap, withLatestFrom} from 'rxjs/operators';
import {catchError, finalize, mergeMap, switchMap} from 'rxjs/operators';
import {deactivateLoader} from '@common/core/actions/layout.actions';
import {ALL_PROJECTS_OBJECT} from '@common/core/effects/projects.effects';
import {requestFailed} from '@common/core/actions/http.actions';
@@ -26,15 +26,14 @@ export class ProjectsEffects {
getSelectedProject = createEffect(() => this.actions$.pipe(
ofType(actions.setSelectedProjectId),
withLatestFrom(
concatLatestFrom(() => [
this.store.select(selectSelectedProjectId),
this.store.select(selectCurrentUser),
this.store.select(selectShowOnlyUserWork),
this.store.select(selectShowHidden),
this.store.select(selectRouterConfig),
),
]),
switchMap(([action, selectedProjectId, user, showOnlyUserWork, showHidden, conf]) => {
const customProjectType = conf[0] !== 'projects';
if (!action.projectId) {
return [
deactivateLoader(action.type),
@@ -51,6 +50,7 @@ export class ProjectsEffects {
actions.getCompanyTags(),
deactivateLoader(action.type)];
} else {
const customProjectType = conf?.[0] !== 'projects';
this.fetchingExampleExperiment = action.example && action.projectId;
return this.projectsApi.projectsGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */

View File

@@ -28,7 +28,7 @@ export class NestedDatasetsPageComponent extends CommonProjectsPageComponent {
hideMenu = false;
entityType = ProjectTypeEnum.datasets;
projectCardClicked(data: { hasSubProjects: boolean; id: string; name: string }) {
override projectCardClicked(data: { hasSubProjects: boolean; id: string; name: string }) {
if (data.hasSubProjects) {
this.router.navigate(['simple', data.id, 'projects'], {relativeTo: this.route.parent?.parent});
} else {
@@ -50,7 +50,7 @@ export class NestedDatasetsPageComponent extends CommonProjectsPageComponent {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected getExtraProjects(selectedProjectId, selectedProject) {
protected override getExtraProjects(selectedProjectId, selectedProject) {
return [];
}

View File

@@ -17,6 +17,7 @@ import {ConfigurationItem} from '../../../business-logic/model/tasks/configurati
import {GetCurrentUserResponseUserObjectCompany} from '../../../business-logic/model/users/getCurrentUserResponseUserObjectCompany';
import {Queue} from '../../../business-logic/model/queues/queue';
import {Container} from '../../../business-logic/model/tasks/container';
import {Output} from '~/business-logic/model/tasks/output';
/**
* an extended object of task that includes projection, will come from the server as an api response.
@@ -57,7 +58,7 @@ export interface ISelectedExperiment {
/**
* an object that will transfrom from the ISelectedExperiment response to more comfortable object to display.
*/
export interface IExperimentInfo extends Omit<Task, 'id' | 'user' | 'project' | 'company' | 'execution' | 'container'> {
export interface IExperimentInfo extends Omit<Task, 'id' | 'user' | 'project' | 'company' | 'execution' | 'container'|'output'> {
id?: string;
model?: IExperimentModelInfo;
execution?: IExecutionForm;
@@ -70,9 +71,10 @@ export interface IExperimentInfo extends Omit<Task, 'id' | 'user' | 'project' |
project?: Project;
company?: GetCurrentUserResponseUserObjectCompany;
container?: Container;
output?: ISelectedExperimentOutput
}
export interface ISelectedExperimentOutput {
export interface ISelectedExperimentOutput extends Omit<Output, 'model'> {
destination?: string;
model?: {
id: Model['id'];

View File

@@ -16,4 +16,6 @@
</div>
</div>
</div>
<router-outlet class="outlet"></router-outlet>
<div class="content">
<router-outlet class="outlet"></router-outlet>
</div>

View File

@@ -1,7 +1,7 @@
import {HTTP} from '~/app.constants';
export const isFileserverUrl = (url: string) =>
url.startsWith(HTTP.FILE_BASE_URL);
url?.startsWith(HTTP.FILE_BASE_URL);
export const convertToReverseProxy = (url: string) => {
try {

View File

@@ -262,7 +262,10 @@ export class NotifierContainerComponent implements OnDestroy, OnInit {
}
Promise.all(stepPromises).then(() => {
if (numberOfNotifications > this.config.behaviour.stacking) {
if (typeof this.config.behaviour.stacking === 'number' ?
numberOfNotifications > this.config.behaviour.stacking :
!this.config.behaviour.stacking
) {
this.removeNotificationFromList(this.notifications[0]);
}
this.tempPromiseResolver(null);

View File

@@ -3,7 +3,7 @@
@font-face {
font-family: '#{$icomoon-font-family}';
src: url('./#{$icomoon-font-family}.ttf?n9hc0z') format('truetype');
src: url('./#{$icomoon-font-family}.ttf?wj7mzq') format('truetype');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -23,20 +23,45 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.al-ico-tune {
&:before {
content: $al-ico-tune;
}
}
.al-ico-equal-outline {
&:before {
content: $al-ico-equal-outline;
}
}
.al-ico-maximize {
&:before {
content: $al-ico-maximize;
}
}
.al-ico-gpu-card {
&:before {
content: $al-ico-gpu-card;
}
}
.al-ico-legend {
&:before {
content: $al-ico-legend;
}
}
.al-ico-model-filled {
&:before {
content: $al-ico-model-filled;
content: $al-ico-model-filled;
}
}
.al-ico-type-report {
&:before {
content: $al-ico-type-report;
content: $al-ico-type-report;
}
}
.al-ico-info-circle-outline {
&:before {
content: $al-ico-info-circle-outline;
content: $al-ico-info-circle-outline;
}
}
.al-ico-ghost {

View File

@@ -1,6 +1,11 @@
$icomoon-font-family: "trains" !default;
$icomoon-font-path: "fonts" !default;
$al-ico-tune: "\e9f8";
$al-ico-equal-outline: "\e9f7";
$al-ico-maximize: "\e9f6";
$al-ico-gpu-card: "\e9f4";
$al-ico-legend: "\e9f3";
$al-ico-model-filled: "\e9f2";
$al-ico-type-report: "\e9f1";
$al-ico-info-circle-outline: "\e9f0";

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10.62" height="10.62" viewBox="0 0 10.62 10.62">
<polygon points="9.24 2.24 8.39 1.39 5.31 4.46 2.24 1.39 1.39 2.24 4.46 5.31 1.39 8.39 2.24 9.24 5.31 6.16 8.39 9.24 9.24 8.39 6.16 5.31 9.24 2.24" fill="#e2001b"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="242" viewBox="0 0 300 242">
<path d="m279.18,0H20.82C9.33.01.01,9.33,0,20.82v200.01c.01,11.49,9.33,20.81,20.82,20.82h258.36c11.49-.01,20.81-9.33,20.82-20.82V20.82C299.99,9.33,290.67.01,279.18,0ZM103.29,230.93v-26.35h89.77v26.35h-89.77Zm0-37.35v-26.55h89.77v26.55h-89.77Zm-11-139.21v26.55H10.71v-26.55h81.58Zm111.77,26.55v-26.55h85.23v14.51h0v12.04h-85.23Zm85.23,11v26.55h-85.23v-26.55h85.23Zm-96.23-37.55v9.62s0,0,0,.01c0,0,0,0,0,0v16.91h-89.77v-26.55h89.77Zm0,37.55v26.55h-89.77v-26.55h89.77Zm-100.77,26.55H10.71v-26.55h81.58v26.55Zm-81.58,11h81.58v26.55H10.71v-26.55Zm92.58,0h89.77v26.55h-89.77v-26.55Zm100.77,0h85.23v26.55h-85.23v-26.55ZM10.71,167.03h81.58v26.55H10.71v-26.55Zm193.35,0h85.23v26.55h-85.23v-26.55ZM10.71,220.83v-16.25h81.58v26.35H20.82c-5.56-.05-10.06-4.54-10.11-10.1Zm268.52,10.1s-.03,0-.05,0h0s-75.12,0-75.12,0v-26.35h85.23v16.55c-.07,5.48-4.57,9.87-10.06,9.8Z" opacity="0.1" />
</svg>

After

Width:  |  Height:  |  Size: 967 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13.12" height="15" viewBox="0 0 13.12 15">
<path d="m8.57,13.04c.16.16.24.36.24.59s-.08.43-.24.59-.37.24-.64.24-.48-.08-.64-.24-.24-.36-.24-.59.08-.43.24-.59.37-.24.64-.24.48.08.64.24Zm.2-6.03h-1.68l.19,5.06h1.3l.19-5.06Zm4.16-.22c-.12-.21-.3-.38-.51-.51L2.12.19C1.46-.21.6,0,.2.67c-.13.22-.2.48-.2.74v12.19c0,.77.62,1.4,1.39,1.41.26,0,.51-.07.73-.2l3.88-2.29v-1.63l-4.33,2.56c-.09.05-.19.02-.24-.07-.02-.03-.02-.06-.02-.09V1.71c0-.1.08-.18.18-.18.03,0,.06,0,.09.02l9.79,5.79c.08.05.11.16.06.24-.01.02-.03.04-.06.06l-1.46.87v1.63l2.43-1.44c.67-.39.89-1.25.51-1.91Z" fill="#8492C2"/>
</svg>

After

Width:  |  Height:  |  Size: 639 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13.12" height="15" viewBox="0 0 13.12 15">
<path d="m8.57,13.04c.16.16.24.36.24.59s-.08.43-.24.59-.37.24-.64.24-.48-.08-.64-.24-.24-.36-.24-.59.08-.43.24-.59.37-.24.64-.24.48.08.64.24Zm.2-6.03h-1.68l.19,5.06h1.3l.19-5.06Zm4.16-.22c-.12-.21-.3-.38-.51-.51L2.12.19C1.46-.21.6,0,.2.67c-.13.22-.2.48-.2.74v12.19c0,.77.62,1.4,1.39,1.41.26,0,.51-.07.73-.2l3.88-2.29v-1.63l-4.33,2.56c-.09.05-.19.02-.24-.07-.02-.03-.02-.06-.02-.09V1.71c0-.1.08-.18.18-.18.03,0,.06,0,.09.02l9.79,5.79c.08.05.11.16.06.24-.01.02-.03.04-.06.06l-1.46.87v1.63l2.43-1.44c.67-.39.89-1.25.51-1.91Z" fill="#50e3c3"/>
</svg>

After

Width:  |  Height:  |  Size: 638 B

View File

@@ -50,7 +50,7 @@ export const getSingleValues = createAction('[App] getSingleValues', props<{
export const setPlotData = createAction('[App] setPlot', props<{ data: ReportsApiMultiplotsResponse }>());
export const setScalarData = createAction('[App] setScalar', props<{ data: ExtFrame[] }>());
export const setSampleData = createAction('[App] setSample', props<{ data: DebugSample }>());
export const setSingleValues = createAction('[App] setSingleValues', props<{ data: SingleValueTaskMetrics }>());
export const setSingleValues = createAction('[App] setSingleValues', props<{ data: SingleValueTaskMetrics[] }>());
export const setParallelCoordinateExperiments = createAction('[App] setParcoor', props<{ data: Task[] }>());
export const reportsPlotlyReady = createAction('[App] plotly ready');

View File

@@ -4,7 +4,7 @@
<a class="webapp-link" [href]="webappLink" target="_blank" [class.dark-theme]="isDarkTheme">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="m2.67,14.66c-.74,0-1.33-.6-1.33-1.33h0V2.67c0-.74.6-1.33,1.33-1.33h5.33v1.33H2.67v10.67h10.67v-5.33h1.34v5.33c0,.74-.6,1.33-1.34,1.33H2.67Zm4.86-7.14l4.86-4.86h-1.72v-1.33h4v4h-1.33v-1.72l-4.86,4.86-.95-.94Z" fill="#8693be"/>
<path d="m2.67,14.66c-.74,0-1.33-.6-1.33-1.33h0V2.67c0-.74.6-1.33,1.33-1.33h5.33v1.33H2.67v10.67h10.67v-5.33h1.34v5.33c0,.74-.6,1.33-1.34,1.33H2.67Zm4.86-7.14l4.86-4.86h-1.72v-1.33h4v4h-1.33v-1.72l-4.86,4.86-.95-.94Z" fill="#8492c2"/>
</svg>
<span class="webapp-link_tooltip">View original resource</span>
</a>
@@ -53,14 +53,14 @@
></sm-parallel-coordinates-graph>
</ng-container>
<ng-container *ngSwitchCase="'single'">
<div *ngSwitchCase="'single'" class="single-value-summary-section">
<sm-single-value-summary-table
*ngIf="singleValueData && singleValueData[0]"
[data]="singleValueData"
[experimentName]="singleValueData[0]?.metric"
[darkTheme]="isDarkTheme"
class="single-value-summary-table"></sm-single-value-summary-table>
</ng-container>
></sm-single-value-summary-table>
</div>
</ng-container>
</ng-container>

View File

@@ -11,7 +11,18 @@
font-size: 14px;
}
.single-value-summary-section {
display: flex;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
padding: 0 12px;
sm-single-value-summary-table {
max-width: 100%;
}
}
.show-text {
cursor: pointer;

View File

@@ -20,7 +20,7 @@ import {ExtFrame} from '@common/shared/single-graph/plotly-graph-base';
import {DebugSample} from '@common/shared/debug-sample/debug-sample.reducer';
import {getSignedUrl, setS3Credentials} from '@common/core/actions/common-auth.actions';
import {ConfigurationService} from '@common/shared/services/configuration.service';
import {_mergeVariants, convertMultiPlots, mergeMultiMetricsGroupedVariant, prepareMultiPlots, tryParseJson} from '@common/tasks/tasks.utils';
import {_mergeVariants, convertMultiPlots, createMultiSingleValuesChart, mergeMultiMetricsGroupedVariant, prepareGraph, prepareMultiPlots, tryParseJson} from '@common/tasks/tasks.utils';
import {selectSignedUrl} from '@common/core/reducers/common-auth-reducer';
import {loadExternalLibrary} from '@common/shared/utils/load-external-library';
import {ImageViewerComponent} from '@common/shared/debug-sample/image-viewer/image-viewer.component';
@@ -47,7 +47,6 @@ export class AppComponent implements OnInit {
title = 'report-widgets';
public plotData: ExtFrame;
public frame: DebugSample;
public parcoords: any;
public plotLoaded: boolean;
private environment: Environment;
public activated: boolean = false;
@@ -193,8 +192,8 @@ export class AppComponent implements OnInit {
const newGraphs = convertMultiPlots(merged);
const originalObject = this.searchParams.get('objects');
const series = this.searchParams.get('series');
this.plotData = Object.values(newGraphs)[0]?.find(a => originalObject === (a.task ?? a.data[0].task)) ??
Object.values(newGraphs)[0].find(a => (a.data[0] as any).seriesName === series) ??
this.plotData = series ? Object.values(newGraphs)[0].find(a => (a.data[0] as any).seriesName === series) :
Object.values(newGraphs)[0]?.find(a => originalObject === (a.task ?? a.data[0].task)) ??
Object.values(newGraphs)[0]?.[0];
}
@@ -297,7 +296,14 @@ export class AppComponent implements OnInit {
filter(singleValueData => !!singleValueData),
take(1)
).subscribe(singleValueData => {
this.singleValueData = singleValueData.values;
const objects = this.searchParams.getAll('objects');
if (objects.length > 1) {
this.type = 'scalar';
const singleValuesData = createMultiSingleValuesChart(singleValueData);
this.plotData = prepareGraph(singleValuesData.data, singleValuesData.layout, {}, {type: 'singleValue'});
} else {
this.singleValueData = singleValueData[0].values;
}
this.cdr.detectChanges();
});
}
@@ -306,9 +312,11 @@ export class AppComponent implements OnInit {
this.activated = true;
await this.waitForVisibility();
this.singleGraphHeight = window.innerHeight;
!['sample', 'single'].includes(this.type) && loadExternalLibrary(this.store, this.environment.plotlyURL, reportsPlotlyReady);
const models = this.searchParams.has('models') || this.searchParams.get('objectType') === 'model';
const objects = this.searchParams.getAll('objects');
if ((!['sample', 'single'].includes(this.type) || objects.length > 1)) {
loadExternalLibrary(this.store, this.environment.plotlyURL, reportsPlotlyReady);
}
const queryParams = {
tasks: objects.length > 0 ? objects : this.searchParams.getAll('tasks'),
metrics: this.searchParams.getAll('metrics'),
@@ -400,7 +408,7 @@ export class AppComponent implements OnInit {
const isCompare = entityIds.length > 1;
let url = `${window.location.origin.replace('4201', '4200')}/projects/${project ?? '*'}/`;
if (isCompare) {
url += `${isModels ? 'compare-models;ids=' : 'compare-experiments;ids='}${entityIds.join(',')}/
url += `${isModels ? 'compare-models;ids=' : 'compare-experiments;ids='}${entityIds.filter(id => !!id).join(',')}/
${this.getComparePath(this.type)}?metricPath=${metricPath}&metricName=lala${variants.map(par => `&params=${par}`).join('')}`;
} else {
url += `${isModels ? 'models/' : 'experiments/'}${entityIds}/${this.getOutputPath(isModels, this.type)}`;
@@ -428,6 +436,7 @@ ${this.getComparePath(this.type)}?metricPath=${metricPath}&metricName=lala${vari
return 'info-output/debugImages';
}
}
return '';
}
private getComparePath(type: WidgetTypes) {

View File

@@ -145,7 +145,7 @@ export class AppEffects {
})
),
mergeMap((res: { data: ReportsGetTaskDataResponse }) => [
setSingleValues({data: res.data.single_value_metrics[0]}),
setSingleValues({data: res.data.single_value_metrics}),
setTaskData({sourceProject: (res.data.tasks[0]?.project as any).id, sourceTasks: res.data.tasks.map(t => t.id)})
]
)

View File

@@ -31,7 +31,7 @@ export const appFeatureKey = 'app';
export interface State {
plotData: MetricsPlotEvent[] | ReportsApiMultiplotsResponse;
sampleData: DebugSample;
singleValuesData: SingleValueTaskMetrics;
singleValuesData: SingleValueTaskMetrics[];
parallelCoordinateData: Task[];
scaleFactor: number;
plotlyReady: boolean;

View File

@@ -1,30 +1,23 @@
import {BASE_ENV, Environment} from './base';
/*
1 https://api1.rnd.dev2.allegro.ai (community)
2 https://api2.rnd.dev2.allegro.ai (enterprise)
3 https://api.qa.hosted.allegro.ai
4 https://api.allegro-master.hosted.allegro.ai
5 https://api.allegro.ai
6 https://api2.qa.hosted.allegro.ai
7 https://api1.testing2.dev2.allegro.ai
8 https://api2.testing2.dev2.allegro.ai
9 https://api.vimeo.hosted.allegro.ai
10 https://api.maxq.hosted.allegro.ai
11 https://api.community-master.hosted.allegro.ai
12 https://api.dev.hosted.allegro.ai
13 https://api.staging.hosted.allegro.ai
14 https://api.clear.ml
15 https://api.dev.hosted.clear.ml
16 https://api.deloitte.hosted.allegro.ai
17 https://api.trains-master.hosted.allegro.ai
*/
'https://api.allegro-master.hosted.allegro.ai', // 1
'https://api.community-master.hosted.allegro.ai', // 2
'https://api.allegro.ai', // 3
'https://api.clear.ml', // 4
'https://api1.rnd.dev2.allegro.ai', // 5
'https://api2.rnd.dev2.allegro.ai', // 6
'https://api.dev.hosted.allegro.ai', // 7
'https://api.dev.hosted.clear.ml', // 8
'https://api.staging.hosted.allegro.ai', // 9
'https://api.neuralguard.hosted.allegro.ai', // 10
*/
export const environment = {
...BASE_ENV,
production: false,
baseUrl: 'localhost:4200',
autoLogin: false,
apiBaseUrl: 'service/4/api',
apiBaseUrl: 'service/1/api',
// communityServer: true,
accountAdministration: true,
fileBaseUrl: 'https://files.allegro.ai',

View File

@@ -128,6 +128,7 @@ $sm-dark-theme: mat.define-dark-theme((
@include mat.dialog-theme($theme);
@include mat.slider-theme($sm-neon-theme);
@include mat.form-field-theme($theme);
@include mat.tooltip-theme($theme);
@include mat.select-theme($sm-dark-theme);
@@ -142,6 +143,7 @@ $sm-dark-theme: mat.define-dark-theme((
html, body {
height: 100%;
overflow: hidden;
--mat-select-trigger-text-size: 14px;
}
body {
@@ -167,6 +169,13 @@ body {
}
}
.ellipsis {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.mat-mdc-dialog-container {
padding: 0 !important;
}
@@ -289,6 +298,7 @@ sm-debug-image-snippet {
.image-var {
top: 8px;
bottom: unset;
line-height: 20px;
}
}
@@ -320,6 +330,9 @@ sm-debug-image-snippet {
.mat-mdc-select-panel {
padding: 0;
font-size: 14px;
--mdc-typography-body1-font-size: 14px;
--mdc-typography-body1-line-height: 16px;
--mat-select-trigger-text-size: 14px;
&.light-theme {
--mdc-theme-surface: white;
@@ -329,6 +342,8 @@ sm-debug-image-snippet {
&.dark, &.black, &.dark-theme {
--mdc-theme-surface: #000;
border: 1px solid $blue-500;
--mat-select-panel-background-color: #000;
--mat-option-label-text-color: #{$blue-200};
.mat-mdc-option {
--mdc-theme-text-primary-on-background: #{$blue-200};
@@ -337,7 +352,7 @@ sm-debug-image-snippet {
.mat-mdc-option {
--mdc-typography-body1-line-height: 36px;
--mdc-typography-body1-font-size: 14px;
--mat-menu-item-label-text-size: 14px;
min-height: 36px;
}
@@ -372,8 +387,8 @@ sm-debug-image-snippet {
.dark-theme, .light-theme {
.mat-mdc-form-field {
--mdc-typography-body1-font-size: 14px;
--mdc-typography-body1-line-height: 16px;
--mat-menu-item-label-text-line-height: 16;
--mat-menu-item-label-text-size: 14px;
&.mat-form-field-appearance-outline {
&.black {
@@ -511,3 +526,26 @@ input[type=number] {
::-webkit-scrollbar-corner {
background-color: transparent
}
.mat-mdc-tooltip {
&.sm-tooltip {
--mdc-plain-tooltip-container-color: #{$purple};
.mdc-tooltip__surface {
max-width: 400px;
font-size: 11px;
line-height: 1.55;
letter-spacing: 0.3px;
}
&.break-line {
.mdc-tooltip__surface {
white-space: pre-line;
}
}
}
&.parameter-tooltip {
margin: 6px auto 6px -74px;
}
}

View File

@@ -7,27 +7,32 @@ export interface SearchState {
searchQuery: {query: string; regExp?: boolean; original?: string};
placeholder: string;
active: boolean;
initiated: boolean;
}
// Todo remove selectedProjectId
const searchInitState: SearchState = {
isSearching: false,
searchQuery: null,
placeholder: null,
active : false
active : false,
initiated : false
};
export const searchReducer = createReducer(
searchInitState,
on(setSearching, (state, action) => ({...state, isSearching: action.payload})),
on(setSearchQuery, (state, action) => ({...state, searchQuery: action})),
on(initSearch, (state, action) => ({...searchInitState, placeholder: action.payload || 'Search'})),
on(resetSearch, () => ({...searchInitState, placeholder: 'Search'})),
on(setSearching, (state, action): SearchState => ({...state, isSearching: action.payload})),
on(setSearchQuery, (state, action): SearchState => ({...state, searchQuery: action})),
on(initSearch, (state, action): SearchState => ({
...state,
placeholder: action.payload || 'Search',
initiated: true
})),
on(resetSearch, (): SearchState => ({...searchInitState, placeholder: 'Search'})),
);
export const selectCommonSearch = createFeatureSelector<SearchState>('commonSearch');
export const selectIsSearching = createSelector(selectCommonSearch, (state: SearchState): boolean => state ? state.isSearching : false);
export const selectSearchQuery = createSelector(selectCommonSearch, (state: SearchState) => state?.searchQuery || searchInitState.searchQuery);
export const selectPlaceholder = createSelector(selectCommonSearch, (state: SearchState): string => state ? state.placeholder : '');
export const selectSearchQuery = createSelector(selectCommonSearch, (state: SearchState) => state ? state.searchQuery : searchInitState.searchQuery);
export const selectPlaceholder = createSelector(selectCommonSearch, (state: SearchState) => state ? state.placeholder : '');

View File

@@ -1,6 +1,6 @@
<span class="search-container" [class.open]="isSearching$ | async">
<sm-search
#search
#searchComponent
class="search-header"
[class.regex-error]="regexError"
[value]="(searchQuery$ | async)?.original"
@@ -8,7 +8,7 @@
[hideIcons]="true"
[minimumChars]="minChars"
(focusout)="onSearchFocusOut()"
(valueChanged)="onSearchValueChanged($event)"
(valueChanged)="search($event)"
>
<i *ngIf="regexError" class="regexp al-icon al-ico-error-circle pointer" [smTooltip]="regexError" [matTooltipPosition]="'below'"></i>
<i
@@ -17,7 +17,7 @@
smClickStopPropagation
[smTooltip]="'Regex'" [matTooltipPosition]="'below'"
[class.active]="regExp"
(click)="toggleRegExp(); search.searchBarInput.nativeElement.focus();"></i>
(click)="toggleRegExp(); searchComponent.searchBarInput.nativeElement.focus();"></i>
</sm-search>
</span>
<ng-container *ngIf="searchActive">

View File

@@ -4,7 +4,7 @@ import {Store} from '@ngrx/store';
import {setSearching, setSearchQuery} from '../../common-search.actions';
import {SearchState, selectIsSearching, selectPlaceholder, selectSearchQuery} from '../../common-search.reducer';
import {Observable} from 'rxjs';
import {debounceTime, filter, tap} from 'rxjs/operators';
import {debounceTime, distinctUntilChanged, filter, tap} from 'rxjs/operators';
import {SearchComponent} from '@common/shared/ui-components/inputs/search/search.component';
@Component({
@@ -23,11 +23,40 @@ export class CommonSearchComponent implements OnInit {
@ViewChild(SearchComponent) searchElem: SearchComponent;
public regExp: boolean = false;
private closeTimer: number;
private queryString: string;
minChars = 3;
public regexError: boolean;
constructor(private store: Store, private router: Router, private route: ActivatedRoute, private cdr: ChangeDetectorRef) {
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
distinctUntilChanged((pe: NavigationEnd, ce: NavigationEnd) => pe.url?.split('?')[1] === ce.url?.split('?')[1])
).subscribe(() => {
const query = this.route.snapshot.queryParams?.q ?? '';
const qregex = this.route.snapshot.queryParams?.qreg === 'true';
this.store.dispatch(setSearchQuery({
query: qregex ? query : query.trim(),
regExp: qregex,
original: query
}));
if (query) {
this.openSearch();
} else {
if (document.activeElement !== this.searchElem.searchBarInput.nativeElement) {
this.store.dispatch(setSearching({payload: false}))
}
}
})
if( this.route.snapshot.queryParams.q) {
const query = this.route.snapshot.queryParams?.q ?? '';
const qregex = this.route.snapshot.queryParams?.qreg ?? false;
this.store.dispatch(setSearchQuery({
query: qregex ? query : query.trim(),
regExp: qregex,
original: query
}));
setTimeout( () => this.openSearch());
}
}
ngOnInit() {
@@ -41,23 +70,23 @@ export class CommonSearchComponent implements OnInit {
setTimeout(this.setSearchActive.bind(this));
}
onSearchValueChanged(query: string) {
this.queryString = query;
this.search();
this.cdr.detectChanges();
}
private search() {
public search(query: string) {
try {
if (this.regExp) {
new RegExp(this.queryString);
new RegExp(query);
}
this.regexError = null;
this.store.dispatch(setSearchQuery({
query: this.regExp ? this.queryString : this.queryString.trim(),
regExp: this.regExp,
original: this.queryString
}));
this.router.navigate([], {
relativeTo: this.route,
replaceUrl: true,
queryParamsHandling: "merge",
queryParams: {
q: query || undefined
}
})
} catch (e) {
this.regexError = e.message?.replace(/:.+:/, ':');
}
@@ -92,13 +121,17 @@ export class CommonSearchComponent implements OnInit {
this.searchElem.clear();
this.store.dispatch(setSearching({payload: false}));
document.body.focus();
setTimeout( () =>this.searchElem.searchBarInput.nativeElement.blur(), 100);
}
toggleRegExp() {
this.regExp = !this.regExp;
window.clearTimeout(this.closeTimer);
if (this.queryString?.length >= this.minChars) {
this.search();
}
this.router.navigate([], {
relativeTo: this.route,
replaceUrl: true,
queryParamsHandling: "merge",
queryParams: {
qreg: this.regExp ? this.regExp : undefined}
})
}
}

View File

@@ -94,6 +94,9 @@ $sm-neon-theme: mat.define-dark-theme((
--mdc-typography-body1-letter-spacing: 0;
--mdc-typography-button-letter-spacing: 0;
.mat-mdc-slide-toggle{
--mdc-switch-state-layer-size: 24px;
}
.mat-mdc-checkbox .mdc-checkbox {
--mdc-checkbox-selected-icon-color: #{$purple};
@@ -101,8 +104,8 @@ $sm-neon-theme: mat.define-dark-theme((
--mdc-checkbox-selected-focus-icon-color: #{$purple};
}
.mat-expansion-panel-header-description, .mat-expansion-indicator:after {
color: $white;
.mat-expansion-panel-header-description, .mat-expansion-indicator {
--mat-expansion-header-indicator-color: #{$blue-300};
}
.link {
@@ -131,6 +134,9 @@ $sm-neon-theme: mat.define-dark-theme((
--mdc-typography-body1-letter-spacing: 0;
--mdc-typography-button-letter-spacing: 0;
--mdc-switch-state-layer-size: 24px;
--bs-border-width: 1px #{$blue-300};
mat-progress-bar {
border-radius: 4px;
@@ -174,7 +180,7 @@ html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: $font-family-base, sans-serif;
font-family: $font-family-base;
font-size: 14px;
overflow: hidden;
}
@@ -279,7 +285,7 @@ hr {
//}
mat-expansion-panel {
box-shadow: unset;
box-shadow: unset !important;
.mat-expansion-panel-header {
margin-bottom: 0;
@@ -388,6 +394,8 @@ button {
.empty-menu {
height: 100px;
font-size: 14px;
--mat-menu-item-label-text-tracking: 0;
}
.capital-case {
@@ -597,9 +605,11 @@ html {
&.cdk-keyboard-focused, &.cdk-program-focused {
color: rgba(0, 0, 0, 0.87);
}
&.mat-mdc-focus-indicator {
--mdc-list-list-item-hover-label-text-color: rgba(0, 0, 0, 0.87);
--mat-menu-item-hover-state-layer-color: #{$blue-50};
}
.mdc-list-item__primary-text {
--mdc-list-list-item-label-text-color: rgba(0, 0, 0, 0.87);
}
@@ -615,14 +625,13 @@ html {
width: calc(100% - 8px);
height: 40px;
min-height: 40px;
--mdc-typography-body1-line-height: 20px;
font-size: 14px;
--mdc-typography-body1-font-size: 14px;
--mat-menu-item-label-text-line-height: 20px;
--mat-menu-item-label-text-size: 14px;
padding: 0 32px 0 12px;
border-radius: 4px;
.mdc-list-item__primary-text {
--mdc-typography-body1-line-height: 20px;
--mat-menu-item-label-text-tracking: 0;
display: flex;
align-items: center;
gap: 0 12px;
@@ -630,10 +639,6 @@ html {
overflow: hidden;
text-overflow: ellipsis;
}
&.mat-mdc-menu-item {
}
}
hr {
@@ -859,7 +864,7 @@ button.btn.button-outline-dark {
.ace_placeholder {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
color: $blue-grey !important;
color: $blue-300 !important;
}
.modebar-btn[data-attr="plotly-embedded-modebar-button"] {

View File

@@ -1,3 +1,6 @@
import {IOption} from '@common/shared/ui-components/inputs/select-autocomplete-with-chips/select-autocomplete-with-chips.component';
import {TIME_INTERVALS} from '@common/workers-and-queues/workers-and-queues.consts';
export type TableSelectionState = 'All' | 'Partial' | 'None';
export const TIME_FORMAT_STRING = 'MMM d yyyy H:mm';
@@ -110,3 +113,13 @@ export const MESSAGES_SEVERITY = {
};
export const rootProjectsPageSize = 50;
export const timeFrameOptions: IOption[] = [
{label: '3 Hours', value: (3 * TIME_INTERVALS.HOUR).toString()},
{label: '6 Hours', value: (6 * TIME_INTERVALS.HOUR).toString()},
{label: '12 Hours', value: (12 * TIME_INTERVALS.HOUR).toString()},
{label: '1 Day', value: (TIME_INTERVALS.DAY).toString()},
{label: '1 Week', value: (TIME_INTERVALS.WEEK).toString()},
{label: '1 Month', value: (TIME_INTERVALS.MONTH).toString()}
];

View File

@@ -48,7 +48,7 @@ export const resetDontShowAgainForBucketEndpoint = createAction(AUTH_PREFIX + 'R
export const resetShowS3Popup = createAction(AUTH_PREFIX + 'RESET_SHOW_S3_POPUP');
export const showS3PopUp = createAction(
AUTH_PREFIX + 'SHOW_S3_POPUP',
props<{credentials: Credentials; credentialsError: string; isAzure: boolean}>()
props<{credentials: Credentials; credentialsError: string; provider: 's3' | 'azure' | 'gcs'}>()
);
export const getTutorialBucketCredentials = createAction(AUTH_PREFIX + 'GET_TUTORIAL_BUCKET_CREDENTIALS');
export const showLocalFilePopUp = createAction(

View File

@@ -1,5 +1,5 @@
import {VIEW_PREFIX} from '~/app.constants';
import {createAction, props} from '@ngrx/store';
import {Action, createAction, props} from '@ngrx/store';
import {omit} from 'lodash-es';
import {HttpErrorResponse} from '@angular/common/http';
import {Ace} from 'ace-builds';
@@ -69,7 +69,7 @@ export const resetAceCaretsPositions = createAction(VIEW_PREFIX + '[reset ace ca
export const addMessage = createAction(
VIEW_PREFIX + '[add message]',
(severity: MessageSeverityEnum, msg: string, userActions?: { actions: any[]; name: string }[], suppressNextMessages?: boolean) =>
(severity: MessageSeverityEnum, msg: string, userActions?: { actions: Action[]; name: string }[], suppressNextMessages?: boolean) =>
({severity, msg, userActions, suppressNextMessages})
);
@@ -95,8 +95,6 @@ export const setRedactedArguments = createAction(VIEW_PREFIX + 'SET_REDACTED_ARG
export const setHideRedactedArguments = createAction(VIEW_PREFIX + 'SET_SHOW_REDACTED_ARGUMENTS', props<{ hide: boolean }>());
export const plotlyReady = createAction(VIEW_PREFIX + '[plotly ready]');
export const aceReady = createAction(VIEW_PREFIX + '[ace ready]');
export const openAppsAwarenessDialog = createAction(VIEW_PREFIX + '[apps awareness dialog]',
props<{ appsYouTubeIntroVideoId }>()
);
export const openAppsAwarenessDialog = createAction(VIEW_PREFIX + '[apps awareness dialog]' );

View File

@@ -116,6 +116,11 @@ export const setCompanyTags = createAction(
props<{ tags: string[]; systemTags: string[] }>()
);
export const addCompanyTag = createAction(
PROJECTS_PREFIX + '[add company tag]',
props<{tag: string}>()
);
export const setMainPageTagsFilter = createAction(
PROJECTS_PREFIX + '[set main page tags filters]',
props<{ tags?: string[]; feature: string}>()
@@ -222,7 +227,7 @@ export const resetTablesFilterProjectsOptions = createAction(
export const getTablesFilterProjectsOptions = createAction(
PROJECTS_PREFIX + ' [get tables filter projects options]',
props<{ searchString: string; loadMore: boolean}>()
props<{ searchString: string; loadMore: boolean; allowPublic?: boolean}>()
);
export const setTablesFilterProjectsOptions = createAction(

View File

@@ -31,6 +31,7 @@ export const setURLParams = createAction(
isDeep?: boolean;
update?: boolean;
version?: string;
others?: {[key: string]: string};
}>()
);

View File

@@ -1,18 +1,21 @@
import {Injectable} from '@angular/core';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {Actions, concatLatestFrom, createEffect, 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, deactivateLoader, setServerError} from '../actions/layout.actions';
import {catchError, filter, finalize, map, mergeMap, switchMap, throttleTime, withLatestFrom} from 'rxjs/operators';
import {AuthGetCredentialsResponse} from '~/business-logic/model/auth/authGetCredentialsResponse';
import {select, Store} from '@ngrx/store';
import {Store} from '@ngrx/store';
import {selectCurrentUser} from '../reducers/users-reducer';
import {GetCurrentUserResponseUserObject} from '~/business-logic/model/users/getCurrentUserResponseUserObject';
import {AdminService} from '~/shared/services/admin.service';
import {selectDontShowAgainForBucketEndpoint, selectS3BucketCredentialsBucketCredentials, selectSignedUrl} from '@common/core/reducers/common-auth-reducer';
import {EMPTY, of} from 'rxjs';
import {S3AccessResolverComponent} from '@common/layout/s3-access-resolver/s3-access-resolver.component';
import {
S3AccessDialogData,
S3AccessResolverComponent
} from '@common/layout/s3-access-resolver/s3-access-resolver.component';
import {MatDialog} from '@angular/material/dialog';
import {setCredentialLabel} from '../actions/common-auth.actions';
import {SignResponse} from '@common/settings/admin/base-admin-utils';
@@ -38,7 +41,7 @@ export class CommonAuthEffects {
getAllCredentialsEffect = createEffect(() => this.actions.pipe(
ofType(authActions.getAllCredentials),
switchMap(action => this.credentialsApi.authGetCredentials({}).pipe(
withLatestFrom(this.store.select(selectCurrentUser)),
concatLatestFrom(() => this.store.select(selectCurrentUser)),
mergeMap(([res, user]: [AuthGetCredentialsResponse, GetCurrentUserResponseUserObject]) => [
authActions.updateAllCredentials({credentials: res.credentials, extra: res?.['additional_credentials'], workspace: user.company.id}),
deactivateLoader(action.type)
@@ -103,7 +106,7 @@ export class CommonAuthEffects {
filter(action => !!action.url),
mergeMap(action =>
of(action).pipe(
withLatestFrom(
concatLatestFrom(() =>
this.store.select(state => selectSignedUrl(action.url)(state))
),
switchMap(([, signedUrl]) =>
@@ -115,7 +118,7 @@ export class CommonAuthEffects {
switch (res.type) {
case 'popup':
this.signAfterPopup.push(action);
return [authActions.showS3PopUp({credentials: res.bucket, isAzure: res.azure, credentialsError: null})];
return [authActions.showS3PopUp({credentials: res.bucket, provider: res.provider, credentialsError: null})];
case 'sign':
return [authActions.setSignedUrl({url: action.url, signed: res.signed, expires: res.expires})];
default:
@@ -129,9 +132,7 @@ export class CommonAuthEffects {
s3popup = createEffect(() => this.actions.pipe(
ofType(authActions.showS3PopUp),
withLatestFrom(
this.store.select(selectDontShowAgainForBucketEndpoint)
),
concatLatestFrom(() => this.store.select(selectDontShowAgainForBucketEndpoint)),
throttleTime(500),
filter(([action, dontShowAgain]) =>
action?.credentials?.Bucket + action?.credentials?.Endpoint !== dontShowAgain &&
@@ -141,10 +142,8 @@ export class CommonAuthEffects {
if (action?.credentials?.Bucket) {
this.openPopup[action.credentials.Bucket] = true;
}
return this.matDialog.open(S3AccessResolverComponent, {data: action, maxWidth: 700}).afterClosed().pipe(
withLatestFrom(
this.store.pipe(select(selectS3BucketCredentialsBucketCredentials)),
),
return this.matDialog.open(S3AccessResolverComponent, {data: action as S3AccessDialogData, maxWidth: 700}).afterClosed().pipe(
concatLatestFrom(() => this.store.select(selectS3BucketCredentialsBucketCredentials)),
switchMap(([data, bucketCredentials]) => {
window.setTimeout(() => this.signAfterPopup = []);
if (data) {

View File

@@ -43,7 +43,7 @@ import {
OperationErrorDialogComponent
} from '@common/shared/ui-components/overlay/operation-error-dialog/operation-error-dialog.component';
import {ApiTasksService} from '~/business-logic/api-services/tasks.service';
import {createMetricColumn} from '@common/shared/utils/tableParamEncode';
import {createMetricColumn, excludedKey} from '@common/shared/utils/tableParamEncode';
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';
@@ -102,6 +102,7 @@ export class ProjectsEffects {
switchMap(([action, showHidden, scrollId, filters]) => forkJoin([
this.projectsApi.projectsGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */
allow_public: action.allowPublic,
page_size: rootProjectsPageSize,
size: rootProjectsPageSize,
order_by: ['name'],
@@ -190,7 +191,10 @@ export class ProjectsEffects {
resetProjectSelections$ = createEffect(() => this.actions$.pipe(
ofType(actions.resetProjectSelection),
mergeMap(() => [setSelectedExperiments({experiments: []}), setSelectedModels({models: []})])
mergeMap(() => [
setSelectedExperiments({experiments: []}),
setSelectedModels({models: []})
])
));
updateProject$ = createEffect(() => this.actions$.pipe(
@@ -307,7 +311,7 @@ export class ProjectsEffects {
started: ['2000-01-01T00:00:00', null],
status: ['-draft'],
order_by: ['-started'],
type: ['__$not', 'annotation_manual', '__$not', 'annotation', '__$not', 'dataset_import'],
type: [excludedKey, 'annotation_manual', excludedKey, 'annotation', excludedKey, 'dataset_import'],
system_tags: ['-archived'],
scroll_id: null,
size: 1000

View File

@@ -41,7 +41,8 @@ export class RouterEffects {
...(action.filters && {filter: encodeFilters(action.filters)}),
...(action.isArchived !== undefined && {archive: action.isArchived ? 'true' : null}),
...(action.isDeep && {deep: true}),
...(action.version && {version: action.version})
...(action.version && {version: action.version}),
...(action.others && action.others)
}
} as NavigationExtras;
this.router.navigate([], extra);

View File

@@ -77,11 +77,7 @@ ${this.errorService.getErrorMsg(err?.error)}`)])
updateCurrentUser = createEffect(() => this.actions.pipe(
ofType(updateCurrentUser),
mergeMap(({user}) => this.userService.usersUpdate({...user}).pipe(
mergeMap((res: UsersUpdateResponse) => {
if (res.updated) {
return [setCurrentUserName({name: user.name})];
}
})
mergeMap((res: UsersUpdateResponse) => res.updated ? [setCurrentUserName({name: user.name})] : [])
)),
catchError(err => [addMessage(MESSAGES_SEVERITY.ERROR, `Update User Failed ${this.errorService.getErrorMsg(err?.error)}`)])
));

View File

@@ -1,4 +1,4 @@
import {Injectable} from '@angular/core';
import {inject, Injectable} from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { environment } from '../../../../environments/environment';
@@ -12,8 +12,12 @@ import {setCurrentUser} from '~/core/actions/users.action';
@Injectable()
export class WebappInterceptor implements HttpInterceptor {
protected user: GetCurrentUserResponseUserObject;
protected router: Router;
protected store: Store;
constructor(protected router: Router, protected store: Store) {
constructor() {
this.router = inject(Router);
this.store = inject(Store);
this.store.select(selectCurrentUser).subscribe(user => this.user = user);
}
@@ -47,9 +51,9 @@ export class WebappInterceptor implements HttpInterceptor {
this.store.dispatch(setCurrentUser({user: null, terms_of_use: null}));
this.router.navigate(['login'], {queryParams: {redirect: redirectUrl}, replaceUrl: true});
}
return throwError(err);
return throwError(() => err);
} else {
return throwError(err);
return throwError(() => err);
}
}
}

View File

@@ -149,5 +149,9 @@ export const commonAuthReducer = [
credentials: {[action.credentials[0]?.company || action.workspace]: action.credentials, ...action.extra}, revokeSucceed: false
})),
on(setSignedUrl, (state, action) => ({...state, signedUrls: {...state.signedUrls, [action.url]: {signed: action.signed, expires: action.expires}}})),
on(removeSignedUrl, (state, action) => ({...state, signedUrls: {...state.signedUrls, [action.url]: null}})),
on(removeSignedUrl, (state, action) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {[action.url]: remove, ...rest} = state.signedUrls;
return {...state, signedUrls: rest};
}),
] as ReducerTypes<AuthState, any>[];

View File

@@ -178,6 +178,7 @@ export const projectsReducer = createReducer(
companyTags: action.tags,
systemTags: action.systemTags
})),
on(projectsActions.addCompanyTag, (state, action): RootProjects => ({...state, companyTags: Array.from(new Set(state.companyTags.concat(action.tag))).sort()})),
on(projectsActions.addProjectTags, (state, action): RootProjects => ({
...state,
projectTags: Array.from(new Set(state.projectTags.concat(action.tags))).sort()

View File

@@ -39,16 +39,24 @@ export const initUsers: UsersState = {
};
export const users = state => state.users as UsersState;
export const selectSettings = createSelector(users, (state): any => state?.settings);
export const selectSettings = createSelector(users, (state) => state?.settings);
export const selectMaxDownloadItems = createSelector(selectSettings, (state): number => state?.max_download_items ?? 1000);
export const selectCurrentUser = createSelector(users, state => state.currentUser);
export const selectActiveWorkspace = createSelector(users, state => state.activeWorkspace);
export const selectActiveWorkspaceTier = createSelector(selectActiveWorkspace, workspace => workspace?.tier);
export const selectUserWorkspaces = createSelector(users, state => state.userWorkspaces);
export const selectSelectedWorkspaceTab = createSelector(users, state => state.selectedWorkspaceTab);
export const selectWorkspaces = createSelector(users, state => state.workspaces);
export const selectShowOnlyUserWork = createSelector(users, state => state.showOnlyUserWork);
export const selectServerVersions = createSelector(users, state => state.serverVersions);
export const selectGettingStarted = createSelector(users, state => state.gettingStarted);
export const selectWorkspaceOwner = createSelector(selectActiveWorkspace, selectUserWorkspaces, (active, workspaces) => {
if (workspaces && active) {
const activeWs = workspaces.find(ws => ws.id === active.id);
return activeWs?.owners?.[0]?.name || '';
}
return null;
});
export const usersReducerFunctions = [
on(fetchCurrentUser, state => ({...state})),

View File

@@ -15,7 +15,7 @@ export const searchSetTerm = createAction(
export const searchStart = createAction(
SEARCH_PREFIX + 'SEARCH_START',
props<{ query: string; regExp?: boolean; force?: boolean; activeLink: ActiveSearchLink }>()
props<{ query: string; regExp?: boolean }>()
);
export const searchClear = createAction(SEARCH_PREFIX + 'SEARCH_CLEAR');

View File

@@ -1,5 +1,5 @@
import {Injectable} from '@angular/core';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {Actions, concatLatestFrom, createEffect, ofType} from '@ngrx/effects';
import {activeLoader, deactivateLoader} from '../core/actions/layout.actions';
import {
getCurrentPageResults,
@@ -26,7 +26,7 @@ import {selectActiveSearch, selectSearchScrollIds, selectSearchTerm} from './das
import {ProjectsGetAllExRequest} from '~/business-logic/model/projects/projectsGetAllExRequest';
import {ApiTasksService} from '~/business-logic/api-services/tasks.service';
import {ApiModelsService} from '~/business-logic/api-services/models.service';
import {catchError, mergeMap, map, switchMap, withLatestFrom} from 'rxjs/operators';
import {catchError, mergeMap, map, switchMap} from 'rxjs/operators';
import {isEqual} from 'lodash-es';
import {activeSearchLink} from '~/features/dashboard-search/dashboard-search.consts';
import {emptyAction} from '~/app.constants';
@@ -35,6 +35,7 @@ import {selectCurrentUser, selectShowOnlyUserWork} from '@common/core/reducers/u
import {selectHideExamples, selectShowHidden} from '@common/core/reducers/projects.reducer';
import {ApiReportsService} from '~/business-logic/api-services/reports.service';
import {Report} from '~/business-logic/model/reports/report';
import {excludedKey} from '@common/shared/utils/tableParamEncode';
export const getEntityStatQuery = (action, searchHidden) => ({
/* eslint-disable @typescript-eslint/naming-convention */
@@ -52,7 +53,7 @@ export const getEntityStatQuery = (action, searchHidden) => ({
fields: ['name', 'id']
},
search_hidden: searchHidden,
type: ['__$not', 'annotation_manual', '__$not', 'annotation', '__$not', 'dataset_import'],
type: [excludedKey, 'annotation_manual', excludedKey, 'annotation', excludedKey, 'dataset_import'],
system_tags: ['-archived', '-pipeline', '-dataset'],
},
models: {
@@ -92,6 +93,9 @@ export const getEntityStatQuery = (action, searchHidden) => ({
/* eslint-enable @typescript-eslint/naming-convention */
});
export const orderBy = ['-last_update'];
@Injectable()
export class DashboardSearchEffects {
constructor(
@@ -100,7 +104,7 @@ export class DashboardSearchEffects {
public modelsApi: ApiModelsService,
public experimentsApi: ApiTasksService,
public reportsApi: ApiReportsService,
private store: Store<any>
private store: Store
) {
}
@@ -111,29 +115,26 @@ export class DashboardSearchEffects {
startSearch = createEffect(() => this.actions.pipe(
ofType(searchStart),
withLatestFrom(
concatLatestFrom(() => [
this.store.select(selectActiveSearch),
this.store.select(selectSearchTerm)
),
]),
mergeMap(([action, active, term]) => {
const actionsToFire = [];
if (!active) {
actionsToFire.push(searchClear());
actionsToFire.push(searchActivate());
}
if (!isEqual(term, action)) {
actionsToFire.push(getResultsCount(action));
actionsToFire.push(searchSetTerm(action));
}
actionsToFire.push(getResultsCount(term));
return actionsToFire;
})
));
getCurrentPageResults = createEffect(() => this.actions.pipe(
ofType(getCurrentPageResults),
withLatestFrom(
concatLatestFrom(() => [
this.store.select(selectSearchTerm)
),
]),
map(([action, term]) => {
switch (action.activeLink) {
case activeSearchLink.experiments:
@@ -156,13 +157,13 @@ export class DashboardSearchEffects {
searchProjects = createEffect(() => this.actions.pipe(
ofType(searchProjects),
withLatestFrom(
concatLatestFrom(() => [
this.store.select(selectSearchScrollIds),
this.store.select(selectShowOnlyUserWork),
this.store.select(selectCurrentUser),
this.store.select(selectHideExamples),
this.store.select(selectShowHidden),
),
]),
switchMap(([action, scrollIds, userFocus, user, hideExamples, showHidden]) => this.projectsApi.projectsGetAllEx({
_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
@@ -178,7 +179,8 @@ export class DashboardSearchEffects {
...(userFocus && {active_users: [user.id]}),
...(hideExamples && {allow_public: false}),
include_stats: true,
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination', 'basename']
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination', 'basename'],
order_by: orderBy,
/* eslint-enable @typescript-eslint/naming-convention */
}).pipe(
mergeMap(res => [setProjectsResults({
@@ -190,12 +192,12 @@ export class DashboardSearchEffects {
searchPipelines = createEffect(() => this.actions.pipe(
ofType(searchPipelines),
withLatestFrom(
concatLatestFrom(() => [
this.store.select(selectSearchScrollIds),
this.store.select(selectShowOnlyUserWork),
this.store.select(selectCurrentUser),
this.store.select(selectHideExamples),
),
]),
switchMap(([action, scrollIds, userFocus, user, hideExamples]) => this.projectsApi.projectsGetAllEx({
_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
@@ -211,7 +213,8 @@ export class DashboardSearchEffects {
size: SEARCH_PAGE_SIZE,
...(userFocus && {active_users: [user.id]}),
include_stats: true,
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination', 'tags', 'system_tags', 'basename']
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination', 'tags', 'system_tags', 'basename'],
order_by: orderBy,
/* eslint-enable @typescript-eslint/naming-convention */
}).pipe(
mergeMap(res => [setPipelinesResults({
@@ -223,12 +226,12 @@ export class DashboardSearchEffects {
searchOpenDatasets = createEffect(() => this.actions.pipe(
ofType(searchOpenDatasets),
withLatestFrom(
concatLatestFrom(() => [
this.store.select(selectSearchScrollIds),
this.store.select(selectShowOnlyUserWork),
this.store.select(selectCurrentUser),
this.store.select(selectHideExamples),
),
]),
switchMap(([action, scrollIds, userFocus, user, hideExamples]) => this.projectsApi.projectsGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */
_any_: {
@@ -247,7 +250,8 @@ export class DashboardSearchEffects {
include_dataset_stats: true,
stats_with_children: false,
include_stats: true,
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination', 'tags', 'system_tags', 'basename']
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination', 'tags', 'system_tags', 'basename'],
order_by: orderBy,
/* eslint-enable @typescript-eslint/naming-convention */
}).pipe(
mergeMap(res => [setOpenDatasetsResults({
@@ -260,12 +264,12 @@ export class DashboardSearchEffects {
searchModels = createEffect(() => this.actions.pipe(
ofType(searchModels),
withLatestFrom(
concatLatestFrom(() => [
this.store.select(selectSearchScrollIds),
this.store.select(selectShowOnlyUserWork),
this.store.select(selectCurrentUser),
this.store.select(selectHideExamples),
),
]),
switchMap(([action, scrollIds, userFocus, user, hideExamples]) => this.modelsApi.modelsGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */
_any_: {
@@ -278,7 +282,8 @@ export class DashboardSearchEffects {
...(hideExamples && {allow_public: false}),
system_tags: ['-archived'],
include_stats: true,
only_fields: ['ready', 'created', 'framework', 'user.name', 'name', 'parent.name', 'task.name', 'id', 'company']
only_fields: ['ready', 'created', 'framework', 'user.name', 'name', 'parent.name', 'task.name', 'id', 'company'],
order_by: orderBy,
/* eslint-enable @typescript-eslint/naming-convention */
}).pipe(
mergeMap(res => [setModelsResults({models: res.models, scrollId: res.scroll_id}), deactivateLoader(action.type)]),
@@ -287,13 +292,13 @@ export class DashboardSearchEffects {
searchExperiments = createEffect(() => this.actions.pipe(
ofType(searchExperiments),
withLatestFrom(
concatLatestFrom(() => [
this.store.select(selectSearchScrollIds),
this.store.select(selectShowOnlyUserWork),
this.store.select(selectCurrentUser),
this.store.select(selectHideExamples),
this.store.select(selectShowHidden),
),
]),
switchMap(([action, scrollIds, userFocus, user, hideExamples, showHidden]) => this.experimentsApi.tasksGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */
_any_: {
@@ -305,7 +310,8 @@ export class DashboardSearchEffects {
...(userFocus && {user: [user.id]}),
...(hideExamples && {allow_public: false}),
only_fields: EXPERIMENT_SEARCH_ONLY_FIELDS,
type: ['__$not', 'annotation_manual', '__$not', 'annotation', '__$not', 'dataset_import'],
order_by: orderBy,
type: [excludedKey, 'annotation_manual', excludedKey, 'annotation', excludedKey, 'dataset_import'],
system_tags: ['-archived', '-pipeline', '-dataset'],
search_hidden: showHidden,
/* eslint-enable @typescript-eslint/naming-convention */
@@ -319,12 +325,12 @@ export class DashboardSearchEffects {
searchReports = createEffect(() => this.actions.pipe(
ofType(searchReports),
withLatestFrom(
concatLatestFrom(() => [
this.store.select(selectSearchScrollIds),
this.store.select(selectShowOnlyUserWork),
this.store.select(selectCurrentUser),
this.store.select(selectHideExamples),
),
]),
switchMap(([action, scrollIds, userFocus, user, hideExamples]) => this.reportsApi.reportsGetAllEx({
_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
@@ -337,6 +343,7 @@ export class DashboardSearchEffects {
...(userFocus && {user: [user.id]}),
system_tags: ['-archived'],
only_fields: ['name', 'comment', 'company', 'tags', 'report', 'project.name', 'user.name', 'status', 'last_update', 'system_tags'] as (keyof Report)[],
order_by: orderBy,
}).pipe(
mergeMap(res => [setReportsResults({
reports: res.tasks,

View File

@@ -18,6 +18,7 @@ import {Store} from '@ngrx/store';
import {ErrorService} from '../shared/services/error.service';
import {selectCurrentUser, selectShowOnlyUserWork} from '../core/reducers/users-reducer';
import {selectHideExamples, selectShowHidden} from '@common/core/reducers/projects.reducer';
import {excludedKey} from '@common/shared/utils/tableParamEncode';
@Injectable()
export class CommonDashboardEffects {
@@ -72,7 +73,7 @@ export class CommonDashboardEffects {
page_size: 5,
order_by: ['-last_update'],
status: ['published', 'closed', 'failed', 'stopped', 'in_progress', 'completed'],
type: ['__$not', 'annotation_manual', '__$not', 'annotation', '__$not', 'dataset_import'],
type: [excludedKey, 'annotation_manual', excludedKey, 'annotation', excludedKey, 'dataset_import'],
only_fields: ['type', 'status', 'created', 'name', 'id', 'last_update', 'started', 'project.name'],
system_tags: ['-archived', '-pipeline'],
user: showOnlyUserWork ? [user.id] : null,

View File

@@ -5,7 +5,7 @@ import {User} from '~/business-logic/model/users/user';
import {setRecentExperiments, setRecentProjects} from './common-dashboard.actions';
export interface IRecentTask {
id?: Task['id'];
id: Task['id'];
name?: Task['name'];
user?: User;
type?: Task['type'];

View File

@@ -1,7 +1,7 @@
import {initSearch, resetSearch} from '../common-search/common-search.actions';
import {filter, skip} from 'rxjs/operators';
import {distinctUntilChanged, distinctUntilKeyChanged, filter} from 'rxjs/operators';
import {Model} from '~/business-logic/model/models/model';
import {clearSearchResults, getCurrentPageResults, searchClear, searchDeactivate, searchStart} from '../dashboard-search/dashboard-search.actions';
import {clearSearchResults, getCurrentPageResults, searchClear, searchDeactivate, searchSetTerm, searchStart} from '../dashboard-search/dashboard-search.actions';
import {IRecentTask} from './common-dashboard.reducer';
import {ITask} from '~/business-logic/model/al-task';
import {combineLatest, Observable, Subscription} from 'rxjs';
@@ -23,10 +23,11 @@ import {Project} from '~/business-logic/model/projects/project';
import {setSelectedProjectId} from '../core/actions/projects.actions';
import {isExample} from '../shared/utils/shared-utils';
import {activeLinksList, ActiveSearchLink, activeSearchLink} from '~/features/dashboard-search/dashboard-search.consts';
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {ChangeDetectorRef, Component, inject, OnDestroy, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import { selectShowOnlyUserWork } from '@common/core/reducers/users-reducer';
import {IReport} from '@common/reports/reports.consts';
import {isEqual} from "lodash-es";
@Component({
selector: 'sm-dashboard-search-base',
@@ -36,7 +37,7 @@ import {IReport} from '@common/reports/reports.consts';
(experimentSelected)="taskSelected($event)"
(modelSelected)="modelSelected($event)"
(pipelineSelected)="pipelineSelected($event)"
(activeLinkChanged)="activeLinkChanged($event)"
(activeLinkChanged)="changeActiveLink($event)"
(reportSelected)="reportSelected($event)"
(openDatasetSelected)="openDatasetCardClicked($event)"
(loadMoreClicked)="loadMore()"
@@ -53,7 +54,6 @@ import {IReport} from '@common/reports/reports.consts';
export class DashboardSearchBaseComponent implements OnInit, OnDestroy{
public activeLink = 'projects' as ActiveSearchLink;
private searchSubs;
private allResultsSubscription: Subscription;
public searchQuery$: Observable<SearchState['searchQuery']>;
public activeSearch$: Observable<boolean>;
public modelsResults$: Observable<Array<Model>>;
@@ -65,32 +65,54 @@ export class DashboardSearchBaseComponent implements OnInit, OnDestroy{
private scrollIds: Map<ActiveSearchLink, string>;
public resultsCount$: Observable<Map<ActiveSearchLink, number>>;
public reportsResults$: Observable<Array<IReport>>;
constructor(public store: Store, public router: Router){
this.searchQuery$ = store.select(selectSearchQuery);
this.activeSearch$ = store.select(selectActiveSearch);
this.modelsResults$ = store.select(selectModelsResults);
this.reportsResults$ = store.select(selectReportsResults);
this.pipelinesResults$ = store.select(selectPipelinesResults);
this.datasetsResults$ = store.select(selectDatasetsResults);
this.projectsResults$ = store.select(selectProjectsResults);
this.experimentsResults$ = store.select(selectExperimentsResults);
this.searchTerm$ = store.select(selectSearchTerm);
this.resultsCount$ = store.select(selectResultsCount);
public store = inject(Store);
public router = inject(Router);
public route = inject(ActivatedRoute);
private subs = new Subscription();
private cdr: ChangeDetectorRef;
constructor() {
this.cdr = inject(ChangeDetectorRef);
this.searchQuery$ = this.store.select(selectSearchQuery);
this.activeSearch$ = this.store.select(selectActiveSearch);
this.modelsResults$ = this.store.select(selectModelsResults);
this.reportsResults$ = this.store.select(selectReportsResults);
this.pipelinesResults$ = this.store.select(selectPipelinesResults);
this.datasetsResults$ = this.store.select(selectDatasetsResults);
this.projectsResults$ = this.store.select(selectProjectsResults);
this.experimentsResults$ = this.store.select(selectExperimentsResults);
this.searchTerm$ = this.store.select(selectSearchTerm);
this.resultsCount$ = this.store.select(selectResultsCount);
this.syncAppSearch();
}
public ngOnInit(): void {
this.allResultsSubscription = this.resultsCount$.pipe(
this.subs.add(this.resultsCount$.pipe(
filter(resultsCount => !!resultsCount),
).subscribe((resultsCount) => this.setFirstActiveLink(resultsCount));
distinctUntilChanged()
).subscribe((resultsCount) => this.setFirstActiveLink(resultsCount)));
this.subs.add(this.route.queryParams.pipe(distinctUntilKeyChanged('tab'))
.subscribe(params => {
if (params.tab && activeLinksList.find( link => link.name === params.tab)) {
this.activeLink = params.tab;
this.activeLinkChanged(this.activeLink)
this.cdr.markForCheck();
}
}));
this.subs.add(this.searchQuery$.pipe(
distinctUntilChanged((previous, current) => isEqual(previous, current)))
.subscribe(query => {
this.store.dispatch(searchSetTerm(query));
}));
}
ngOnDestroy(): void {
this.store.dispatch(searchClear());
this.searchTermChanged('');
this.stopSyncSearch();
this.allResultsSubscription.unsubscribe();
this.subs.unsubscribe();
this.searchSubs.unsubscribe();
}
stopSyncSearch() {
@@ -102,10 +124,9 @@ export class DashboardSearchBaseComponent implements OnInit, OnDestroy{
this.store.dispatch(initSearch({payload: 'Search for all'}));
this.searchSubs = combineLatest([
this.searchQuery$,
this.searchTerm$,
this.store.select(selectShowOnlyUserWork),
])
.pipe(skip(1))
.subscribe(([query]) => this.searchTermChanged(query?.query, query?.regExp));
this.searchSubs.add(this.store.select(selectSearchScrollIds).subscribe(scrollIds => this.scrollIds = scrollIds));
@@ -119,7 +140,7 @@ export class DashboardSearchBaseComponent implements OnInit, OnDestroy{
public searchTermChanged(term: string, regExp?: boolean) {
if (term && term.length > 0) {
this.store.dispatch(searchStart({query:term, regExp, force: term.length < 3, activeLink: this.activeLink}));
this.store.dispatch(searchStart({query:term, regExp}));
} else {
this.activeLink = activeSearchLink.projects;
this.store.dispatch(searchDeactivate());
@@ -153,7 +174,6 @@ export class DashboardSearchBaseComponent implements OnInit, OnDestroy{
public activeLinkChanged(activeLink) {
this.activeLink = activeLink;
if (!this.scrollIds?.[activeLink]) {
this.store.dispatch(getCurrentPageResults({activeLink}));
}
@@ -162,11 +182,13 @@ export class DashboardSearchBaseComponent implements OnInit, OnDestroy{
setFirstActiveLink(resultsCount) {
if (resultsCount[this.activeLink] > 0) {
this.activeLinkChanged(this.activeLink);
this.router.navigate([], {queryParams: {tab: this.activeLink}, queryParamsHandling: "merge", replaceUrl: true})
this.store.dispatch(getCurrentPageResults({activeLink: this.activeLink}));
} else {
const firstTabIndex = activeLinksList.findIndex(activeLink => resultsCount[activeLink.name] > 0);
if (firstTabIndex > -1) {
this.activeLinkChanged(activeLinksList[firstTabIndex].name);
this.router.navigate([], {queryParams: {tab: activeLinksList[firstTabIndex].name}, queryParamsHandling: "merge"})
this.store.dispatch(getCurrentPageResults({activeLink: activeLinksList[firstTabIndex].name as ActiveSearchLink}));
} else {
this.store.dispatch(clearSearchResults());
}
@@ -176,4 +198,8 @@ export class DashboardSearchBaseComponent implements OnInit, OnDestroy{
loadMore() {
this.store.dispatch(getCurrentPageResults({activeLink: this.activeLink}));
}
changeActiveLink(tab: string) {
this.router.navigate([], {queryParams: {tab}, queryParamsHandling: "merge"})
}
}

View File

@@ -1,4 +1,4 @@
@import "src/app/webapp-common/shared/ui-components/styles/variables";
@import "variables";
:host {
flex: 0 0 220px;

View File

@@ -1,14 +1,43 @@
import { Component} from '@angular/core';
import {
PipelineControllerStepComponent
} from '@common/pipelines-controller/pipeline-controller-step/pipeline-controller-step.component';
import { fileSizeConfigStorage } from '@common/shared/pipes/filesize.pipe';
import {ChangeDetectorRef, Component, EventEmitter, Input, Output} from '@angular/core';
import {DagModelItem} from '@ngneat/dag';
import {StepStatusEnum} from '@common/pipelines-controller/pipeline-controller-info/pipeline-controller-info.component';
import {fileSizeConfigStorage} from '@common/shared/pipes/filesize.pipe';
export interface TreeVersion {
name: string;
version: string;
job_id: string;
status: StepStatusEnum,
last_update: number,
parents: string[],
job_size: number
}
export interface VersionItem extends DagModelItem {
name: string;
id: string;
data: TreeVersion;
}
@Component({
selector: 'sm-dataset-version-step',
templateUrl: './dataset-version-step.component.html',
styleUrls: ['../../pipelines-controller/pipeline-controller-step/pipeline-controller-step.component.scss', './dataset-version-step.component.scss']
})
export class DatasetVersionStepComponent extends PipelineControllerStepComponent{
fileSizeConfigStorage = fileSizeConfigStorage;
export class DatasetVersionStepComponent {
protected _step: VersionItem;
protected readonly fileSizeConfigStorage = fileSizeConfigStorage;
constructor(private cdr: ChangeDetectorRef) { }
@Input() set step(step: VersionItem) {
this._step = {...step, data: {...step.data, status: step.data?.status || StepStatusEnum.pending}};
}
get step() {
return this._step;
}
@Input() selected: boolean;
@Output() openConsole = new EventEmitter();
}

View File

@@ -6,14 +6,15 @@
[action]="'leave'"
(smHesitate)="menu.closed.emit();"
>
<p class="command">{{command}}</p>
<div class="w-100 d-flex flex-row-reverse">
<p class="command">{{command}}</p>
<div class="w-100 d-flex action">
<i class="al-icon al-ico-success sm me-1" [class.visible]="copySuccess"></i>
<div
class="d-flex-center copy-button pointer"
ngxClipboard
[cbContent]="command"
(cbOnSuccess)="$event.event.stopPropagation(); copied()"
>Copy command</div><i class="al-icon al-ico-success sm me-1" [class.visible]="copySuccess"></i>
>Copy command</div>
</div>
</div>
</mat-menu>
@@ -23,30 +24,32 @@
[delay]="1000" [action]="'leave'"
(smHesitate)="menuHesitate.hesitateStatus && menu.closed.emit()"
>
<i class="al-icon al-ico-download pointer line-item"
data-id="downloadButton"
#idElement
[matMenuTriggerFor]="menu"
(click)="openMenu(); menuHesitate.hesitateStatus = true"
></i>
<button
class="btn"
data-id="downloadButton"
[matMenuTriggerFor]="menu"
(click)="openMenu(); menuHesitate.hesitateStatus = true"
><i class="al-icon al-ico-download pointer line-item"></i></button>
</span>
<sm-table
[columns]="columns"
[tableData]="tableData"
[selectionMode]="null"
[virtualScroll]="true"
[scrollable]="true">
[scrollable]="true"
[resizableColumns]="true"
(columnResized)="resizeCol($event)"
>
<ng-template
let-col
let-i="rowIndex"
let-row="rowData"
let-line="rowData"
pTemplate="body">
<div [ngSwitch]="col.id" class="w-100">
<ng-container *ngSwitchCase="'0'">
<span class="ellipsis" [attr.fileType]="row[0].match('\\.([^ .]+)$')?.[1] || 'none'" smShowTooltipIfEllipsis [smTooltip]="row[0]">{{row[0]}}</span>
<div [ngSwitch]="col.id">
<ng-container *ngSwitchCase="'name'">
<span class="ellipsis" [attr.fileType]="line.name?.match('\\.([^ .]+)$')?.[1] || 'none'" smShowTooltipIfEllipsis [smTooltip]="line.name">{{line.name}}</span>
</ng-container>
<div *ngSwitchDefault class="ellipsis" smShowTooltipIfEllipsis [smTooltip]="row[col.id]">{{row[col.id]}}</div>
<div *ngSwitchDefault class="ellipsis" smShowTooltipIfEllipsis [smTooltip]="line[col.id]">{{line[col.id]}}</div>
</div>
</ng-template>
</sm-table>

View File

@@ -1,4 +1,5 @@
@import 'src/app/webapp-common/shared/ui-components/styles/variables';
@import 'variables';
@import "../../../webapp-common/assets/fonts/variables.scss";
:host {
display: block;
@@ -25,7 +26,7 @@
[filetype]::before {
content: "\e9d3"; // fallback icon (file)
color: #596c71;
font-family: 'trains';
font-family: #{$icomoon-font-family};
font-size: 24px;
margin-right: 12px;
vertical-align: middle;

View File

@@ -1,8 +1,17 @@
import {ChangeDetectionStrategy, Component, Input, ViewChild} from '@angular/core';
import {ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild} from '@angular/core';
import {MatMenuTrigger} from '@angular/material/menu';
import {ISmCol} from '@common/shared/ui-components/data/table/table.consts';
import {fileSizeConfigStorage, FileSizePipe} from '@common/shared/pipes/filesize.pipe';
const columnIds = ['name', 'size', 'hash'];
type Row = { [K in typeof columnIds[number]]: string }
interface RowData extends Row {
id: string
}
@Component({
selector: 'sm-simple-dataset-version-content',
templateUrl: './simple-dataset-version-content.component.html',
@@ -11,12 +20,13 @@ import {fileSizeConfigStorage, FileSizePipe} from '@common/shared/pipes/filesize
})
export class SimpleDatasetVersionContentComponent {
public columns: ISmCol[];
public tableData: string[][];
public tableData: RowData[];
public command: string;
private ngFile = new FileSizePipe();
@ViewChild(MatMenuTrigger) trigger: MatMenuTrigger;
copySuccess: boolean;
private colSizes = {} as { [colId: string]: number };
@Input() set id(id: string) {
this.command = `clearml-data get --id ${id}`;
@@ -24,21 +34,43 @@ export class SimpleDatasetVersionContentComponent {
}
@Input() set data(csv: string) {
const lines = csv?.split('\n') ?? [];
const lines = csv?.trimEnd().split('\n') ?? [];
const header = lines.splice(0, 1)[0] ?? '';
this.columns = header.split(/, ?/).map((caption, index) => ({
id: `${index}`,
header: caption,
style: {width: index === 1 ? '5px' : '300px'}
}));
const tableData = lines.map(line => line.split(/, ?/));
if (Number(tableData[0]?.[1]) && this.columns[1]?.header?.includes('ize')) {
tableData.forEach( line => line[1] = this.ngFile.transform(parseInt(line[1], 10) || 0, fileSizeConfigStorage) as string);
const colWidth = (this.ref.nativeElement.getBoundingClientRect().width - 150) / 2 ?? 300;
this.columns = header.split(/, ?/)
.map((caption, index) => {
const width = this.colSizes?.[columnIds[index]] ? `${this.colSizes[columnIds[index]]}px` : null;
return {
id: columnIds[index],
header: caption,
style: index === 1 ?
{width: width ?? '150px'} :
{
// maxWidth: width ?? `${colWidth}px`,
width: width ?? `${colWidth}px`,
}
};
});
const tableData = lines.map((line, index) => ({
...line.split(/, ?/).reduce((acc, val, i) => {
acc[columnIds[i]] = val;
return acc;
}, {} as Row),
id: `${index}`
} as RowData));
if (Number(tableData[0]?.size) && this.columns[1]?.header?.includes('ize')) {
this.tableData = tableData.map( line => ({
...line,
size: this.ngFile.transform(parseInt(line.size, 10) || 0, fileSizeConfigStorage) as string
}));
} else {
this.tableData = tableData;
}
this.tableData = tableData;
}
constructor(private readonly ref: ElementRef){}
openMenu() {
this.trigger.openMenu();
}
@@ -47,4 +79,9 @@ export class SimpleDatasetVersionContentComponent {
this.copySuccess = true;
window.setTimeout(() => this.copySuccess = false, 3000);
}
resizeCol({columnId, widthPx}: {columnId: string, widthPx: number}) {
this.colSizes[columnId] = widthPx;
this.columns = this.columns.map(col => col.id === columnId ? {...col, style: {width: widthPx + 'px'}} : col);
}
}

View File

@@ -77,7 +77,7 @@
</div>
</div>
<div class="section">
<div class="header"><span>DESCRIPTION</span><i data-id="editIcon" class="al-icon al-ico-edit sm" (click)="editDescription.emit(entity)"></i></div>
<div class="header"><span>DESCRIPTION</span><i tabindex="1" data-id="editIcon" class="al-icon al-ico-edit sm" (click)="editDescription.emit(entity)" (keyup)="editDescription.emit(entity)"></i></div>
<div class="param continue h-auto">
<div class="multi-line-value">{{entity?.comment}}</div>
</div>

View File

@@ -2,7 +2,7 @@ import {Component, EventEmitter, Output} from '@angular/core';
import {PipelineInfoComponent} from '@common/pipelines-controller/pipeline-details/pipeline-info.component';
import { fileSizeConfigStorage } from '@common/shared/pipes/filesize.pipe';
import {DATASETS_STATUS_LABEL, EXPERIMENTS_STATUS_LABELS} from '~/features/experiments/shared/experiments.const';
import {Task} from '~/business-logic/model/tasks/task';
import {IExperimentInfo} from '~/features/experiments/shared/experiment-info.model';
@Component({
selector: 'sm-simple-dataset-version-details',
@@ -10,10 +10,10 @@ import {Task} from '~/business-logic/model/tasks/task';
styleUrls: ['./simple-dataset-version-details.component.scss', '../../pipelines-controller/pipeline-details/pipeline-info.component.scss']
})
export class SimpleDatasetVersionDetailsComponent extends PipelineInfoComponent {
public fileSizeConfigStorage = fileSizeConfigStorage;
public override fileSizeConfigStorage = fileSizeConfigStorage;
public convertStatusMap = DATASETS_STATUS_LABEL;
public convertStatusMapBase = EXPERIMENTS_STATUS_LABELS;
@Output() editDescription = new EventEmitter<Task>();
@Output() editDescription = new EventEmitter<IExperimentInfo>();
}

View File

@@ -1,14 +1,14 @@
import {ChangeDetectorRef, Component, NgZone} from '@angular/core';
import {Component} from '@angular/core';
import {PipelineControllerInfoComponent, PipelineItem, StatusOption} from '@common/pipelines-controller/pipeline-controller-info/pipeline-controller-info.component';
import {DagManagerUnsortedService} from '@common/shared/services/dag-manager-unsorted.service';
import {experimentDetailsUpdated, getSelectedPipelineStep, setSelectedPipelineStep} from '@common/experiments/actions/common-experiments-info.actions';
import {last} from 'lodash-es';
import {Store} from '@ngrx/store';
import {MatDialog} from '@angular/material/dialog';
import {EditJsonComponent, EditJsonData} from '@common/shared/ui-components/overlay/edit-json/edit-json.component';
import {Task} from '~/business-logic/model/tasks/task';
import {CommonExperimentsInfoEffects} from '@common/experiments/effects/common-experiments-info.effects';
import {tap} from 'rxjs/operators';
import {TaskStatusEnum} from '~/business-logic/model/tasks/taskStatusEnum';
import {IExperimentInfo} from '~/features/experiments/shared/experiment-info.model';
@Component({
selector: 'sm-simple-dataset-version-info',
@@ -20,22 +20,17 @@ import {tap} from 'rxjs/operators';
providers: [DagManagerUnsortedService]
})
export class SimpleDatasetVersionInfoComponent extends PipelineControllerInfoComponent {
detailsPanelMode = StatusOption.content;
defaultDetailsMode = StatusOption.content;
public maximizeResults: boolean;
constructor(
protected _dagManager: DagManagerUnsortedService<PipelineItem>,
protected store: Store,
protected cdr: ChangeDetectorRef,
protected zone: NgZone,
private dialog: MatDialog,
private commonExperimentsInfoEffects: CommonExperimentsInfoEffects
) {
super(_dagManager, store, cdr, zone);
super();
this.detailsPanelMode = StatusOption.content;
this.defaultDetailsMode = StatusOption.content;
}
convertPipelineToDagModel(pipeline): PipelineItem[] {
override convertPipelineToDagModel(pipeline): PipelineItem[] {
const res = super.convertPipelineToDagModel(pipeline);
if (res?.length > 0) {
window.setTimeout(() => this.selectStep(last(res)), 1000);
@@ -45,15 +40,15 @@ export class SimpleDatasetVersionInfoComponent extends PipelineControllerInfoCom
return res;
}
getEntityId(params) {
override getEntityId(params) {
return params?.versionId;
}
protected getTreeObject(task) {
protected override getTreeObject(task) {
return task?.configuration?.['Dataset Struct']?.value;
}
toggleResultSize() {
override toggleResultSize() {
this.maximizeResults = ! this.maximizeResults;
if (this.detailsPanelMode === StatusOption.content) {
this.detailsPanelMode = null;
@@ -64,13 +59,13 @@ export class SimpleDatasetVersionInfoComponent extends PipelineControllerInfoCom
}
}
selectStep(step?: PipelineItem) {
override selectStep(step?: PipelineItem) {
if (step) {
const id = step?.data?.job_id;
if (id) {
this.store.dispatch(getSelectedPipelineStep({id}));
} else {
this.store.dispatch(setSelectedPipelineStep({step: {id, type: step.data.job_type, status: step.data.status, name: step.name}}));
this.store.dispatch(setSelectedPipelineStep({step: {id, type: step.data.job_type, status: step.data.status as unknown as TaskStatusEnum, name: step.name}}));
this.showLog = false;
}
this.selectedEntity = step;
@@ -82,13 +77,15 @@ export class SimpleDatasetVersionInfoComponent extends PipelineControllerInfoCom
this.showLog = !this.showLog;
}
protected getPanelMode() {
protected override getPanelMode() {
return this.detailsPanelMode;
}
protected resetUninitializedRunningFields() {}
protected override resetUninitializedRunningFields() {
editDescription(dataset: Task) {
}
editDescription(dataset: IExperimentInfo) {
const editJsonComponent = this.dialog.open(EditJsonComponent, {
data: {
textData: dataset.comment,

View File

@@ -45,7 +45,7 @@
selectionMode="single"
[initialColumns]="tableCols"
[colsOrder]="(tableColsOrder$ | async)"
[tableCols]="tableCols$| async"
[tableCols]="tableCols$ | async"
[experiments]="experiments$ | async"
[users]="users$ | async"
[hyperParamsOptions]="hyperParamsOptions$ | async"
@@ -105,14 +105,15 @@
#contextMenu
[experiment]="contextExperiment"
[selectedExperiment]="selectedExperiment$ | async"
[selectedExperiments]="singleRowContext ? [contextExperiment] : selectedExperiments"
[selectedExperiments]="singleRowContext ? (selectedExperiment$ | async) ? [(selectedExperiment$ | async)] : undefined : selectedExperiments"
[selectedDisableAvailableIsMultiple]="!singleRowContext"
[selectedDisableAvailable]="singleRowContext ? getSingleSelectedDisableAvailable(contextExperiment) : (selectedExperimentsDisableAvailable$ | async)"
[selectedDisableAvailable]="singleRowContext ? getSingleSelectedDisableAvailable((selectedExperiment$ | async) || contextExperiment ) : (selectedExperimentsDisableAvailable$ | async)"
[numSelected]="singleRowContext ? 1 : selectedExperiments.length"
[tagsFilterByProject]="tagsFilterByProject$ | async"
[projectTags]="tags$ | async"
[companyTags]="companyTags$ | async"
[activateFromMenuButton]="false"
[useCurrentEntity]="singleRowContext"
[minimizedView]="true"
[tableMode]="!minimizedView"
[backdrop]="menuBackdrop"

View File

@@ -6,10 +6,6 @@ import {CountAvailableAndIsDisableSelectedFiltered} from '@common/shared/entity-
import * as experimentsActions from '@common/experiments/actions/common-experiments-view.actions';
import {INITIAL_CONTROLLER_TABLE_COLS} from '@common/pipelines-controller/controllers.consts';
import {EXPERIMENTS_TABLE_COL_FIELDS} from '~/features/experiments/shared/experiments.const';
import {Store} from '@ngrx/store';
import {ActivatedRoute, Router} from '@angular/router';
import {MatDialog} from '@angular/material/dialog';
import {RefreshService} from '@common/core/services/refresh.service';
import {take, withLatestFrom} from 'rxjs/operators';
import {selectDefaultNestedModeForFeature} from '@common/core/reducers/projects.reducer';
import {setBreadcrumbsOptions} from '@common/core/actions/projects.actions';
@@ -20,26 +16,20 @@ import {setBreadcrumbsOptions} from '@common/core/actions/projects.actions';
styleUrls: ['./simple-dataset-versions.component.scss', '../../pipelines-controller/controllers.component.scss']
})
export class SimpleDatasetVersionsComponent extends ControllersComponent implements OnInit {
entityType = EntityTypeEnum.dataset;
public shouldOpenDetails = true;
isArchived: boolean;
protected getParamId(params) {
protected override getParamId(params) {
return params?.versionId;
}
constructor(protected store: Store,
protected route: ActivatedRoute,
protected router: Router,
protected dialog: MatDialog,
protected refresh: RefreshService
) {
super(store, route, router, dialog, refresh);
constructor() {
super();
this.entityType = EntityTypeEnum.dataset;
this.tableCols = INITIAL_CONTROLLER_TABLE_COLS.map((col) =>
col.id === EXPERIMENTS_TABLE_COL_FIELDS.NAME ? {...col, header: 'VERSION NAME'} : col);
}
ngOnInit() {
override ngOnInit() {
super.ngOnInit();
this.experiments$
.pipe(take(1))
@@ -57,7 +47,7 @@ export class SimpleDatasetVersionsComponent extends ControllersComponent impleme
});
}
createFooterItems(config: {
override createFooterItems(config: {
entitiesType: EntityTypeEnum;
selected$: Observable<Array<any>>;
showAllSelectedIsActive$: Observable<boolean>;
@@ -71,10 +61,10 @@ export class SimpleDatasetVersionsComponent extends ControllersComponent impleme
this.footerItems.splice(5, 1);
}
downloadTableAsCSV() {
override downloadTableAsCSV() {
this.table.table.downloadTableAsCSV(`ClearML ${this.selectedProject.id === '*'? 'All': this.selectedProject?.basename?.substring(0,60)} Datasets`);
}
setupBreadcrumbsOptions() {
override setupBreadcrumbsOptions() {
this.sub.add(this.selectedProject$.pipe(
withLatestFrom(this.store.select(selectDefaultNestedModeForFeature))
).subscribe(([selectedProject, defaultNestedModeForFeature]) => {

View File

@@ -1,10 +1,6 @@
<sm-dialog-template *ngIf="!showButton; else content" iconClass="al-icon al-ico-datasets al-color blue-300" header="CREATE NEW DATASET">
<ng-container *ngTemplateOutlet="content"></ng-container>
</sm-dialog-template>
<ng-template #content>
<div class="content">
<div class="code">

View File

@@ -1,3 +1,5 @@
@import "variables";
.content {
width: 100%;
display: flex;
@@ -16,8 +18,15 @@
flex: 1;
flex-direction: column;
a.dark {
color: white;
a {
color: $purple;
&.dark {
color: white;
}
&:hover {
text-decoration: underline;
}
}
sm-code-editor {

View File

@@ -23,7 +23,7 @@
<i class="al-icon al-ico-add sm me-2"></i>NEW DATASET
</button>
<div empty-state class="empty-datasets">
<div empty-state class="empty-state">
<div class="title-icon"><i class="al-icon al-ico-datasets xxl"></i></div>
<div class="title">NO DATASETS TO SHOW</div>
<div class="sub-title">Run your first dataset to see it displayed here

View File

@@ -19,16 +19,13 @@ import {
selectProjectTags
} from '@common/core/reducers/projects.reducer';
import {combineLatest, Observable, Subscription} from 'rxjs';
import {debounceTime, withLatestFrom} from 'rxjs/operators';
import {debounceTime, skip, withLatestFrom} from 'rxjs/operators';
import {getAllProjectsPageProjects, resetProjects} from '@common/projects/common-projects.actions';
@Component({
selector: 'sm-nested-simple-datasets-page',
templateUrl: './nested-simple-datasets-page.component.html',
styleUrls: [
'../../../webapp-common/nested-project-view/nested-project-view-page/nested-project-view-page.component.scss',
'../../../webapp-common/datasets/simple-datasets/empty.scss'
],
styleUrls: ['../../../webapp-common/nested-project-view/nested-project-view-page/nested-project-view-page.component.scss'],
imports: [
ProjectsSharedModule,
SMSharedModule,
@@ -45,7 +42,7 @@ export class NestedSimpleDatasetsPageComponent extends CommonProjectsPageCompone
projectsTags$: Observable<string[]>;
private mainPageFilterSub: Subscription;
projectCardClicked(data: { hasSubProjects: boolean; id: string; name: string }) {
override projectCardClicked(data: { hasSubProjects: boolean; id: string; name: string }) {
if (data.hasSubProjects) {
this.router.navigate(['simple', data.id, 'projects'], {relativeTo: this.route.parent?.parent});
} else {
@@ -68,11 +65,11 @@ export class NestedSimpleDatasetsPageComponent extends CommonProjectsPageCompone
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected getExtraProjects(selectedProjectId, selectedProject) {
protected override getExtraProjects(selectedProjectId, selectedProject) {
return [];
}
ngOnInit() {
override ngOnInit() {
super.ngOnInit();
this.projectsTags$ = this.store.select(selectProjectTags);
this.store.dispatch(getProjectsTags({entity: 'dataset'}));
@@ -80,28 +77,28 @@ export class NestedSimpleDatasetsPageComponent extends CommonProjectsPageCompone
this.mainPageFilterSub = combineLatest([
this.store.select(selectMainPageTagsFilter),
this.store.select(selectMainPageTagsFilterMatchMode)
]).pipe(debounceTime(0))
]).pipe(debounceTime(0), skip(1))
.subscribe(() => {
this.store.dispatch(resetProjects());
this.store.dispatch(getAllProjectsPageProjects());
});
}
noProjectsReRoute() {
override noProjectsReRoute() {
return this.router.navigate(['..', 'datasets'], {relativeTo: this.route});
}
shouldReRoute(selectedProject, config) {
override shouldReRoute(selectedProject, config) {
const relevantSubProjects = selectedProject?.sub_projects?.filter(proj => proj.name.includes('.datasets'));
return config[3] === 'projects' && selectedProject.id !== '*' && (relevantSubProjects?.every(subProject => subProject.name.startsWith(selectedProject.name + '/.')));
};
}
ngOnDestroy() {
override ngOnDestroy() {
super.ngOnDestroy();
this.mainPageFilterSub.unsubscribe();
this.store.dispatch(setTags({tags: []}));
}
setupBreadcrumbsOptions() {
override setupBreadcrumbsOptions() {
this.subs.add(this.selectedProject$.pipe(
withLatestFrom(this.store.select(selectDefaultNestedModeForFeature))
).subscribe(([selectedProject, defaultNestedModeForFeature]) => {

View File

@@ -38,9 +38,14 @@
(delete)="delete.emit()"
></sm-pipeline-card-menu>
</div>
<div *ngIf="project.last_update; else: noRun" class="last-run">Updated {{project.last_update | timeAgo}}</div>
<span
*ngIf="project.last_update; else: noRun"
class="last-run"
[smTooltip]="project.last_update | date: timeFormatString"
matTooltipPosition="after"
>Updated {{project.last_update | timeAgo}}</span>
<ng-template #noRun>
<div class="last-run"></div>
<span class="last-run"></span>
</ng-template>
</div>
<div class="d-flex justify-content-around w-100">

View File

@@ -1,33 +0,0 @@
@import "variables";
.empty-datasets {
width: 100%;
grid-column: 1/-1;
display: flex;
flex-direction: column;
max-width: map-get($grid-max-widths, xl);
margin: 0 auto;
.title-icon {
color: $blue-600;
text-align: center;
margin-bottom: 24px;
}
.title {
font-size: 20px;
font-weight: 500;
color: $blue-100;
margin-bottom: 6px;
text-align: center;
}
.sub-title {
text-align: center;
margin-bottom: 64px;
.link {
color: $neon-yellow;
}
}
}

View File

@@ -43,7 +43,7 @@
</div>
</div>
<ng-template #emptyState>
<div class="empty-datasets">
<div class="empty-state">
<div class="title-icon"><i class="al-icon al-ico-datasets xxl"></i></div>
<div class="title">NO DATASETS TO SHOW</div>
<div class="sub-title">Run your first dataset to see it displayed here

View File

@@ -1,13 +1,14 @@
@import "variables";
@import "./empty";
@import "empty";
:host {
.sm-card-list-layout{
&.in-empty-state{
.sm-card-list-layout {
&.in-empty-state {
height: 100%;
grid-template-rows: 64px 1fr;
}
}
display: block;
height: 100%;
overflow-y: scroll;
@@ -22,6 +23,5 @@
background-color: $blue-800;
margin-bottom: -12px;
}
@include empty-card-mixin();
}

View File

@@ -20,19 +20,23 @@ import {selectDefaultNestedModeForFeature} from '@common/core/reducers/projects.
})
export class SimpleDatasetsComponent extends PipelinesPageComponent implements OnInit {
public projectCardClicked(project: ProjectsGetAllResponseSingle) {
public override projectCardClicked(project: ProjectsGetAllResponseSingle) {
this.router.navigate(['simple', project.id, 'experiments'], {relativeTo: this.projectId? this.route.parent.parent: this.route});
this.store.dispatch(setSelectedProjectId({projectId: project.id, example: this.isExample(project)}));
}
protected getName() {
protected override getName() {
return EntityTypeEnum.simpleDataset;
}
protected getDeletePopupEntitiesList() {
protected override getDeletePopupEntitiesList() {
return 'version';
}
override noProjectsReRoute() {
return this.router.navigate(['..', 'datasets'], {relativeTo: this.route});
}
createDataset() {
this.dialog.open(DatasetEmptyComponent, {
maxWidth: '95vw',
@@ -40,14 +44,14 @@ export class SimpleDatasetsComponent extends PipelinesPageComponent implements O
});
}
public createExamples() {
public override createExamples() {
this.store.dispatch(showExampleDatasets());
}
ngOnInit() {
override ngOnInit() {
super.ngOnInit();
this.showExamples$ = this.store.select(selectShowDatasetExamples);
}
toggleNestedView(nested: boolean) {
override toggleNestedView(nested: boolean) {
this.store.dispatch(setDefaultNestedModeForFeature({feature: 'datasets', isNested: nested}));
if (nested) {
@@ -57,7 +61,7 @@ export class SimpleDatasetsComponent extends PipelinesPageComponent implements O
}
}
setupBreadcrumbsOptions() {
override setupBreadcrumbsOptions() {
this.subs.add(this.selectedProject$.pipe(
withLatestFrom(this.store.select(selectDefaultNestedModeForFeature))
).subscribe(([selectedProject, defaultNestedModeForFeature]) => {

View File

@@ -3,64 +3,61 @@ import {Task} from '~/business-logic/model/tasks/task';
import {TaskMetric} from '~/business-logic/model/events/taskMetric';
import {EventsDebugImagesResponse} from '~/business-logic/model/events/eventsDebugImagesResponse';
export const DEBUG_IMAGES_PREFIX = 'DEBUG_IMAGES_';
export const DEBUG_IMAGES_PREFIX = '[DEBUG IMAGES] ';
export const resetDebugImages = createAction(DEBUG_IMAGES_PREFIX + 'RESET_DEBUG_IMAGES');
export const resetDebugImages = createAction(DEBUG_IMAGES_PREFIX + 'reset debug images');
export const setDebugImages = createAction(
DEBUG_IMAGES_PREFIX + 'SET_DEBUG_IMAGES',
DEBUG_IMAGES_PREFIX + 'set debug images',
props<{ res: EventsDebugImagesResponse; task: string }>()
);
export const getDebugImagesMetrics = createAction(
DEBUG_IMAGES_PREFIX + 'GET_DEBUG_IMAGES_METRICS',
DEBUG_IMAGES_PREFIX + 'get debug images metrics',
props<{ tasks: string[] }>()
);
export const refreshDebugImagesMetrics = createAction(
DEBUG_IMAGES_PREFIX + 'REFRESH_DEBUG_IMAGES_METRICS',
DEBUG_IMAGES_PREFIX + 'refresh_debug_images_metrics',
props<{ tasks: string[]; autoRefresh?: boolean }>()
);
export const fetchExperiments = createAction(
DEBUG_IMAGES_PREFIX + 'FETCH_EXPERIMENTS',
DEBUG_IMAGES_PREFIX + 'fetch experiments',
props<{ tasks: string[] }>()
);
export const setExperimentsNames = createAction(
DEBUG_IMAGES_PREFIX + 'SET_EXPERIMENTS_NAMES',
DEBUG_IMAGES_PREFIX + 'set experiments names',
props<{ tasks: Partial<Task>[] }>()
);
export const setMetrics = createAction(
DEBUG_IMAGES_PREFIX + 'SET_DEBUG_IMAGES_METRICS',
DEBUG_IMAGES_PREFIX + 'set debug images metrics',
props<{ metrics: any[] }>()
);
export const setSelectedMetric = createAction(
DEBUG_IMAGES_PREFIX + 'SET_DEBUG_IMAGES_SELECTED_METRIC',
DEBUG_IMAGES_PREFIX + 'set debug images selected metric',
props<{ payload: TaskMetric }>()
);
export const refreshMetric = createAction(
DEBUG_IMAGES_PREFIX + 'REFRESH_IMAGES_SELECTED_METRIC',
DEBUG_IMAGES_PREFIX + 'refresh images selected metric',
props<{ payload: TaskMetric; autoRefresh?: boolean }>()
);
export const getNextBatch= createAction(
DEBUG_IMAGES_PREFIX + 'GET_NEXT_DEBUG_IMAGES_BATCH',
DEBUG_IMAGES_PREFIX + 'get next debug images batch',
props<{ payload: TaskMetric }>()
);
export const getPreviousBatch= createAction(
DEBUG_IMAGES_PREFIX + 'GET_PREVIOUS_DEBUG_IMAGES_BATCH',
DEBUG_IMAGES_PREFIX + 'get previous debug images batch',
props<{ payload: TaskMetric }>()
);
export const setTimeIsNow = createAction(
DEBUG_IMAGES_PREFIX + 'SET_TIME_IS_NOW',
DEBUG_IMAGES_PREFIX + 'set time is now',
props<{ task: string; timeIsNow: boolean }>()
);

View File

@@ -1,6 +1,6 @@
import {Injectable} from '@angular/core';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {catchError, mergeMap, map, switchMap, withLatestFrom, filter} from 'rxjs/operators';
import {Actions, concatLatestFrom, createEffect, ofType} from '@ngrx/effects';
import {catchError, mergeMap, map, switchMap, filter} from 'rxjs/operators';
import * as debugActions from './debug-images-actions';
import {activeLoader, deactivateLoader} from '../core/actions/layout.actions';
import {ApiTasksService} from '~/business-logic/api-services/tasks.service';
@@ -8,7 +8,7 @@ import {ApiEventsService} from '~/business-logic/api-services/events.service';
import {requestFailed} from '../core/actions/http.actions';
import {refreshExperiments} from '../experiments/actions/common-experiments-view.actions';
import {Action, Store} from '@ngrx/store';
import {selectDebugImages} from './debug-images-reducer';
import {selectDebugImages, selectSelectedMetricForTask} from './debug-images-reducer';
import {EventsDebugImagesResponse} from '~/business-logic/model/events/eventsDebugImagesResponse';
import {EventsGetTaskMetricsResponse} from '~/business-logic/model/events/eventsGetTaskMetricsResponse';
import {COMPARE_DEBUG_IMAGES_ONLY_FIELDS} from '../experiments-compare/experiments-compare.constants';
@@ -17,8 +17,10 @@ import {setBeginningOfTime} from '@common/shared/debug-sample/debug-sample.actio
export const ALL_IMAGES = '-- All --';
export const removeAllImagesFromPayload = (payload) =>
({...payload, metric: payload.metric === ALL_IMAGES ? null : payload.metric});
export const removeAllImagesFromPayload = (payload, selectedMetric?: string ) => {
const metric = selectedMetric ?? payload.metric
return ({...payload, metric: metric === ALL_IMAGES ? null : metric});
}
// interface Image {
@@ -40,7 +42,7 @@ export class DebugImagesEffects {
constructor(
private actions$: Actions, private apiTasks: ApiTasksService,
private eventsApi: ApiEventsService, private store: Store<any>
private eventsApi: ApiEventsService, private store: Store
) {
}
@@ -53,11 +55,11 @@ export class DebugImagesEffects {
fetchDebugImages$ = createEffect(() => this.actions$.pipe(
ofType(debugActions.setSelectedMetric, debugActions.getNextBatch, debugActions.getPreviousBatch, debugActions.refreshMetric),
withLatestFrom(this.store.select(selectDebugImages)),
mergeMap(([action, debugImages]) =>
this.eventsApi.eventsDebugImages({
concatLatestFrom(() => [this.store.select(selectDebugImages), this.store.select(selectSelectedMetricForTask)]),
mergeMap(([action, debugImages, selectedMetricForTask ]) =>
this.eventsApi.eventsDebugImages({
/* eslint-disable @typescript-eslint/naming-convention */
metrics: [removeAllImagesFromPayload(action.payload)],
metrics: [removeAllImagesFromPayload(action.payload, selectedMetricForTask[action.payload.task])],
iters: 3,
scroll_id: (action.type !== debugActions.setSelectedMetric.type && debugImages?.[action.payload.task]) ?
debugImages[action.payload.task].scroll_id :
@@ -126,9 +128,11 @@ export class DebugImagesEffects {
fetchMetrics$ = createEffect(() => this.actions$.pipe(
ofType(debugActions.getDebugImagesMetrics, debugActions.refreshDebugImagesMetrics),
switchMap((action) => this.store.select(selectActiveWorkspaceReady).pipe(
filter(ready => ready),
map(() => action))),
switchMap((action) => this.store.select(selectActiveWorkspaceReady)
.pipe(
filter(ready => ready),
map(() => action))
),
switchMap((action) => this.eventsApi.eventsGetTaskMetrics({
/* eslint-disable @typescript-eslint/naming-convention */
tasks: action.tasks,
@@ -136,7 +140,11 @@ export class DebugImagesEffects {
/* eslint-enable @typescript-eslint/naming-convention */
})
.pipe(
mergeMap((res: EventsGetTaskMetricsResponse) => [debugActions.setMetrics({metrics: res.metrics}), deactivateLoader(action.type)]),
mergeMap((res: EventsGetTaskMetricsResponse) => [
debugActions.setMetrics({metrics: res.metrics}),
deactivateLoader(debugActions.getDebugImagesMetrics.type),
deactivateLoader(debugActions.refreshDebugImagesMetrics.type),
]),
catchError(error => [requestFailed(error), deactivateLoader(action.type)])
)
)

View File

@@ -1,5 +1,11 @@
import {
fetchExperiments, getDebugImagesMetrics, resetDebugImages, setDebugImages, setExperimentsNames, setMetrics, setTimeIsNow
fetchExperiments,
getDebugImagesMetrics,
resetDebugImages,
setDebugImages,
setExperimentsNames,
setMetrics, setSelectedMetric,
setTimeIsNow
} from './debug-images-actions';
import {Task} from '~/business-logic/model/tasks/task';
import {createFeatureSelector, createReducer, createSelector, on} from '@ngrx/store';
@@ -7,14 +13,14 @@ import {EventsDebugImagesResponse} from '~/business-logic/model/events/eventsDeb
export interface IDebugImagesState {
debugImages: {[taskId: string]: EventsDebugImagesResponse};
debugImages: { [taskId: string]: EventsDebugImagesResponse };
settingsList: Array<IDebugImagesSettings>;
tasks: Array<Partial<Task>>;
optionalMetrics: Array<ITaskOptionalMetrics>;
selectedMetricsForTask: { [taskId: string]: string };
searchTerm: string;
scrollId: any;
noMore: boolean;
selectedMetric: any;
timeIsNow: any;
}
@@ -37,7 +43,7 @@ export const initialState: IDebugImagesState = {
optionalMetrics: [],
scrollId: {},
noMore: true,
selectedMetric: null,
selectedMetricsForTask: {},
timeIsNow: {}
}
;
@@ -47,6 +53,7 @@ export const selectDebugImages = createSelector(debugImages, (state) => state.de
export const selectTaskNames = createSelector(debugImages, (state) => state.tasks);
export const selectNoMore = createSelector(debugImages, (state) => state.noMore);
export const selectOptionalMetrics = createSelector(debugImages, (state) => state.optionalMetrics);
export const selectSelectedMetricForTask = createSelector(debugImages, (state) => state.selectedMetricsForTask);
export const selectTimeIsNow = createSelector(debugImages, (state) => state.timeIsNow);
@@ -61,8 +68,8 @@ export const debugSamplesReducer = createReducer(
on(setDebugImages, (state, action) => ({...state, debugImages: {...state.debugImages, [action.task]: action.res}})),
on(setExperimentsNames, (state, action) => ({...state, tasks: action.tasks})),
on(setMetrics, (state, action) => ({...state, optionalMetrics: action.metrics})),
on(setSelectedMetric, (state, action) => ({...state, selectedMetricsForTask: {...state.selectedMetricsForTask, [action.payload.task]: action.payload.metric}})),
on(getDebugImagesMetrics, state => ({...state, optionalMetrics: initialState.optionalMetrics, debugImages: initialState.debugImages})),
on(setTimeIsNow, (state, action) => ({...state, timeIsNow: {...state.timeIsNow, [action.task]: action.timeIsNow}})),
on(fetchExperiments, () => ({...initialState})),
// eslint-disable-next-line @typescript-eslint/naming-convention

View File

@@ -0,0 +1,26 @@
export interface DebugSampleEvent {
timestamp: number;
type?: string;
task?: string;
iter?: number;
metric?: string;
variant?: string;
key?: string;
url?: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
'@timestamp'?: string;
worker?: string;
variantAndMetric?: string;
}
export interface Iteration {
events: DebugSampleEvent[];
iter: number;
}
export interface DebugSamples {
metrics: string[];
metric: string;
scrollId: string;
data: Iteration[];
}

View File

@@ -1,7 +1,7 @@
import {Component, Input, Output} from '@angular/core';
import {EventEmitter} from '@angular/core';
import {Iteration, Event} from '@common/debug-images/debug-images.component';
import {ThemeEnum} from '@common/constants';
import {DebugSampleEvent, Iteration} from '@common/debug-images/debug-images-types';
@Component({
selector: 'sm-debug-images-view',
@@ -23,9 +23,9 @@ export class DebugImagesViewComponent {
@Output() imageClicked = new EventEmitter();
@Output() refreshClicked = new EventEmitter();
@Output() createEmbedCode = new EventEmitter<{metrics?: string[]; variants?: string[]; domRect: DOMRect}>();
@Output() urlError = new EventEmitter();
@Output() urlError = new EventEmitter<{ frame: DebugSampleEvent; experimentId: string }>();
public imageUrlError(data: { frame: Event; experimentId: string }) {
public imageUrlError(data: { frame: DebugSampleEvent; experimentId: string }) {
this.urlError.emit(data);
}
}

View File

@@ -1,6 +1,6 @@
<div class="p-3 images-container">
<div class="single-debug-images-container"
*ngFor="let experimentId of experimentIds?.slice(0,LIMITED_VIEW_LIMIT); trackBy: trackExperiment;
*ngFor="let experimentId of experimentIds | slice : 0 : LIMITED_VIEW_LIMIT; trackBy: trackExperiment;
let first = first; let last = last"
[class.separator]="experimentIds?.length > 1">
<header *ngIf="experimentIds?.length > 1">
@@ -9,15 +9,14 @@
[experiment]="experiments | itemById: experimentId"
[tags]="(experiments | itemById: experimentId)?.tags"
(copyIdClicked)="copyIdToClipboard()"
>
</sm-experiment-compare-general-data>
></sm-experiment-compare-general-data>
</header>
<div class="navigator-container" [class.active]="bindNavigationMode" [class.first]="first"
[class.last]="last" [style.display]="isDarkTheme ? 'none' : 'flex'">
<div class="connector-icon-container pointer" smTooltip="Sync browsing" [matTooltipShowDelay]="500"
<div data-id="syncBrowsing" class="connector-icon-container pointer" smTooltip="Sync browsing" [matTooltipShowDelay]="500"
[class.active]="bindNavigationMode" [class.hidden]="last" (click)="toggleConnectNavigation()">
<i class="al-icon" [class.al-ico-connect]="bindNavigationMode"
[class.al-ico-disconnect]="!bindNavigationMode"></i></div>
<i class="al-icon" [class.al-ico-connect]="bindNavigationMode" [class.al-ico-disconnect]="!bindNavigationMode"></i>
</div>
<div class="metric-bar" [class.minimized]="minimized"
*ngIf="!thereAreNoMetrics(experimentId) && !disableStatusRefreshFilter" data-id="metricBar">
<label data-id="metricText">Metric:</label>
@@ -34,7 +33,7 @@
</mat-form-field>
<label data-id="IterationText">Iterations:</label>
<div [ngClass]="{'disabled': (beginningOfTime$| async)[experimentId]}"
<div [ngClass]="{'disabled': (beginningOfTime$| async)?.[experimentId]}"
(click)="nextBatch({task: experimentId, metric: metricSelect.value})"
class="al-icon al-ico-next-batch al-color blue-300"
smTooltip="Older images" data-id="OlderImages"></div>
@@ -43,23 +42,23 @@
<div class="al-icon al-ico-between al-color light-blue-grey"></div>
<b>{{debugImages?.[experimentId]?.data?.[0].iter}}</b>
<div [ngClass]="{'disabled': (timeIsNow$| async)[experimentId]}"
<div [ngClass]="{'disabled': (timeIsNow$| async)?.[experimentId]}"
(click)="(!timeIsNow[experimentId]) && previousBatch({task: experimentId, metric: metricSelect.value})"
class="al-icon al-ico-prev-batch al-color blue-300"
smTooltip="Newer images" data-id="NewerImages"></div>
<div [ngClass]="{'disabled': (timeIsNow$| async)[experimentId] && !allowAutorefresh }"
<div [ngClass]="{'disabled': (timeIsNow$| async)?.[experimentId] && !allowAutorefresh }"
(click)="(!(timeIsNow[experimentId] && !allowAutorefresh)) && backToNow({task: experimentId, metric: metricSelect.value})"
class="al-icon al-ico-back-to-top al-color blue-300"
smTooltip="Newest samples" data-id="NewestSamples"></div>
</div>
</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="400" height="200" viewBox="0 0 300 150">
<svg class="mb-3" xmlns="http://www.w3.org/2000/svg" width="200" height="100" 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>
<h4>NO DEBUG SAMPLES</h4>
</div>
<sm-debug-images-view
*ngIf="debugImages?.[experimentId]?.data"
@@ -70,10 +69,9 @@
[isDarkTheme]="isDarkTheme"
[isDatasetVersionPreview]="disableStatusRefreshFilter"
(imageClicked)="imageClicked($event, experimentId)"
(urlError)="urlError()"
(urlError)="urlError($event)"
(createEmbedCode)="createEmbedCode($event, experimentId)"
>
</sm-debug-images-view>
></sm-debug-images-view>
</div>
<div *ngIf="experimentIds?.length>LIMITED_VIEW_LIMIT"
class="limit-message-container">

View File

@@ -110,9 +110,9 @@ sm-debug-images-view {
left: 50%;
transform: translateY(-50%) translateX(-50%);
display: block;
color: $cloudy-blue-two;
font-weight: 500;
color: $blue-100;
line-height: 1.2;
h4 {color: $blue-100;}
&.dark {
color: $blue-250;

View File

@@ -1,18 +1,6 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges
} from '@angular/core';
import {ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges} from '@angular/core';
import {combineLatest, Observable, Subscription} from 'rxjs';
import {Store} from '@ngrx/store';
import {AdminService} from '~/shared/services/admin.service';
import {selectS3BucketCredentials} from '../core/reducers/common-auth-reducer';
import {MatDialog} from '@angular/material/dialog';
import * as debugActions from './debug-images-actions';
@@ -22,51 +10,30 @@ import {
selectDebugImages,
selectNoMore,
selectOptionalMetrics,
selectSelectedMetricForTask,
selectTaskNames,
selectTimeIsNow
} from './debug-images-reducer';
import {selectRouterParams} from '../core/reducers/router-reducer';
import {distinctUntilChanged, filter, map, withLatestFrom} from 'rxjs/operators';
import {Task} from '~/business-logic/model/tasks/task';
import {ActivatedRoute} from '@angular/router';
import {ActivatedRoute, Params} from '@angular/router';
import {TaskStatusEnum} from '~/business-logic/model/tasks/taskStatusEnum';
import {ImageViewerComponent} from '../shared/debug-sample/image-viewer/image-viewer.component';
import {selectSelectedExperiment} from '~/features/experiments/reducers';
import {TaskMetric} from '~/business-logic/model/events/taskMetric';
import {isEqual} from 'lodash-es';
import {ALL_IMAGES} from './debug-images-effects';
import {getSignedUrl} from '../core/actions/common-auth.actions';
import {getSignedUrl, removeSignedUrl} from '../core/actions/common-auth.actions';
import {addMessage} from '../core/actions/layout.actions';
import {RefreshService} from '@common/core/services/refresh.service';
import {selectBeginningOfTime} from '@common/shared/debug-sample/debug-sample.reducer';
import {LIMITED_VIEW_LIMIT} from '@common/experiments-compare/experiments-compare.constants';
import {ReportCodeEmbedService} from '~/shared/services/report-code-embed.service';
export interface Event {
timestamp: number;
type?: string;
task?: string;
iter?: number;
metric?: string;
variant?: string;
key?: string;
url?: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
'@timestamp'?: string;
worker?: string;
}
export interface Iteration {
events: Event[];
iter: number;
}
interface DebugSamples {
metrics: string[];
metric: string;
scrollId: string;
data: Iteration[];
}
import {EventsDebugImagesResponse} from '~/business-logic/model/events/eventsDebugImagesResponse';
import {DebugSampleEvent, DebugSamples} from '@common/debug-images/debug-images-types';
import {concatLatestFrom} from '@ngrx/effects';
import {isGoogleCloudUrl} from '@common/settings/admin/base-admin-utils';
@Component({
selector: 'sm-debug-images',
@@ -87,11 +54,10 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
private refreshingSubscription: Subscription;
private optionalMetricsSubscription: Subscription;
public debugImages$: Observable<any>;
private routerParams$: Observable<any>;
private routerParams$: Observable<Params>;
private tasks$: Observable<Partial<Task>[]>;
public timeIsNow$: Observable<any>;
public beginningOfTime$: Observable<any>;
public timeIsNow$: Observable<boolean>;
public beginningOfTime$: Observable<boolean>;
public mergeIterations: boolean;
public debugImages: { [experimentId: string]: DebugSamples } = null;
@@ -103,9 +69,9 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
public optionalMetrics$: Observable<ITaskOptionalMetrics[]>;
public optionalMetrics: { [experimentId: string]: string };
public selectedMetrics: { [taskId: string]: string } = {};
public beginningOfTime: any;
public beginningOfTime: boolean;
private beginningOfTimeSubscription: Subscription;
public timeIsNow: any;
public timeIsNow: boolean;
private timeIsNowSubscription: Subscription;
minimized: boolean;
readonly allImages = ALL_IMAGES;
@@ -118,7 +84,6 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
constructor(
private store: Store,
private adminService: AdminService,
private dialog: MatDialog,
private changeDetection: ChangeDetectorRef,
private activeRoute: ActivatedRoute,
@@ -135,10 +100,11 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
this.debugImagesSubscription = combineLatest([
store.select(selectS3BucketCredentials),
store.select(selectDebugImages)
store.select(selectDebugImages),
store.select(selectSelectedMetricForTask)
])
.pipe(
map(([, debugImages]) => !debugImages ? {} : Object.entries(debugImages).reduce(((acc, val: any) => {
map(([, debugImages, metricForTask]) => !debugImages ? {} : Object.entries(debugImages).reduce(((acc, val: [string, EventsDebugImagesResponse]) => {
const id = val[0];
const iterations = val[1].metrics.find(m => m.task === id).iterations;
if (iterations?.length === 0) {
@@ -152,12 +118,12 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
return {
...event,
url: event.url,
variantAndMetric: this.selectedMetric === ALL_IMAGES ? `${event.metric}/${event.variant}` : ''
variantAndMetric: (this.selectedMetric === ALL_IMAGES || metricForTask[id] === ALL_IMAGES ) ? `${event.metric}/${event.variant}` : ''
};
})
}))
};
acc[id].metrics = val[1].metrics.map(metric => metric.metric || metric.iterations[0].events[0].metric);
acc[id].metrics = val[1].metrics.map((metric: any) => metric.metric || metric.iterations[0].events[0].metric);
acc[id].metric = acc[id].metrics[0];
acc[id].scrollId = val[1].scroll_id;
return acc;
@@ -205,6 +171,7 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
if (multipleExperiments) {
this.routerParamsSubscription = this.routerParams$
.subscribe(params => {
this.selectedMetric = null;
this.experimentIds = (params.ids ? params.ids.split(',') : params.experimentId.split(','));
this.store.dispatch(getDebugImagesMetrics({tasks: this.experimentIds.slice(0, LIMITED_VIEW_LIMIT)}));
this.store.dispatch(fetchExperiments({tasks: this.experimentIds.slice(0, LIMITED_VIEW_LIMIT)}));
@@ -234,10 +201,13 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
this.selectedExperimentSubscription = this.store.select(selectSelectedExperiment)
.pipe(
filter(experiment => !!experiment && !this.disableStatusRefreshFilter),
distinctUntilChanged((previous, current) => previous?.id === current?.id)
).subscribe(experiment => {
distinctUntilChanged((previous, current) => previous?.id === current?.id),
withLatestFrom(this.store.select(selectSelectedMetricForTask))
).subscribe(([experiment, metricForTask]) => {
this.selectedMetric = null;
this.experimentNames = {[experiment.id]: experiment.name};
this.experimentIds = [experiment.id];
this.selectedMetrics[experiment.id] = metricForTask[experiment.id] ;
this.store.dispatch(getDebugImagesMetrics({tasks: this.experimentIds}));
});
}
@@ -245,19 +215,16 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
this.refreshingSubscription = this.refresh.tick
.pipe(
filter(auto =>
auto !== null && (!this.disableStatusRefreshFilter || this.selected.status === TaskStatusEnum.InProgress)
),
withLatestFrom(
this.store.select(selectTimeIsNow),
)
filter(auto => auto !== null && !this.disableStatusRefreshFilter),
concatLatestFrom(() => this.store.select(selectTimeIsNow))
)
.subscribe(([auto, timeIsNow]) => {
if (multipleExperiments) {
this.store.dispatch(debugActions.refreshDebugImagesMetrics({tasks: this.experimentIds, autoRefresh: auto}));
}
this.store.dispatch(getDebugImagesMetrics({tasks: this.experimentIds}));
this.experimentIds.forEach(experimentId => {
if (experimentId && timeIsNow?.[experimentId] && this.debugImages[experimentId] && this.elRef.nativeElement.scrollTop < 40) {
if (experimentId && !(timeIsNow?.[experimentId] === false) && this.elRef.nativeElement.scrollTop < 40) {
this.store.dispatch(debugActions.refreshMetric({
payload: {
task: experimentId,
@@ -269,19 +236,22 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
});
});
this.optionalMetricsSubscription = this.optionalMetrics$.subscribe(optionalMetrics => {
this.optionalMetricsSubscription = this.optionalMetrics$.pipe(concatLatestFrom(()=> [this.store.select(selectSelectedMetricForTask)]))
.subscribe(([optionalMetrics, selectedMetricForTask ]) => {
const optionalMetricsDic = {};
optionalMetrics.forEach(experimentMetrics => optionalMetricsDic[experimentMetrics.task] = experimentMetrics.metrics);
if ((!isEqual(this.optionalMetrics, optionalMetricsDic)) && optionalMetrics.length > 0) {
this.optionalMetrics = optionalMetricsDic;
optionalMetrics.forEach(optionalMetric => {
optionalMetric.metrics[0] && this.store.dispatch(debugActions.setSelectedMetric({
payload: {
task: optionalMetric.task,
metric: optionalMetric.metrics[0]
}
}));
});
if (!this.selectedMetric || !optionalMetrics?.[0].metrics.includes(this.selectedMetric)) {
optionalMetrics.forEach(optionalMetric => {
optionalMetric.metrics[0] && this.store.dispatch(debugActions.setSelectedMetric({
payload: {
task: optionalMetric.task,
metric: selectedMetricForTask[optionalMetric.task] ?? optionalMetric.metrics[0]
}
}));
});
}
this.changeDetection.detectChanges();
}
});
@@ -300,7 +270,12 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
this.store.dispatch(resetDebugImages());
}
public urlError(/*{frame}*/) {
public urlError({frame}: { frame: DebugSampleEvent; experimentId: string }) {
const url = frame.url;
if (isGoogleCloudUrl(url)) {
this.store.dispatch(removeSignedUrl({url}));
this.store.dispatch(getSignedUrl({url}));
}
// this.adminService.checkImgUrl(frame.oldSrc || frame.src);
}
@@ -331,7 +306,7 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
return experimentID;
}
selectMetric(change: any, task) {
selectMetric(change: {value: string}, taskId) {
this.selectedMetric = change.value;
if (this.bindNavigationMode) {
this.experimentIds.forEach(experimentId => {
@@ -339,8 +314,8 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
this.store.dispatch(debugActions.setSelectedMetric({payload: {task: experimentId, metric: change.value}}));
});
} else {
this.selectedMetrics[task] = change.value;
this.store.dispatch(debugActions.setSelectedMetric({payload: {task, metric: change.value}}));
this.selectedMetrics[taskId] = change.value;
this.store.dispatch(debugActions.setSelectedMetric({payload: {task: taskId, metric: change.value}}));
}
}
@@ -394,16 +369,16 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
}
thereAreNoMetrics(experiment) {
return !(this.optionalMetrics && this.optionalMetrics[experiment] && this.optionalMetrics[experiment].length > 0);
thereAreNoMetrics(experiment: string) {
return !(this.optionalMetrics?.[experiment]?.length > 0);
}
thereAreNoDebugImages(experiment) {
return !(this.debugImages && this.debugImages[experiment] && this.debugImages[experiment].data?.length > 0);
thereAreNoDebugImages(experiment: string) {
return !(this.debugImages?.[experiment]?.data?.length > 0);
}
shouldShowNoImagesForExperiment(experiment: string) {
return (this.thereAreNoMetrics(experiment) && this.optionalMetrics && this.optionalMetrics[experiment]) || (this.thereAreNoDebugImages(experiment) && this.debugImages && this.debugImages[experiment]);
return this.thereAreNoMetrics(experiment) && this.thereAreNoDebugImages(experiment);
}
copyIdToClipboard() {

View File

@@ -1,10 +1,10 @@
import {createAction, props} from '@ngrx/store';
import {Task} from '~/business-logic/model/tasks/task';
import {Params} from '@angular/router';
import {ISmCol} from '../../shared/ui-components/data/table/table.consts';
import {SortMeta} from 'primeng/api';
import {TableFilter} from '../../shared/utils/tableParamEncode';
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
import {ITableExperiment} from '@common/experiments/shared/common-experiment-model.model';
export const EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ = 'EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_';
@@ -14,6 +14,8 @@ export const SET_SELECT_EXPERIMENTS_FOR_COMPARE = EXPERIMENTS_COMPARE_SELECT_EXP
export const RESET_SELECT_EXPERIMENT_FOR_COMPARE = EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + 'RESET_SELECT_EXPERIMENT_FOR_COMPARE';
export const TOGGLE_SHOW_SACLARS_OPTIONS = EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + 'TOGGLE_SHOW_SACLARS_OPTIONS';
export const SET_HIDE_IDENTICAL_ROWS = EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + 'SET_HIDE_IDENTICAL_ROWS';
export const SET_SHOW_ROW_EXTREMES = EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + 'SET_SHOW_ROW_EXTREMES';
export const SET_SHOW_GLOBAL_LEGEND = EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + 'SET_SHOW_GLOBAL_LEGEND';
export const SET_REFRESHING = EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + 'SET_REFRESHING';
export const SET_EXPERIMENTS_UPDATE_TIME = EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + 'SET_EXPERIMENTS_UPDATE_TIME';
export const REFRESH_IF_NEEDED = EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + 'REFRESH_IF_NEEDED';
@@ -22,10 +24,12 @@ export const SET_NAVIGATION_PREFERENCES = EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_
export const setHideIdenticalFields = createAction(SET_HIDE_IDENTICAL_ROWS, props<{payload: boolean}>());
export const setShowRowExtremes = createAction(SET_SHOW_ROW_EXTREMES, props<{payload: boolean}>());
export const setShowGlobalLegend = createAction(SET_SHOW_GLOBAL_LEGEND);
export const setExperimentsUpdateTime = createAction(SET_EXPERIMENTS_UPDATE_TIME, props<{ payload: {[key: string]: Date}}>());
export const refreshIfNeeded = createAction(REFRESH_IF_NEEDED, props<{ payload: boolean; autoRefresh?: boolean; entityType: string }>());
export const toggleShowScalarOptions = createAction(TOGGLE_SHOW_SACLARS_OPTIONS);
export const setSearchExperimentsForCompareResults = createAction(SET_SELECT_EXPERIMENTS_FOR_COMPARE, props<{ payload: Array<Task> }>());
export const setSearchExperimentsForCompareResults = createAction(SET_SELECT_EXPERIMENTS_FOR_COMPARE, props<{ payload: ITableExperiment[] }>());
export const setShowSearchExperimentsForCompare = createAction(SET_SHOW_SEARCH_EXPERIMENTS_FOR_COMPARE, props<{ payload: boolean }>());
export const resetSelectCompareHeader = createAction(RESET_SELECT_EXPERIMENT_FOR_COMPARE, props<{fullReset?: boolean}>());
export const getSelectedExperimentsForCompareAddDialog = createAction(GET_SELECTED_EXPERIMENTS_FOR_COMPARE, props<{tasksIds?: string[]}>());
@@ -52,8 +56,12 @@ export const compareAddDialogSetTableSort = createAction(
props<{ orders: SortMeta[]; projectId: string; colIds: string[] }>()
);
export const refetchExperimentRequested = createAction(REFETCH_EXPERIMENT_REQUESTED, props<{ autoRefresh: boolean; entity: EntityTypeEnum }>());
export const setNavigationPreferences = createAction(SET_NAVIGATION_PREFERENCES, props<{ navigationPreferences: Params }>());
export const setAddTableViewArchived = createAction(
EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + '[show archived in add table]',
props<{show: boolean}>()
);
export const setExportTable = createAction(
EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + '[set export table]',
props<{export: boolean}>()
);

View File

@@ -3,18 +3,22 @@ import {ScalarKeyEnum} from '~/business-logic/model/events/scalarKeyEnum';
import {EventsGetMultiTaskPlotsResponse} from '~/business-logic/model/events/eventsGetMultiTaskPlotsResponse';
import {ExperimentCompareSettings} from '@common/experiments-compare/reducers/experiments-compare-charts.reducer';
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
import {EventsGetTaskSingleValueMetricsResponse} from '~/business-logic/model/events/eventsGetTaskSingleValueMetricsResponse';
import {EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_} from '@common/experiments-compare/actions/compare-header.actions';
import {ChartHoverModeEnum} from "@common/experiments/shared/common-experiments.const";
export const EXPERIMENTS_COMPARE_METRICS_CHARTS_ = 'EXPERIMENTS_COMPARE_METRICS_CHARTS_';
// COMMANDS:
export const SET_EXPERIMENT_PLOTS = EXPERIMENTS_COMPARE_METRICS_CHARTS_ + 'SET_EXPERIMENT_PLOTS';
// EVENTS:
export const getMultiScalarCharts = createAction(
EXPERIMENTS_COMPARE_METRICS_CHARTS_ + 'GET_MULTI_SCALAR_CHARTS',
props<{ taskIds: string[]; entity: EntityTypeEnum; autoRefresh?: boolean; xAxisType: ScalarKeyEnum }>()
);
export const getMultiSinleScalars = createAction(
EXPERIMENTS_COMPARE_METRICS_CHARTS_ + 'GET_MULTI_SINGLE_SCALAR_CHARTS',
props<{ taskIds: string[]; entity: EntityTypeEnum; autoRefresh?: boolean; cached?: boolean }>()
);
@@ -23,6 +27,11 @@ export const getMultiPlotCharts = createAction(
props<{ taskIds: Array<string>; entity: EntityTypeEnum; autoRefresh?: boolean }>()
);
export const setExperimentMultiScalarSingleValue = createAction(
EXPERIMENTS_COMPARE_METRICS_CHARTS_ + 'SET_MULTI_SINGLE_SCALAR_CHARTS',
props<EventsGetTaskSingleValueMetricsResponse>()
);
export const setSelectedExperiments = createAction(
EXPERIMENTS_COMPARE_METRICS_CHARTS_ + 'SET_SELECTED_EXPERIMENTS',
props<{selectedExperiments: string[]}>()
@@ -54,3 +63,17 @@ export const setExperimentMetricsSearchTerm = createAction(
);
export const resetExperimentMetrics = createAction(EXPERIMENTS_COMPARE_METRICS_CHARTS_ + 'RESET_EXPERIMENT_METRICS');
export const getGlobalLegendData = createAction(
EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + '[get global legend data]',
props<{ids: string[], entity: EntityTypeEnum}>()
);
export const setGlobalLegendData = createAction(
EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + '[set global legend data]',
props<{data: {name: string, tags: string[], systemTags: string[], id: string, project: {id: string}}[]}>()
);
export const setScalarsHoverMode = createAction(
EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_ + 'SET SCALARS HOVER MODE',
props<{ hoverMode: ChartHoverModeEnum }>()
);

View File

@@ -1,7 +1,16 @@
import {createAction, props} from '@ngrx/store';
import {createActionGroup, props, emptyProps} from '@ngrx/store';
import {ExperimentParams} from '../shared/experiments-compare-details.model';
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
export const resetState = createAction('[experiment compare params] RESET_STATE');
export const setExperiments = createAction('[experiment compare params] SET_EXPERIMENTS', props<{experiments: ExperimentParams[]}>());
export const experimentListUpdated = createAction('[experiment compare params] EXPERIMENT_LIST_UPDATED', props<{ids: string[]; entity: EntityTypeEnum}>());
export const paramsActions = createActionGroup({
source: 'experiment compare params',
events: {
'reset state': emptyProps(),
'set experiments': props<{experiments: ExperimentParams[]}>(),
'experiment list updated': props<{ids: string[]; entity: EntityTypeEnum}>(),
'set view': props<{primary: string; secondary: string}>()
}
})
// export const resetState = createAction('[experiment compare params] RESET_STATE');
// export const setExperiments = createAction('[experiment compare params] SET_EXPERIMENTS', props<{experiments: ExperimentParams[]}>());
// export const experimentListUpdated = createAction('[experiment compare params] EXPERIMENT_LIST_UPDATED', props<{ids: string[]; entity: EntityTypeEnum}>());

View File

@@ -0,0 +1,15 @@
import {CanActivateFn, createUrlTreeFromSnapshot} from '@angular/router';
import {inject} from '@angular/core';
import {Store} from '@ngrx/store';
import {selectViewMode} from '@common/experiments-compare/reducers';
import {map} from 'rxjs/operators';
export const compareNavigationGuard: CanActivateFn = route => {
const store = inject(Store);
const currentPage = route?.url?.[0]?.path;
return store.select(selectViewMode(currentPage))
.pipe(
map(mode => createUrlTreeFromSnapshot(route, [mode]))
);
};

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