release v1.15 (#70)

Co-authored-by: shallegro <shay@allego.ai>
This commit is contained in:
shyallegro
2024-03-24 10:55:14 +02:00
committed by GitHub
parent d9eed64770
commit d4f9424589
319 changed files with 10511 additions and 7596 deletions

View File

@@ -13,7 +13,7 @@
### 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.
* in [proxy.config.mjs](proxy.config.mjs) 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)

View File

@@ -10,14 +10,16 @@
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular-devkit/build-angular:browser-esbuild",
"options": {
"preserveSymlinks": true,
"outputPath": "build",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"polyfills": [
"zone.js"
],
"stylePreprocessorOptions": {
"includePaths": [
"src/app/webapp-common/shared/ui-components/styles/"
@@ -57,9 +59,9 @@
"@aws-crypto/crc32",
"@aws-crypto/sha1-browser",
"@aws-crypto/crc32c",
"bowser",
"filesize/lib/filesize.es6",
"hex-rgb",
"britecharts",
"localforage",
"dom-to-image",
"ace-builds",
@@ -67,9 +69,9 @@
"taira",
"base-64",
"export-to-csv",
"dompurify"
"dompurify",
"hammerjs"
],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
@@ -89,7 +91,6 @@
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
@@ -110,7 +111,6 @@
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
@@ -129,27 +129,27 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "trains-webapp:build",
"proxyConfig": "proxy.config.js",
"proxyConfig": "./proxy.config.mjs",
"liveReload": false,
"port": 4300
"port": 4300,
"buildTarget": "trains-webapp:build"
},
"configurations": {
"appdev": {
"browserTarget": "trains-webapp:build:appdev"
"buildTarget": "trains-webapp:build:appdev"
},
"staging": {
"browserTarget": "trains-webapp:build:demo"
"buildTarget": "trains-webapp:build:demo"
},
"production": {
"browserTarget": "trains-webapp:build:production"
"buildTarget": "trains-webapp:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "trains-webapp:build"
"buildTarget": "trains-webapp:build"
}
},
"test": {
@@ -157,7 +157,10 @@
"options": {
"main": "src/test.ts",
"karmaConfig": "./karma.conf.js",
"polyfills": "src/polyfills.ts",
"polyfills": [
"zone.js",
"zone.js/testing"
],
"stylePreprocessorOptions": {
"includePaths": [
"src/app/webapp-common/shared/ui-components/styles/"
@@ -198,14 +201,16 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular-devkit/build-angular:browser-esbuild",
"options": {
"preserveSymlinks": true,
"outputPath": "dist/report-widgets",
"baseHref": "widgets",
"index": "src/app/webapp-common/clearml-applications/report-widgets/src/index.html",
"main": "src/app/webapp-common/clearml-applications/report-widgets/src/main.ts",
"polyfills": "src/app/webapp-common/clearml-applications/report-widgets/src/polyfills.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "src/app/webapp-common/clearml-applications/report-widgets/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
@@ -226,7 +231,20 @@
"inject": false
}
],
"scripts": []
"scripts": [],
"allowedCommonJsDependencies": [
"string-to-color",
"dom-to-image",
"dompurify",
"url",
"taira",
"@aws-crypto/crc32",
"@aws-crypto/crc32c",
"@aws-crypto/sha1-browser",
"@aws-crypto/sha256-browser",
"fast-xml-parser",
"bowser"
]
},
"configurations": {
"production": {
@@ -253,7 +271,6 @@
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
@@ -265,13 +282,13 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "report-widgets:build:production",
"headers": {
"Content-Security-Policy": "frame-ancestors *"
}
},
"buildTarget": "report-widgets:build:production"
},
"development": {
"browserTarget": "report-widgets:build:development"
"buildTarget": "report-widgets:build:development"
}
},
"defaultConfiguration": "development"
@@ -279,14 +296,17 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "report-widgets:build"
"buildTarget": "report-widgets:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/app/webapp-common/clearml-applications/report-widgets/src/test.ts",
"polyfills": "src/app/webapp-common/clearml-applications/report-widgets/src/polyfills.ts",
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "src/app/webapp-common/clearml-applications/report-widgets/tsconfig.spec.json",
"karmaConfig": "src/app/webapp-common/clearml-applications/report-widgets/karma.conf.js",
"inlineStyleLanguage": "scss",

8323
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{
"name": "clearml-webapp",
"version": "1.14.0",
"version": "1.15.0",
"license": "",
"scripts": {
"ng": "ng",
"start": "npx ng serve",
"start-widgets": "npx ng serve --port 4201 --project report-widgets --proxy-config proxy.config.js --live-reload false",
"hmr": "npx ng serve --proxy-config proxy.config.js --hmr --port 4300",
"build": "npx ng build --configuration production --source-map --vendor-chunk",
"start-widgets": "npx ng serve --port 4201 --project report-widgets --proxy-config proxy.config.mjs --live-reload false",
"hmr": "npx ng serve --live-reload true",
"build": "npx ng build --configuration production",
"build-dev": "node ./node_modules/.bin/ng build --extract-css=false",
"build-widgets": "npx ng build --project report-widgets --configuration production",
"fetch": "./scripts/get-remote-build.sh",
@@ -19,100 +19,102 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^16.2.12",
"@angular/cdk": "^16.2.11",
"@angular/common": "^16.2.12",
"@angular/compiler": "^16.2.12",
"@angular/core": "^16.2.12",
"@angular/forms": "^16.2.12",
"@angular/material": "^16.2.11",
"@angular/platform-browser": "^16.2.12",
"@angular/platform-browser-dynamic": "^16.2.12",
"@angular/platform-server": "^16.2.12",
"@angular/router": "^16.2.12",
"@angular/service-worker": "^16.2.12",
"@angular/youtube-player": "^16.2.11",
"@aws-sdk/client-s3": "^3.441.0",
"@aws-sdk/s3-request-presigner": "^3.441.0",
"@angular/animations": "^17.1.1",
"@angular/cdk": "^17.1.1",
"@angular/common": "^17.1.1",
"@angular/compiler": "^17.1.1",
"@angular/core": "^17.1.1",
"@angular/forms": "^17.1.1",
"@angular/material": "^17.1.1",
"@angular/platform-browser": "^17.1.1",
"@angular/platform-browser-dynamic": "^17.1.1",
"@angular/platform-server": "^17.1.1",
"@angular/router": "^17.1.1",
"@angular/service-worker": "^17.1.1",
"@angular/youtube-player": "^17.1.1",
"@aws-sdk/client-s3": "^3.499.0",
"@aws-sdk/s3-request-presigner": "^3.499.0",
"@ctrl/ngx-github-buttons": "^9.0.0",
"@ctrl/tinycolor": "^4.0.2",
"@ctrl/tinycolor": "^4.0.3",
"@ngneat/dag": "^2.0.0",
"@ngrx/effects": "^16.3.0",
"@ngrx/entity": "^16.3.0",
"@ngrx/router-store": "^16.3.0",
"@ngrx/store": "^16.3.0",
"ace-builds": "^1.31.1",
"angular-google-tag-manager": "^1.8.0",
"@ngrx/effects": "^17.1.0",
"@ngrx/entity": "^17.1.0",
"@ngrx/router-store": "^17.1.0",
"@ngrx/store": "^17.1.0",
"ace-builds": "^1.32.3",
"angular-google-tag-manager": "^1.9.0",
"angular-resizable-element": "^7.0.2",
"angular-split": "^16.2.1",
"angular-split": "^17.1.1",
"ansi-to-html": "^0.7.2",
"bootstrap": "^5.3.2",
"chart.js": "^4.4.0",
"chart.js": "^4.4.1",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-annotation": "^3.0.1",
"chartjs-plugin-zoom": "^2.0.1",
"curved-arrows": "^0.1.0",
"d3-selection": "^3.0.0",
"date-fns": "^2.30.0",
"date-fns": "^3.3.1",
"diff": "^5.1.0",
"dom-to-image": "^2.6.0",
"dompurify": "^3.0.6",
"export-to-csv": "^1.2.1",
"export-to-csv": "^1.2.2",
"filesize": "^10.1.0",
"dompurify": "^3.0.8",
"has-ansi": "^5.0.1",
"hocon-parser": "^1.0.1",
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"lucene": "^2.1.1",
"marked": "^7.0.5",
"ng2-charts": "^5.0.3",
"marked": "^11.1.1",
"ng2-charts": "^5.0.4",
"ngx-clipboard": "^16.0.0",
"ngx-color-picker": "^15.0.0",
"ngx-device-detector": "^6.0.2",
"ngx-color-picker": "^16.0.0",
"ngx-device-detector": "^7.0.0",
"ngx-markdown-editor": "^5.3.4",
"ngx-print": "^1.3.1",
"ngx-print": "^1.5.1",
"ngx-window-token": "^7.0.0",
"object-hash": "^3.0.0",
"primeicons": "^6.0.1",
"primeng": "^16.7.1",
"primeng": "^17.4.0",
"rxjs": "^7.8.1",
"string-to-color": "^2.2.2",
"taira": "^3.2.2",
"tslib": "^2.6.2",
"url": "^0.11.3",
"uuid": "^9.0.1",
"zone.js": "~0.14.2"
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.2.9",
"@angular-devkit/core": "^16.2.9",
"@angular-devkit/schematics": "^16.2.9",
"@angular-devkit/schematics-cli": "^16.2.9",
"@angular-eslint/eslint-plugin": "16.2.0",
"@angular-eslint/eslint-plugin-template": "16.2.0",
"@angular-eslint/template-parser": "16.2.0",
"@angular/cli": "^16.2.8",
"@angular/compiler-cli": "^16.2.11",
"@angular/language-service": "^16.2.11",
"@fortawesome/fontawesome-free": "^6.4.2",
"@ngrx/eslint-plugin": "^16.3.0",
"@ngrx/schematics": "^16.3.0",
"@ngrx/store-devtools": "^16.3.0",
"@types/d3-selection": "^3.0.8",
"@types/dom-to-image": "^2.6.6",
"@types/has-ansi": "^5.0.1",
"@types/jasmine": "^5.1.1",
"@types/lodash-es": "^4.17.10",
"@types/node": "^18.18.8",
"@types/plotly.js": "^2.12.29",
"@types/tinycolor2": "^1.4.5",
"@types/uuid": "^9.0.6",
"@typescript-eslint/eslint-plugin": "6.9.1",
"@typescript-eslint/parser": "6.9.1",
"eslint": "^8.53.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-jsdoc": "^46.8.2",
"@angular-devkit/build-angular": "^17.1.1",
"@angular-devkit/core": "^17.1.1",
"@angular-devkit/schematics": "^17.1.1",
"@angular-devkit/schematics-cli": "^17.1.1",
"@angular-eslint/builder": "17.2.1",
"@angular-eslint/eslint-plugin": "17.2.1",
"@angular-eslint/eslint-plugin-template": "17.2.1",
"@angular-eslint/schematics": "17.2.1",
"@angular-eslint/template-parser": "17.2.1",
"@angular/cli": "^17.1.1",
"@angular/compiler-cli": "^17.1.1",
"@angular/language-service": "^17.1.1",
"@fortawesome/fontawesome-free": "^6.5.1",
"@ngrx/eslint-plugin": "^17.1.0",
"@ngrx/schematics": "^17.1.0",
"@ngrx/store-devtools": "^17.1.0",
"@types/d3-selection": "^3.0.10",
"@types/dom-to-image": "^2.6.7",
"@types/has-ansi": "^5.0.2",
"@types/jasmine": "^5.1.4",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.6",
"@types/plotly.js": "^2.12.32",
"@types/tinycolor2": "^1.4.6",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.0.3",
"eslint-plugin-prefer-arrow": "^1.2.3",
"typescript": "~5.1.6"
"typescript": "~5.3.3"
}
}

View File

@@ -1,7 +1,7 @@
const fs = require('fs');
import * as fs from 'fs';
const targets = [
'https://api.trains-master.hosted.allegro.ai', // 1
'http://localhost:8081', // 1
];
const PROXY_CONFIG = {
@@ -30,15 +30,17 @@ const PROXY_CONFIG = {
};
targets.forEach((target, i) => {
const path = `/service/${i+1}/api`;
PROXY_CONFIG[path + '/*'] = {
const path = `/service/${i + 1}/api`;
PROXY_CONFIG[path] = {
target: target,
secure: false,
secure: true,
changeOrigin: true,
cookieDomainRewrite: 'localhost',
logLevel: 'debug',
pathRewrite: {
[path]: ''
[`^${path}`]: ''
}
};
});
module.exports = PROXY_CONFIG;
export default PROXY_CONFIG;

View File

@@ -66,13 +66,11 @@ export const routes: Routes = [
},
{
path: 'pipelines',
// canActivate: [RolePermissionsGuard],
data: {search: true},
loadChildren: () => import('@common/pipelines/pipelines.module').then(m => m.PipelinesModule),
},
{
path: 'pipelines',
// canActivate: [RolePermissionsGuard],
data: {search: true},
children: [
{

View File

@@ -3,5 +3,5 @@ import { StoreDevtoolsModule } from '@ngrx/store-devtools';
export const extCoreModules = [
StoreDevtoolsModule.instrument({
maxAge: 50
})
, connectInZone: true})
];

View File

@@ -0,0 +1,18 @@
<sm-search-results-page
(projectSelected)="projectCardClicked($event)"
(experimentSelected)="taskSelected($event)"
(modelSelected)="modelSelected($event)"
(pipelineSelected)="pipelineSelected($event)"
(reportSelected)="reportSelected($event)"
(activeLinkChanged)="changeActiveLink($event)"
(openDatasetSelected)="openDatasetCardClicked($event)"
(loadMoreClicked)="loadMore()"
[projectsList]="projectsResults$ | async"
[experimentsList]="experimentsResults$ | async"
[modelsList]="modelsResults$ | async"
[datasetsList]="datasetsResults$ | async"
[pipelinesList]="pipelinesResults$ | async"
[reportsList]="reportsResults$ | async"
[activeLink]="activeLink"
[resultsCount]="resultsCount$ | async"
></sm-search-results-page>

View File

@@ -0,0 +1,24 @@
import {Component} from '@angular/core';
import {DashboardSearchBaseComponent} from '@common/dashboard/dashboard-search.component.base';
import {selectIsSearching} from '@common/common-search/common-search.reducer';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {debounceTime, filter} from 'rxjs/operators';
@Component({
selector: 'sm-dashboard-search',
templateUrl: './dashboard-search.component.html',
styleUrls: ['./dashboard-search.component.scss'],
})
export class DashboardSearchComponent extends DashboardSearchBaseComponent {
constructor() {
super();
this.store.select(selectIsSearching)
.pipe(
debounceTime(200),
takeUntilDestroyed(),
filter(active => !active)
)
.subscribe(() => this.router.navigate(['dashboard'], ));
}
}

View File

@@ -1,10 +1,15 @@
<div class="search-container">
<div class="d-flex-center tabs">
<div class="ps-3 py-3">
<ng-container *ngFor="let searchTab of activeLinksList">
<span [class.active]="activeLink === searchTab.name" class="pointer category-link"
(click)="activeLinkChanged.emit(searchTab.name)">{{searchTab.label}} ({{resultsCount?.[searchTab.name]}}) </span>
</ng-container>
@for (searchTab of activeLinksList; track searchTab.name) {
<span
class="pointer category-link"
[class.active]="activeLink === searchTab.name"
[tabindex]="$count"
(click)="activeLinkChanged.emit(searchTab.name)"
(keyup)="activeLinkChanged.emit(searchTab.name)"
>{{searchTab.label}} ({{resultsCount?.[searchTab.name]}})</span>
}
</div>
</div>
<div class="page-container">

View File

@@ -4,7 +4,7 @@ import {Task} from '~/business-logic/model/tasks/task';
import {ITask} from '~/business-logic/model/al-task';
import {Model} from '~/business-logic/model/models/model';
import {activeLinksList, ActiveSearchLink, activeSearchLink} from '~/features/dashboard-search/dashboard-search.consts';
import {IReport} from '../../../../webapp-common/reports/reports.consts';
import {IReport} from '@common/reports/reports.consts';
@Component({
selector: 'sm-search-results-page',

View File

@@ -0,0 +1,22 @@
import {RouterModule, Routes} from '@angular/router';
import {NgModule} from '@angular/core';
import {CrumbTypeEnum} from '@common/layout/breadcrumbs/breadcrumbs.component';
import {DashboardSearchComponent} from '~/features/dashboard-search/containers/dashboard-search/dashboard-search.component';
const staticBreadcrumb = [[{
name: 'DASHBOARD',
type: CrumbTypeEnum.Feature
}]];
export const routes: Routes = [
{path: '', component: DashboardSearchComponent, data: {staticBreadcrumb}},
];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [RouterModule]
})
export class DashboardSearchRoutingModule {
}

View File

@@ -1,37 +1,39 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {AsyncPipe} from '@angular/common';
import {StoreModule} from '@ngrx/store';
import {EffectsModule} from '@ngrx/effects';
import {DashboardSearchEffects as commonDashboardSearchEffects } from '@common/dashboard-search/dashboard-search.effects';
import {DashboardSearchEffects} from '~/features/dashboard/dashboard-search/dashboard-search.effects';
import {ProjectsSharedModule} from '../../projects/shared/projects-shared.module';
import {SharedModule} from '~/shared/shared.module';
import {DashboardSearchEffects} from '~/features/dashboard-search/dashboard-search.effects';
import {dashboardSearchReducer} from '@common/dashboard-search/dashboard-search.reducer';
import {ReportsSharedModule} from '../../../webapp-common/reports/reports-shared.module';
import {ScrollingModule} from '@angular/cdk/scrolling';
import {ModelCardComponent} from '@common/shared/ui-components/panel/model-card/model-card.component';
import {ExperimentCardComponent} from '@common/shared/ui-components/panel/experiment-card/experiment-card.component';
import {CheckPermissionDirective} from '~/shared/directives/check-permission.directive';
import {ProjectCardComponent} from '@common/shared/ui-components/panel/project-card/project-card.component';
import {PipelineCardComponent} from '@common/pipelines/pipeline-card/pipeline-card.component';
import {VirtualGridComponent} from '@common/shared/components/virtual-grid/virtual-grid.component';
import {SearchResultsPageComponent} from '~/features/dashboard-search/containers/search-results-page/search-results-page.component';
import {DashboardSearchComponent} from '~/features/dashboard-search/containers/dashboard-search/dashboard-search.component';
import {DatasetsSharedModule} from '~/features/datasets/shared/datasets-shared.module';
import {ReportCardComponent} from '@common/reports/report-card/report-card.component';
import {DashboardSearchRoutingModule} from '~/features/dashboard-search/dashboard-search-routing.module';
@NgModule({
imports : [
CommonModule,
ProjectsSharedModule,
ReportsSharedModule,
imports: [
StoreModule.forFeature('search', dashboardSearchReducer),
EffectsModule.forFeature([DashboardSearchEffects, commonDashboardSearchEffects]),
SharedModule,
ScrollingModule,
ModelCardComponent,
ExperimentCardComponent,
DashboardSearchRoutingModule,
AsyncPipe,
VirtualGridComponent,
CheckPermissionDirective,
ProjectCardComponent,
ExperimentCardComponent,
ModelCardComponent,
PipelineCardComponent,
VirtualGridComponent
DatasetsSharedModule,
ReportCardComponent,
],
declarations:[
SearchResultsPageComponent, DashboardSearchComponent
]
})
export class DashboardSearchModule {
}
export class DashboardSearchModule {}

View File

@@ -3,12 +3,17 @@ import {NgModule} from '@angular/core';
import {DashboardComponent} from './dashboard.component';
import {CrumbTypeEnum} from '@common/layout/breadcrumbs/breadcrumbs.component';
export const routes: Routes = [
{path: '', component: DashboardComponent, data:{staticBreadcrumb:[[{
const staticBreadcrumb = [[{
name: 'DASHBOARD',
type: CrumbTypeEnum.Feature
}]]}
}
}]];
export const routes: Routes = [
{path: '', component: DashboardComponent, data: {staticBreadcrumb}},
{
path: 'search',
loadChildren: () => import('~/features/dashboard-search/dashboard-search.module').then(m => m.DashboardSearchModule),
data: {staticBreadcrumb}}
];
@NgModule({

View File

@@ -1,5 +1,4 @@
<sm-dashboard-search-base [class.dashboard-search]="activeSearch$ | async"></sm-dashboard-search-base>
<div *ngIf="(activeSearch$ | async) !== true" class="dashboard-body">
<div class="dashboard-body">
<div class="recent">
<sm-dashboard-projects (width)="setWidth($event)"></sm-dashboard-projects>
<sm-dashboard-experiments

View File

@@ -1,9 +1,8 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Observable, Subscription} from 'rxjs';
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import {Subscription} from 'rxjs';
import {Store} from '@ngrx/store';
import {ActivatedRoute, Router} from '@angular/router';
import {selectShowOnlyUserWork} from '@common/core/reducers/users-reducer';
import {GetCurrentUserResponseUserObjectCompany} from '~/business-logic/model/users/getCurrentUserResponseUserObjectCompany';
import {filter, skip, take} from 'rxjs/operators';
import {setDeep} from '@common/core/actions/projects.actions';
import {getRecentProjects, getRecentExperiments} from '@common/dashboard/common-dashboard.actions';
@@ -11,8 +10,10 @@ import {selectFirstLogin} from '@common/core/reducers/view.reducer';
import {MatDialog} from '@angular/material/dialog';
import {WelcomeMessageComponent} from '@common/layout/welcome-message/welcome-message.component';
import {firstLogin} from '@common/core/actions/layout.actions';
import {IRecentTask, selectRecentTasks} from '@common/dashboard/common-dashboard.reducer';
import {selectActiveSearch} from '@common/dashboard-search/dashboard-search.reducer';
import {selectRecentTasks} from '@common/dashboard/common-dashboard.reducer';
import {initSearch} from '@common/common-search/common-search.actions';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {selectActiveSearch} from '@common/common-search/common-search.reducer';
@Component({
@@ -21,26 +22,31 @@ import {selectActiveSearch} from '@common/dashboard-search/dashboard-search.redu
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit, OnDestroy {
public activeSearch$: Observable<boolean>;
public recentTasks$: Observable<Array<IRecentTask>>;
public workspace: GetCurrentUserResponseUserObjectCompany;
private store = inject(Store);
private router = inject(Router);
private activatedRoute = inject(ActivatedRoute);
private dialog = inject(MatDialog);
public recentTasks$ = this.store.select(selectRecentTasks);
public width: number;
private welcomeSub: Subscription;
private showOnlyUserWorkSub: Subscription;
private subscriptions = new Subscription();
constructor(
private store: Store<any>,
private router: Router,
private activatedRoute: ActivatedRoute,
private dialog: MatDialog,
) {
this.activeSearch$ = this.store.select(selectActiveSearch);
this.recentTasks$ = this.store.select(selectRecentTasks);
constructor() {
this.store.dispatch(initSearch({payload: 'Search for all'}));
this.showOnlyUserWorkSub = this.store.select(selectShowOnlyUserWork).pipe(skip(1)).subscribe(() => {
this.store.select(selectActiveSearch)
.pipe(
takeUntilDestroyed(),
filter(active => active)
)
.subscribe(() => this.router.navigate(['search'], {relativeTo: this.activatedRoute, queryParamsHandling: 'preserve'}));
this.subscriptions.add(this.store.select(selectShowOnlyUserWork).pipe(
skip(1),
).subscribe(() => {
this.store.dispatch(getRecentProjects());
this.store.dispatch(getRecentExperiments());
});
}));
this.welcomeSub = this.store.select(selectFirstLogin)
.pipe(

View File

@@ -7,19 +7,15 @@ import {StoreModule} from '@ngrx/store';
import {GettingStartedCardComponent} from './dumb/getting-started-card/getting-started-card.component';
import {CommonDashboardModule} from '@common/dashboard/common-dashboard.module';
import {commonDashboardReducer} from '@common/dashboard/common-dashboard.reducer';
import {SearchResultsPageComponent} from './dumb/search-results-page/search-results-page.component';
import {SharedModule} from '~/shared/shared.module';
import {DashboardSearchModule} from './dashboard-search/dashboard-search.module';
import {ProjectDialogModule} from '@common/shared/project-dialog/project-dialog.module';
import {ProjectsSharedModule} from '../projects/shared/projects-shared.module';
import {DashboardSearchBaseComponent} from '@common/dashboard/dashboard-search.component.base';
import {DatasetsSharedModule} from '~/features/datasets/shared/datasets-shared.module';
import {ScrollingModule} from '@angular/cdk/scrolling';
import {CheckPermissionDirective} from '~/shared/directives/check-permission.directive';
import {PlusCardComponent} from '@common/shared/ui-components/panel/plus-card/plus-card.component';
import {NeonButtonComponent} from '@common/shared/ui-components/buttons/neon-button/neon-button.component';
import {OverflowsDirective} from '@common/shared/ui-components/directives/overflows.directive';
import {ReportsSharedModule} from '@common/reports/reports-shared.module';
import {PipelineCardComponent} from '@common/pipelines/pipeline-card/pipeline-card.component';
import {ModelCardComponent} from '@common/shared/ui-components/panel/model-card/model-card.component';
import {ExperimentCardComponent} from '@common/shared/ui-components/panel/experiment-card/experiment-card.component';
@@ -36,21 +32,19 @@ import {VirtualGridComponent} from '@common/shared/components/virtual-grid/virtu
StoreModule.forFeature('dashboard', commonDashboardReducer),
CommonDashboardModule,
SharedModule,
DashboardSearchModule,
DatasetsSharedModule,
ScrollingModule,
CheckPermissionDirective,
PlusCardComponent,
NeonButtonComponent,
OverflowsDirective,
ReportsSharedModule,
PipelineCardComponent,
ModelCardComponent,
ExperimentCardComponent,
ProjectCardComponent,
VirtualGridComponent
],
declarations : [DashboardComponent, GettingStartedCardComponent, DashboardSearchBaseComponent, SearchResultsPageComponent]
declarations : [DashboardComponent, GettingStartedCardComponent]
})
export class DashboardModule {
}

View File

@@ -77,6 +77,7 @@ import {ClickStopPropagationDirective} from '@common/shared/ui-components/direct
import {FilterPipe} from '@common/shared/pipes/filter.pipe';
import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indicators/tooltip/show-tooltip-if-ellipsis.directive';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {DotsLoadMoreComponent} from '@common/shared/ui-components/indicators/dots-load-more/dots-load-more.component';
export const experimentSyncedKeys = [
'view.projectColumnsSortOrder',
@@ -107,7 +108,12 @@ export const getExperimentsConfig = (userPreferences: UserPreferences) => ({
return merge({}, nextState, savedState);
}
if (action.type.startsWith(EXPERIMENTS_PREFIX)) {
localStorage.setItem(localStorageKey, JSON.stringify(pick(nextState, ['view.tableMode', 'view.compareSelectedMetrics', 'view.compareSelectedMetricsPlots'])));
localStorage.setItem(localStorageKey, JSON.stringify(pick(nextState, [
'view.tableMode',
'view.tableCompareView',
'view.compareSelectedMetrics',
'view.compareSelectedMetricsPlots'
])));
}
return nextState;
};
@@ -189,6 +195,7 @@ const DECLARATIONS = [
FilterPipe,
ShowTooltipIfEllipsisDirective,
MatCheckboxModule,
DotsLoadMoreComponent,
],
declarations : [...DECLARATIONS],
providers : [

View File

@@ -1,3 +1,9 @@
export {PROJECT_ROUTES} from '@common/projects/common-projects.consts';
import {HeaderNavbarTabConfig} from '@common/layout/header-navbar-tabs/header-navbar-tabs-config.types';
export const PROJECTS_FEATURES = ['models',' experiments', 'overview'];
export const PROJECT_ROUTES = [
{header: 'overview', subHeader: '', id: 'overviewTab'},
{header: 'experiments', subHeader: '(ARCHIVED)', id: 'experimentsTab'},
{header: 'models', subHeader: '(ARCHIVED)', id: 'modelsTab'}
] as HeaderNavbarTabConfig[];

View File

@@ -1,8 +1,8 @@
<div class="title">APP CREDENTIALS <i
<header><div class="title">APP CREDENTIALS <i
class="fas fa-info-circle info"
smTooltip="Credentials providing API access to this workspace"
matTooltipPosition="above"></i>
</div>
</div></header>
<div class="section mb-4" *ngFor="let cred of credentials$ | async | keyvalue">
<sm-admin-credential-table
[credentials]="cred?.value"

View File

@@ -1,8 +1,10 @@
@import "variables";
:host {
max-width: 900px;
header {
padding: 20px 24px;
}
.title {
color: $white;
font-size: 16px;
@@ -15,7 +17,7 @@
}
.section {
margin: 6px 0 0 24px;
//margin: 6px 0 0 24px;
}
.info {
@@ -28,7 +30,7 @@
}
.add-button {
margin-top: 18px;
margin: 24px 0 48px 24px;
color: $blue-280;
&:hover {

View File

@@ -27,7 +27,7 @@ export class UserCredentialsComponent implements OnInit, OnDestroy {
.pipe(filter(user => !!user), take(1))
.subscribe(user => {
this.user = user;
this.store.dispatch(getAllCredentials());
this.store.dispatch(getAllCredentials({userId: ''}));
});
this.credentials$ = this.store.select(selectCredentials);

View File

@@ -12,7 +12,6 @@ import {ProfilePreferencesComponent} from '@common/settings/admin/profile-prefer
import {ProfileNameComponent} from '@common/settings/admin/profile-name/profile-name.component';
import {AdminFooterComponent} from '@common/settings/admin/admin-footer/admin-footer.component';
import {S3AccessComponent} from '@common/settings/admin/s3-access/s3-access.component';
import {AdminDialogTemplateComponent} from '@common/settings/admin/admin-dialog-template/admin-dialog-template.component';
import {AdminCredentialTableComponent} from '@common/settings/admin/admin-credential-table/admin-credential-table.component';
import {AdminFooterActionsComponent} from '~/features/settings/containers/admin/admin-footer-actions/admin-footer-actions.component';
import {UserCredentialsComponent} from '~/features/settings/containers/admin/user-credentials/user-credentials.component';
@@ -37,6 +36,9 @@ import {MatButtonToggleModule} from '@angular/material/button-toggle';
import {KeyValuePipe} from '@common/shared/pipes/key-value.pipe';
import {ButtonToggleComponent} from '@common/shared/ui-components/inputs/button-toggle/button-toggle.component';
import {LabelValuePipe} from '@common/shared/pipes/label-value.pipe';
import {AdminDialogTemplateComponent} from '@common/settings/admin/admin-dialog-template/admin-dialog-template.component';
import {TimeAgoPipe} from '@common/shared/pipes/timeAgo';
import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indicators/tooltip/show-tooltip-if-ellipsis.directive';
@@ -48,7 +50,6 @@ import {LabelValuePipe} from '@common/shared/pipes/label-value.pipe';
UserCredentialsComponent,
AdminFooterActionsComponent,
AdminCredentialTableComponent,
AdminDialogTemplateComponent,
S3AccessComponent,
CreateCredentialDialogComponent,
AdminFooterComponent,
@@ -82,7 +83,10 @@ import {LabelValuePipe} from '@common/shared/pipes/label-value.pipe';
MatButtonToggleModule,
KeyValuePipe,
ButtonToggleComponent,
LabelValuePipe
LabelValuePipe,
AdminDialogTemplateComponent,
TimeAgoPipe,
ShowTooltipIfEllipsisDirective,
],
exports: [
UserCredentialsComponent,

View File

@@ -1,9 +1,9 @@
@import "variables";
@import "src/app/webapp-common/shared/ui-components/styles/variables.scss";
@import "../../shared/ui-components/styles/variables";
@import "./variables";
@font-face {
font-family: '#{$icomoon-font-family}';
src: url('./#{$icomoon-font-family}.ttf?1skk4q') format('truetype');
src: url('./#{$icomoon-font-family}.ttf?hr04a1') format('truetype');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -24,6 +24,65 @@
-moz-osx-font-smoothing: grayscale;
}
.al-ico-queue {
&:before {
content: $al-ico-queue;
}
}
.al-ico-weight {
&:before {
content: $al-ico-weight;
}
}
.al-ico-link-plus {
&:before {
content: $al-ico-link-plus;
}
}
.al-ico-drag-vertical {
&:before {
content: $al-ico-drag-vertical;
}
}
.al-ico-drag-horizontal {
&:before {
content: $al-ico-drag-horizontal;
}
}
.al-ico-admin-support {
&:before {
content: $al-ico-admin-support;
}
}
.al-ico-scatter-view {
&:before {
content: $al-ico-scatter-view;
}
}.al-ico-scatter-view {
&:before {
content: $al-ico-scatter-view;
}
}
.al-ico-schedulers {
&:before {
content: $al-ico-schedulers;
}
}
.al-ico-triggers {
&:before {
content: $al-ico-triggers;
}
}
.al-ico-autoscalers {
&:before {
content: $al-ico-autoscalers;
}
}
.al-ico-automation {
&:before {
content: $al-ico-automation;
}
}
.al-ico-charts-view:before {
content: "\e9f9";
}

View File

@@ -1,6 +1,17 @@
$icomoon-font-family: "trains" !default;
$icomoon-font-path: "fonts" !default;
$al-ico-queue: "\ea01";
$al-ico-weight: "\ea05";
$al-ico-link-plus: "\ea02";
$al-ico-drag-vertical: "\ea03";
$al-ico-drag-horizontal: "\ea04";
$al-ico-admin-support: "\ea00";
$al-ico-scatter-view: "\e9ff";
$al-ico-schedulers: "\e9fb";
$al-ico-triggers: "\e9fc";
$al-ico-autoscalers: "\e9fd";
$al-ico-automation: "\e9fe";
$al-ico-charts-view: "\e9f9";
$al-ico-compact-view: "\e9fa";
$al-ico-tune: "\e9f8";

View File

@@ -1,15 +1,14 @@
<ng-container *ngIf="activated; else placeHolder" [ngSwitch]="type">
<ng-container *ngIf="(signIsNeeded$ | async) === false; else signIsNeededTemplate">
<ng-container *ngIf="(noPermissions$ | async) === false; else noPermissionsTemplate">
@if (activated) {
@if (signIsNeeded$() === false) {
@if (noPermissions$() === false) {
<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="#8492c2"/>
</svg>
<span class="webapp-link_tooltip">View original resource</span>
</a>
<ng-container [ngSwitch]="type">
<ng-container *ngSwitchCase="type === 'plot' || type === 'scalar' ? type : ''">
@switch (type) {
@case (type === 'plot' || type === 'scalar' ? type : '') {
<sm-single-graph
[hideDownloadButtons]="externalTool"
[class.less-padding]="false"
@@ -17,10 +16,10 @@
[graphsNumber]="1"
[height]="singleGraphHeight"
[chart]="plotData"
[id]="'lala'"
[id]="'report-widget'"
[isDarkTheme]="isDarkTheme"
[showLoaderOnDraw]="false"
[identifier]="'lala'"
[identifier]="'report-widget'"
[width]="400"
[xAxisType]="xaxis"
[isCompare]="true"
@@ -29,63 +28,60 @@
[hideMaximize]="hideMaximize"
(maximizeClicked)="maximize()">
</sm-single-graph>
</ng-container>
<ng-container *ngSwitchCase="'sample'">
<sm-debug-image-snippet
*ngIf="frame?.url"
class="d-flex-center h-100"
[frame]="frame"
[noHoverEffects]="true"
(imageClicked)="hideMaximize === 'show' && sampleClicked($event)">
</sm-debug-image-snippet>
</ng-container>
<ng-container *ngSwitchCase="'parcoords'">
<sm-parallel-coordinates-graph
*ngIf="parcoordsData"
[experiments]="parcoordsData.experiments"
[parameters]="parcoordsData.params"
[metric]="parcoordsData.metric"
[metricValueType]="parcoordsData.valueType"
[darkTheme]="isDarkTheme"
[reportMode]="true"
></sm-parallel-coordinates-graph>
</ng-container>
<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"
></sm-single-value-summary-table>
</div>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-template #placeHolder>
}
@case ('sample') {
@if (frame?.url) {
<sm-debug-image-snippet
class="d-flex-center h-100"
[frame]="frame"
[noHoverEffects]="true"
(imageClicked)="hideMaximize === 'show' && sampleClicked($event)">
</sm-debug-image-snippet>
}
}
@case ('parcoords') {
@if (parcoordsData) {
<sm-parallel-coordinates-graph
[experiments]="parcoordsData.experiments"
[parameters]="parcoordsData.params"
[metrics]="parcoordsData.metrics"
[darkTheme]="isDarkTheme"
[reportMode]="true"
></sm-parallel-coordinates-graph>
}
}
@case ('single') {
<div class="single-value-summary-section">
@if (singleValueData && singleValueData[0]) {
<sm-single-value-summary-table
[data]="singleValueData"
[experimentName]="singleValueData[0]?.metric"
[darkTheme]="isDarkTheme"
></sm-single-value-summary-table>
}
</div>
}
}
} @else {
<div class="placeholder">Missing permissions to view this item.
</div>
}
} @else {
<div class="container" [class.dark-theme]="isDarkTheme">
<div class="s3message">
Missing S3 credentials. Please verify credentials in <a target="_blank" href="/settings/webapp-configuration">WEB APP CLOUD ACCESS</a> in clearml app.
</div>
</div>
}
} @else {
<div class="placeholder">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<path
d="M43.29,43.65H5.29c-.55,0-1-.45-1-1h0c0-.55,.45-1,1-1H43.29c.55,0,1,.45,1,1h0c0,.55-.45,1-1,1h0Zm1-27v-2h-3.5V5.65h-1V14.79l-8.5,5.04-4.5-5.12V5.65h-1V14.49l-4.7,8.48h-4.3V5.65h-1V22.97h-2.42l-4.58-12.22V5.65h-1v5.1h-3.5v2h3.12l.38,1.02v9.2h-3.5v2h3.5v14.68h1v-14.68h3.18l3.82,10.2v4.48h1v-4.84l5.5-9.84h3.54v14.68h1v-14.82l4-2.42,8.92,10.18v7.06h1v-7h3.54v-2h-3.5v-14h3.5ZM8.79,22.97v-6.52l2.44,6.52h-2.44Zm5.32,2h1.68v4.48l-1.68-4.48Zm2.68,5.72v-5.72h3.18l-3.18,5.72Zm6.58-7.72l2.42-4.36v4.36h-2.42Zm3.42-.46v-4.76l2.74,3.12-2.74,1.64Zm5.84-1.14l7.16-4.26v12.54l-7.16-8.28Z"
fill="#5a658e"/>
</svg>
<span class="show-text" (click)="activate()">Show preview</span>
<span class="show-text" tabindex="1" (click)="activate()" (keyup)="activate()">Show preview</span>
</div>
</ng-template>
}
<ng-template #signIsNeededTemplate>
<div class="container" [class.dark-theme]="isDarkTheme">
<div class="s3message">
Missing S3 credentials. Please verify credentials in <a target="_blank" href="/settings/webapp-configuration">WEB APP CLOUD ACCESS</a> in clearml app.
</div>
</div>
</ng-template>
<ng-template #noPermissionsTemplate>
<div class="placeholder">Missing permissions to view this item.
</div>
</ng-template>

View File

@@ -1,4 +1,4 @@
@import "src/app/webapp-common/shared/ui-components/styles/variables";
@import "variables";
.placeholder {
height: 100%;

View File

@@ -1,19 +1,18 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, OnInit, ViewChild} from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
HostListener,
inject,
OnInit,
ViewChild
} from '@angular/core';
import {Store} from '@ngrx/store';
import {MatDialog} from '@angular/material/dialog';
import {Observable} from 'rxjs';
import {filter, map, switchMap, take} from 'rxjs/operators';
import {Environment} from '../environments/base';
import {getParcoords, getPlot, getSample, getScalar, getSingleValues, reportsPlotlyReady} from './app.actions';
import {
selectNoPermissions,
selectParallelCoordinateExperiments,
selectPlotData,
selectReportsPlotlyReady,
selectSampleData,
selectSignIsNeeded,
selectSingleValuesData,
} from './app.reducer';
import {appFeature} from './app.reducer';
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';
@@ -28,22 +27,48 @@ import {SingleGraphComponent} from '@common/shared/single-graph/single-graph.com
import {setCurrentDebugImage} from '@common/shared/debug-sample/debug-sample.actions';
import {isFileserverUrl} from '~/shared/utils/url';
import {MetricValueType, SelectedMetric} from '@common/experiments-compare/experiments-compare.constants';
import {ExtraTask} from '@common/experiments-compare/dumbs/parallel-coordinates-graph/parallel-coordinates-graph.component';
import {
ExtraTask,
ParallelCoordinatesGraphComponent
} from '@common/experiments-compare/dumbs/parallel-coordinates-graph/parallel-coordinates-graph.component';
import {EventsGetTaskSingleValueMetricsResponseValues} from '~/business-logic/model/events/eventsGetTaskSingleValueMetricsResponseValues';
import {ScalarKeyEnum} from '~/business-logic/model/reports/scalarKeyEnum';
import {ReportsApiMultiplotsResponse} from '@common/constants';
import {SingleGraphModule} from '@common/shared/single-graph/single-graph.module';
import {DebugSampleModule} from '@common/shared/debug-sample/debug-sample.module';
import {
SingleValueSummaryTableComponent
} from '@common/shared/single-value-summary-table/single-value-summary-table.component';
import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult';
import {SingleGraphStateModule} from '@common/shared/single-graph/single-graph-state.module';
type WidgetTypes = 'plot' | 'scalar' | 'sample' | 'parcoords' | 'single';
export interface SelectedMetricVariant extends MetricVariantResult {
valueType?: 'min_value' | 'max_value' | 'value';
}
@Component({
selector: 'sm-app-root',
selector: 'sm-widget-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
SingleGraphStateModule,
SingleGraphModule,
DebugSampleModule,
ParallelCoordinatesGraphComponent,
SingleValueSummaryTableComponent
]
})
export class AppComponent implements OnInit {
title = 'report-widgets';
private store = inject(Store);
private configService = inject(ConfigurationService);
private dialog = inject(MatDialog);
private cdr = inject(ChangeDetectorRef);
protected title = 'report-widgets';
public plotData: ExtFrame;
public frame: DebugSample;
public plotLoaded: boolean;
@@ -54,11 +79,11 @@ export class AppComponent implements OnInit {
public type: WidgetTypes;
public singleGraphHeight;
public hideMaximize: 'show' | 'hide' | 'disabled' = 'show';
public signIsNeeded$: Observable<boolean>;
public noPermissions$: Observable<boolean>;
protected signIsNeeded$ = this.store.selectSignal(appFeature.selectSignIsNeeded);
protected noPermissions$ = this.store.selectSignal(appFeature.selectNoPermissions);
public isDarkTheme: boolean;
public externalTool: boolean = false;
public parcoordsData: { experiments: ExtraTask[]; params: string[]; metric: SelectedMetric; valueType: MetricValueType };
public parcoordsData: { experiments: ExtraTask[]; params: string[]; metrics: SelectedMetricVariant[] };
@ViewChild(SingleGraphComponent) 'singleGraph': SingleGraphComponent;
public singleValueData: EventsGetTaskSingleValueMetricsResponseValues[];
public webappLink: string;
@@ -69,16 +94,10 @@ export class AppComponent implements OnInit {
this.singleGraph?.redrawPlot();
}
constructor(
private store: Store,
private configService: ConfigurationService,
private dialog: MatDialog,
private cdr: ChangeDetectorRef) {
constructor() {
this.configService.globalEnvironmentObservable.subscribe(env => {
this.environment = env;
});
this.signIsNeeded$ = store.select(selectSignIsNeeded);
this.noPermissions$ = store.select(selectNoPermissions);
this.searchParams = new URLSearchParams(window.location.search);
this.type = this.searchParams.get('type') as WidgetTypes;
this.webappLink = this.buildSourceLink(this.searchParams, '*', null);
@@ -154,6 +173,9 @@ export class AppComponent implements OnInit {
const metric = plot.metric;
groupedPlots[metric] = cloneDeep(groupedPlots[metric]) || null;
const plotParsed = tryParseJson(plot.plot_str);
if (plotParsed.data[0] && !plotParsed.data[0].name) {
plotParsed.data[0].name = plot.variant;
}
if (groupedPlots[metric] && ['scatter', 'bar'].includes(plotParsed?.data?.[0]?.type) && previousPlotIsMergable) {
groupedPlots[metric].plotParsed = {...groupedPlots[metric].plotParsed, data: _mergeVariants(groupedPlots[metric].plotParsed.data, plotParsed.data)};
} else {
@@ -167,18 +189,18 @@ export class AppComponent implements OnInit {
};
private getPlotData() {
this.store.select(selectReportsPlotlyReady).pipe(
this.store.select(appFeature.selectPlotlyReady).pipe(
filter(ready => !!ready),
switchMap(() => this.store.select(selectPlotData)),
switchMap(() => this.store.select(appFeature.selectPlotData)),
filter(plot => !!plot),
take(1))
.subscribe((metricsPlots) => {
this.plotLoaded = true;
if (this.isSingleExperiment(metricsPlots)) {
const merged = this.mergeVariants(metricsPlots as ReportsApiMultiplotsResponse);
this.plotData = Object.values(merged)[0].plotParsed;
this.plotData = {...Object.values(merged)[0], ...Object.values(merged)[0].plotParsed};
} else {
const {merged,} = prepareMultiPlots(metricsPlots as ReportsApiMultiplotsResponse);
const {merged} = prepareMultiPlots(metricsPlots as ReportsApiMultiplotsResponse);
const newGraphs = convertMultiPlots(merged);
const originalObject = this.searchParams.get('objects');
const series = this.searchParams.get('series');
@@ -213,7 +235,7 @@ export class AppComponent implements OnInit {
});
};
}
private isSingleExperiment(metricsPlots: any) {
try {
@@ -224,9 +246,9 @@ export class AppComponent implements OnInit {
}
private getScalars() {
this.store.select(selectReportsPlotlyReady).pipe(
this.store.select(appFeature.selectPlotlyReady).pipe(
filter(ready => !!ready),
switchMap(() => this.store.select(selectPlotData)),
switchMap(() => this.store.select(appFeature.selectPlotData)),
filter(plot => !!plot),
take(1))
.subscribe(metrics => {
@@ -240,16 +262,24 @@ export class AppComponent implements OnInit {
}
private getParallelCoordinate() {
this.store.select(selectReportsPlotlyReady).pipe(
this.store.select(appFeature.selectPlotlyReady).pipe(
filter(ready => !!ready),
switchMap(() => this.store.select(selectParallelCoordinateExperiments)),
switchMap(() => this.store.select(appFeature.selectParallelCoordinateData)),
filter(experiments => !!experiments),
take(1))
.subscribe(experiments => {
this.parcoordsData = {
experiments,
valueType: this.searchParams.get('value_type') as MetricValueType,
metric: {path: this.searchParams.get('metrics'), name: this.findMetricName(this.searchParams.get('metrics'), experiments)},
metrics: this.searchParams.getAll('metrics').map(metric => {
const [metricHash, variantHash, valueType] = metric.split('.');
const path = `${metricHash}.${variantHash}`;
return {
metric_hash: metricHash,
variant_hash: variantHash,
...this.findMetricName(path, experiments),
valueType: valueType ?? this.searchParams.get('value_type')
} as SelectedMetricVariant;
}),
params: this.searchParams.getAll('variants')
};
this.cdr.detectChanges();
@@ -258,7 +288,7 @@ export class AppComponent implements OnInit {
private getSample() {
this.store.select(selectSampleData)
this.store.select(appFeature.selectSampleData)
.pipe(filter(sample => !!sample))
.subscribe(sample => {
const url = new URL(sample.url);
@@ -281,7 +311,7 @@ export class AppComponent implements OnInit {
}
private getSingleValues() {
this.store.select(selectSingleValuesData)
this.store.select(appFeature.selectSingleValuesData)
.pipe(
filter(singleValueData => !!singleValueData),
take(1)
@@ -383,14 +413,14 @@ export class AppComponent implements OnInit {
private findMetricName(metric: string, experiments: ExtraTask[]) {
const experimentWithCurrentMetric = experiments.find(exp => get(exp.last_metrics, metric));
const lastMetric = get(experimentWithCurrentMetric.last_metrics, metric) as ExtraTask['last_metrics'];
return `${lastMetric.metric}/${lastMetric.variant}`;
return {metric: lastMetric.metric, variant: lastMetric.variant};
}
private buildSourceLink(searchParams: URLSearchParams, project: string, tasks: string[]): string {
const isModels = searchParams.has('models') || this.searchParams.get('objectType') === 'model';
const objects = searchParams.getAll('objects');
const variants = searchParams.getAll('variants');
const metricPath = searchParams.get('metrics') || '';
const metricsPath = searchParams.getAll('metrics');
let entityIds = objects.length > 0 ? objects : searchParams.getAll(isModels ? 'models' : 'tasks');
if (entityIds.length === 0 && tasks?.length > 0) {
entityIds = tasks;
@@ -398,8 +428,8 @@ 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.filter(id => !!id).join(',')}/
${this.getComparePath(this.type)}?metricPath=${metricPath}&metricName=lala${variants.map(par => `&params=${par}`).join('')}`;
url += `${isModels ? 'compare-models;ids=' : 'compare-experiments;ids='}${entityIds.filter(id => !!id).join(',')}/${
this.getComparePath(this.type)}?metricVariants=${encodeURIComponent(metricsPath.join(','))}&metricName=${variants.map(par => `&params=${encodeURIComponent(par)}`).join('')}`;
} else {
url += `${isModels ? 'models/' : 'experiments/'}${entityIds}/${this.getOutputPath(isModels, this.type)}`;
}

View File

@@ -0,0 +1,31 @@
import {ApplicationConfig, importProvidersFrom} from '@angular/core';
import {StoreModule} from '@ngrx/store';
import {EffectsModule} from '@ngrx/effects';
import {appFeature} from '@common/clearml-applications/report-widgets/src/app/app.reducer';
import {authReducer} from '~/features/settings/containers/admin/auth.reducers';
import {AppEffects} from '@common/clearml-applications/report-widgets/src/app/app.effects';
import {extCoreConfig} from '@common/clearml-applications/report-widgets/src/build';
import {provideHttpClient} from '@angular/common/http';
import {BaseAdminService} from '@common/settings/admin/base-admin.service';
import {ApiEventsService} from '~/business-logic/api-services/events.service';
import {SmApiRequestsService} from '~/business-logic/api-services/api-requests.service';
import {ColorHashService} from '@common/shared/services/color-hash/color-hash.service';
import {provideAnimations} from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(
StoreModule.forRoot({}),
StoreModule.forFeature(appFeature),
StoreModule.forFeature({name: 'auth', reducer: authReducer}),
EffectsModule.forRoot([AppEffects])
),
...extCoreConfig,
provideAnimations(),
provideHttpClient(),
BaseAdminService,
ApiEventsService,
SmApiRequestsService,
ColorHashService
]
};

View File

@@ -14,7 +14,6 @@ import {
import {EMPTY, mergeMap, of, switchMap} from 'rxjs';
import {Store} from '@ngrx/store';
import {catchError, filter} from 'rxjs/operators';
import {ApiReportsService} from '~/business-logic/api-services/reports.service';
import {BaseAdminService} from '@common/settings/admin/base-admin.service';
import {ReportsGetTaskDataResponse} from '~/business-logic/model/reports/reportsGetTaskDataResponse';
import {getSignedUrl, setSignedUrl} from '@common/core/actions/common-auth.actions';
@@ -37,7 +36,6 @@ export class AppEffects {
private httpClient: HttpClient,
private store: Store,
private actions$: Actions,
private reportsApi: ApiReportsService,
private adminService: BaseAdminService) {
}

View File

@@ -1,49 +0,0 @@
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AppComponent} from './app.component';
import {EffectsModule} from '@ngrx/effects';
import {AppEffects} from './app.effects';
import {StoreModule} from '@ngrx/store';
import {appReducer} from './app.reducer';
import {HttpClientModule} from '@angular/common/http';
import {MatDialogModule} from '@angular/material/dialog';
import {SingleGraphModule} from '@common/shared/single-graph/single-graph.module';
import {DebugSampleModule} from '@common/shared/debug-sample/debug-sample.module';
import {ApiEventsService} from '~/business-logic/api-services/events.service';
import {ApiReportsService} from '~/business-logic/api-services/reports.service';
import {BaseAdminService} from '@common/settings/admin/base-admin.service';
import {ColorHashService} from '@common/shared/services/color-hash/color-hash.service';
import {authReducer} from '~/features/settings/containers/admin/auth.reducers';
import {extCoreModules} from '~/build-specifics';
import {SmApiRequestsService} from '~/business-logic/api-services/api-requests.service';
import {ParallelCoordinatesGraphComponent} from '@common/experiments-compare/dumbs/parallel-coordinates-graph/parallel-coordinates-graph.component';
import {SingleValueSummaryTableComponent} from '@common/shared/single-value-summary-table/single-value-summary-table.component';
if (!localStorage.getItem('_saved_state_')) {
localStorage.setItem('_saved_state_', '{}');
}
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserAnimationsModule,
BrowserModule,
HttpClientModule,
MatDialogModule,
SingleGraphModule,
DebugSampleModule,
ParallelCoordinatesGraphComponent,
StoreModule.forRoot({appReducer, auth: authReducer}),
EffectsModule.forRoot([AppEffects]),
...extCoreModules,
SingleValueSummaryTableComponent
],
providers: [ApiEventsService, ApiReportsService, SmApiRequestsService, ColorHashService, BaseAdminService],
bootstrap: [AppComponent]
})
export class AppModule {
}

View File

@@ -1,4 +1,4 @@
import {createReducer, createSelector, on} from '@ngrx/store';
import {createFeature, createReducer, on} from '@ngrx/store';
import {reportsPlotlyReady, setNoPermissions, setParallelCoordinateExperiments, setPlotData, setSampleData, setSignIsNeeded, setSingleValues, setTaskData} from './app.actions';
import {DebugSample} from '@common/shared/debug-sample/debug-sample.reducer';
import {MetricsPlotEvent} from '~/business-logic/model/events/metricsPlotEvent';
@@ -44,32 +44,21 @@ export const initialState: State = {
taskData: null
};
export const appReducer = createReducer(
initialState,
on(reportsPlotlyReady, (state) => ({...state, plotlyReady: true})),
on(setPlotData, (state, action) => ({...state, plotData: action.data as ReportsApiMultiplotsResponse})),
on(setSampleData, (state, action) => ({...state, sampleData: action.data})),
on(setSingleValues, (state, action) => ({...state, singleValuesData: action.data})),
on(setParallelCoordinateExperiments, (state, action) => ({...state, parallelCoordinateData: action.data})),
on(setSignIsNeeded, (state) => ({...state, signIsNeeded: true})),
on(setNoPermissions, (state) => ({...state, noPermissions: true})),
on(setTaskData, (state, action) => ({...state, taskData:
{appId: action.appId, sourceTasks: action.sourceTasks, sourceProject: action.sourceProject}})
export const appFeature = createFeature({
name: 'app',
reducer: createReducer(
initialState,
on(reportsPlotlyReady, (state): State => ({...state, plotlyReady: true})),
on(setPlotData, (state, action): State => ({...state, plotData: action.data as ReportsApiMultiplotsResponse})),
on(setSampleData, (state, action): State => ({...state, sampleData: action.data})),
on(setSingleValues, (state, action): State => ({...state, singleValuesData: action.data})),
on(setParallelCoordinateExperiments, (state, action): State => ({...state, parallelCoordinateData: action.data})),
on(setSignIsNeeded, (state): State => ({...state, signIsNeeded: true})),
on(setNoPermissions, (state): State => ({...state, noPermissions: true})),
on(setTaskData, (state, action): State => ({
...state, taskData:
{appId: action.appId, sourceTasks: action.sourceTasks, sourceProject: action.sourceProject}
})
),
),
);
export const selectFeature = state => state.appReducer as State;
export const selectScaleFactor = createSelector(selectFeature, state => state.scaleFactor);
export const selectReportsPlotlyReady = createSelector(selectFeature, state => state.plotlyReady);
export const selectPlotData = createSelector(selectFeature, state => state.plotData);
export const selectSampleData = createSelector(selectFeature, state => state.sampleData);
export const selectSingleValuesData = createSelector(selectFeature, state => state.singleValuesData);
export const selectParallelCoordinateExperiments = createSelector(selectFeature, state => state.parallelCoordinateData);
export const selectSignIsNeeded = createSelector(selectFeature, state => state.signIsNeeded);
export const selectNoPermissions = createSelector(selectFeature, state => state.noPermissions);
export const selectTaskData = createSelector(selectFeature, (state): {
sourceProject: string;
sourceTasks: string[];
appId: string;
} => state.taskData);
});

View File

@@ -0,0 +1 @@
export const extCoreModules = [];

View File

@@ -0,0 +1,10 @@
import {provideStoreDevtools} from '@ngrx/store-devtools';
export const extCoreConfig = [
provideStoreDevtools({
maxAge: 75,
trace: true,
traceLimit: 50,
connectInZone: true
})
];

View File

@@ -17,7 +17,7 @@ export const environment = {
production: false,
baseUrl: 'localhost:4200',
autoLogin: false,
apiBaseUrl: 'service/1/api',
apiBaseUrl: '../service/1/api',
// communityServer: true,
accountAdministration: true,
fileBaseUrl: 'https://files.allegro.ai',

View File

@@ -8,6 +8,6 @@
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<sm-app-root style="width: 100%; height: 100%"></sm-app-root>
<sm-widget-root style="width: 100%; height: 100%"></sm-widget-root>
</body>
</html>

View File

@@ -1,15 +1,14 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import {APP_BASE_HREF} from '@angular/common';
import {Environment} from '../../../../../environments/base';
import {bootstrapApplication} from '@angular/platform-browser';
import {environment} from './environments/environment';
import {updateHttpUrlBaseConstant} from '~/app.constants';
import {appConfig} from '@common/clearml-applications/report-widgets/src/app/app.config';
import {Environment} from '../../../../../environments/base';
import {AppComponent} from './app/app.component';
// bootstrapApplication(AppComponent, appConfig)
// .catch((err) => console.error(err));
if (environment.production) {
enableProdMode();
}
(async () => {
const configData = {baseHref: ''} as Environment;
@@ -18,8 +17,9 @@ if (environment.production) {
(window as any).configuration = {};
} finally {
updateHttpUrlBaseConstant({...environment, ...configData});
await platformBrowserDynamic([
{provide: APP_BASE_HREF, useValue: configData.baseHref}
]).bootstrapModule(AppModule);
await bootstrapApplication(AppComponent, {providers: [
...appConfig.providers,
{provide: APP_BASE_HREF, useValue: configData.baseHref}
]});
}
})();

View File

@@ -1,53 +0,0 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@@ -132,7 +132,7 @@ $sm-dark-theme: mat.define-dark-theme((
@include mat.select-theme($sm-dark-theme);
@import "src/app/webapp-common/shared/ui-components/styles/variables";
@import "variables";
* {
margin: 0;
@@ -191,6 +191,11 @@ body {
.h-100 {
height: 100%;
}
.w-100 {
width: 100% !important;
}
.dark-theme .plot-container .hoverlayer {
line[stroke-width="1"] {
stroke: $blue-300;
@@ -276,11 +281,6 @@ body {
display: inline-block;
}
.modebar {
top: 20px !important;
}
.modebar-btn[data-attr="plotly-disabled-maximize"] {
cursor: default !important;

View File

@@ -7,7 +7,6 @@
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"

View File

@@ -9,7 +9,6 @@
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",

View File

@@ -31,8 +31,9 @@ export const searchReducer = createReducer(
);
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 ? state.searchQuery : searchInitState.searchQuery);
export const selectPlaceholder = createSelector(selectCommonSearch, (state: SearchState) => state ? state.placeholder : '');
export const selectIsSearching = createSelector(selectCommonSearch, state => state ? state.isSearching : false);
export const selectSearchQuery = createSelector(selectCommonSearch, state => state ? state.searchQuery : searchInitState.searchQuery);
export const selectPlaceholder = createSelector(selectCommonSearch, state => state ? state.placeholder : '');
export const selectActiveSearch = createSelector(selectSearchQuery, state => state?.query?.length >= 1);

View File

@@ -8,6 +8,23 @@
@import "layout/layout";
@include mat.core();
// The following mixins include base theme styles that are only needed once per application. These
// theme styles do not depend on the color, typography, or density settings in your theme. However,
// these styles may differ depending on the theme's design system. Currently all themes use the
// Material 2 design system, but in the future it may be possible to create theme based on other
// design systems, such as Material 3.
//
// Please note: you do not need to include the 'base' mixins, if you include the corresponding
// 'theme' mixin elsewhere in your Sass. The full 'theme' mixins already include the base styles.
//
// To learn more about "base" theme styles visit our theming guide:
// https://material.angular.io/guide/theming#theming-dimensions
//
// TODO(v17): Please move these @include statements to the preferred place in your Sass, and pass
// your theme to them. This will ensure the correct values for your app are included.
//@include mat.all-component-bases(/* TODO(v17): pass $your-theme here */);
$custom-typography: mat.define-typography-config(
$font-family: $font-family-base
);
@@ -169,6 +186,15 @@ $sm-neon-theme: mat.define-dark-theme((
}
}
}
.inline-code {
background-color: $blue-50;
border: 1px solid $blue-100;
border-radius: 4px;
padding: 2px 6px;
font-size: 12px;
font-family: $font-family-monospace;
}
}
* {
@@ -319,11 +345,12 @@ mat-expansion-panel {
background: $faint-gray !important;
}
.mat-mdc-radio-button.sm {
.mat-mdc-radio-button {
--mdc-radio-state-layer-size: 16px;
.mdc-radio {
height: 16px;
width: 16px;
}
.mdc-radio__background {
@@ -331,17 +358,15 @@ mat-expansion-panel {
height: 16px;
.mdc-radio__inner-circle {
top: -2px;
left: -2px;
border-width: 8px;
}
}
.mat-radio-ripple {
.mat-ripple.mat-radio-ripple {
height: 32px;
width: 32px;
left: calc(50% - 18px);
top: calc(50% - 18px);
border-radius: 100%;
left: calc(50% - 16px);
top: calc(50% - 16px);
}
}
@@ -637,6 +662,11 @@ html {
padding: 0 32px 0 12px;
border-radius: 4px;
.mat-icon {
margin-right: 12px;
}
.mat-mdc-menu-item-text, .mdc-list-item__primary-text {
--mat-menu-item-label-text-tracking: 0;
display: flex;

View File

@@ -76,6 +76,8 @@ export const ICONS = {
ARROW_UP: 'al-ico-ico-chevron-up',
RUN: 'al-ico-run',
METADATA: 'al-ico-metadata',
ID: 'al-ico-id',
CHECK: 'al-ico-success'
};
export type IconNames = keyof typeof ICONS;

View File

@@ -16,7 +16,7 @@ export const updateS3Credential = createAction(
);
export const createCredential = createAction(
AUTH_PREFIX + 'CREATE_CREDENTIAL (API)',
props<{workspace: GetCurrentUserResponseUserObjectCompany; openCredentialsPopup?: boolean; label?: string}>()
props<{workspace: GetCurrentUserResponseUserObjectCompany; openCredentialsPopup?: boolean; label?: string; userId?: string}>()
);
export const updateCredentialLabel = createAction(
@@ -33,6 +33,7 @@ export const addCredential = createAction(
props<{ newCredential: CredentialKeyExt; workspaceId: string }>()
);
export const resetCredential = createAction(AUTH_PREFIX + 'RESET_CREDENTIAL');
export const resetCredentials = createAction(AUTH_PREFIX + 'RESET_CREDENTIALS');
export const removeCredential = createAction(
AUTH_PREFIX + '[remove credentials]',
props<{ accessKey: string; workspaceId: string }>()
@@ -55,7 +56,9 @@ export const showLocalFilePopUp = createAction(
AUTH_PREFIX + 'SHOW_LOCAL_FILE_POPUP',
props<{ url: string }>()
);
export const getAllCredentials = createAction(AUTH_PREFIX + 'GET_ALL_CREDENTIALS');
export const getAllCredentials = createAction(
AUTH_PREFIX + 'GET_ALL_CREDENTIALS',
props<{userId?: string}>());
export const credentialRevoked = createAction(
AUTH_PREFIX + 'REVOKE_CREDENTIAL (API)',
props<{ accessKey: string; workspaceId: string }>()

View File

@@ -33,8 +33,6 @@ export const setFilterByUser = createAction(
);
export const setUserWorkspacesFromUser = createAction(USERS_PREFIX + ' set user workspaces from current user');
export const setAccountAdministrationPage = createAction(`${USERS_PREFIX} route to account-administration` );
export const getApiVersion = createAction(`${USERS_PREFIX} get api version` );
export const setApiVersion = createAction(`${USERS_PREFIX} set api version`, props<{serverVersions: {server: string; api: string}}>());

View File

@@ -2,6 +2,7 @@ import {Injectable} from '@angular/core';
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 {setCredentialLabel} 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} from 'rxjs/operators';
@@ -12,14 +13,11 @@ import {GetCurrentUserResponseUserObject} from '~/business-logic/model/users/get
import {AdminService} from '~/shared/services/admin.service';
import {selectDontShowAgainForBucketEndpoint, selectS3BucketCredentialsBucketCredentials, selectSignedUrl} from '@common/core/reducers/common-auth-reducer';
import {EMPTY, of} from 'rxjs';
import {
S3AccessDialogData,
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 {isGoogleCloudUrl, SignResponse} from '@common/settings/admin/base-admin-utils';
import {isFileserverUrl} from '~/shared/utils/url';
import {selectRouterQueryParams} from '@common/core/reducers/router-reducer';
@Injectable()
export class CommonAuthEffects {
@@ -32,7 +30,8 @@ export class CommonAuthEffects {
private store: Store,
private adminService: AdminService,
private matDialog: MatDialog
) {}
) {
}
activeLoader = createEffect(() => this.actions.pipe(
ofType(authActions.getAllCredentials, authActions.createCredential),
@@ -41,7 +40,8 @@ export class CommonAuthEffects {
getAllCredentialsEffect = createEffect(() => this.actions.pipe(
ofType(authActions.getAllCredentials),
switchMap(action => this.credentialsApi.authGetCredentials({}).pipe(
switchMap(action => this.credentialsApi.authGetCredentials({},
{userId: action.userId}).pipe(
concatLatestFrom(() => this.store.select(selectCurrentUser)),
mergeMap(([res, user]: [AuthGetCredentialsResponse, GetCurrentUserResponseUserObject]) => [
authActions.updateAllCredentials({credentials: res.credentials, extra: res?.['additional_credentials'], workspace: user.company.id}),
@@ -53,9 +53,15 @@ export class CommonAuthEffects {
revokeCredential = createEffect(() => this.actions.pipe(
ofType(authActions.credentialRevoked),
// eslint-disable-next-line @typescript-eslint/naming-convention
mergeMap(action => this.credentialsApi.authRevokeCredentials({access_key: action.accessKey}).pipe(
mergeMap(() => [authActions.removeCredential(action), deactivateLoader(action.type)]),
concatLatestFrom(() => [
this.store.select(selectRouterQueryParams).pipe(map(params => params.userId)),
]),
mergeMap(([action, userId]) => this.credentialsApi.authRevokeCredentials(
{access_key: action.accessKey}, {userId}).pipe(
mergeMap(() => [
authActions.removeCredential(action),
deactivateLoader(action.type)
]),
catchError(error => [
requestFailed(error),
deactivateLoader(action.type),
@@ -66,32 +72,40 @@ export class CommonAuthEffects {
createCredential = createEffect(() => this.actions.pipe(
ofType(authActions.createCredential),
mergeMap(action => this.credentialsApi.authCreateCredentials({label: action.label}).pipe(
mergeMap(({credentials}) => [
authActions.addCredential({newCredential: credentials, workspaceId: action.workspace?.id}),
deactivateLoader(action.type)
]),
catchError(error => [
requestFailed(error),
setServerError(error, null, 'Unable to create credentials'),
authActions.addCredential({newCredential: {}, workspaceId: action.workspace?.id}),
deactivateLoader(action.type)])
))
concatLatestFrom(() => [
this.store.select(selectRouterQueryParams).pipe(map(params => params.userId)),
]),
mergeMap(([action, userId]) =>
this.credentialsApi.authCreateCredentials({label: action.label},
{userId}).pipe(
mergeMap(({credentials}) => [
authActions.addCredential({newCredential: credentials, workspaceId: action.workspace?.id}),
deactivateLoader(action.type)
]),
catchError(error => [
requestFailed(error),
setServerError(error, null, 'Unable to create credentials'),
authActions.addCredential({newCredential: {}, workspaceId: action.workspace?.id}),
deactivateLoader(action.type)])
))
));
updateCredentialLabel = createEffect(() => this.actions.pipe(
ofType(authActions.updateCredentialLabel),
concatLatestFrom(() => this.store.select(selectRouterQueryParams).pipe(map(params => params.userId))),
// eslint-disable-next-line @typescript-eslint/naming-convention
mergeMap(action => this.credentialsApi.authEditCredentials({access_key: action.credential.access_key, label: action.label}).pipe(
mergeMap(() => [
setCredentialLabel({credential: action.credential, label: action.label}),
deactivateLoader(action.type)
]),
catchError(error => [
requestFailed(error),
setServerError(error, null, 'Unable to update credentials'),
deactivateLoader(action.type)])
))
mergeMap(([action, userId]) =>
this.credentialsApi.authEditCredentials({access_key: action.credential.access_key, label: action.label},
{userId}).pipe(
mergeMap(() => [
setCredentialLabel({credential: action.credential, label: action.label}),
deactivateLoader(action.type)
]),
catchError(error => [
requestFailed(error),
setServerError(error, null, 'Unable to update credentials'),
deactivateLoader(action.type)])
))
));
refresh = createEffect(() => this.actions.pipe(
@@ -129,8 +143,8 @@ export class CommonAuthEffects {
return EMPTY;
}
}
),
),
)
)
)
));

View File

@@ -96,59 +96,58 @@ export class ProjectsEffects {
getTablesFilterProjectsOptions$ = createEffect(() => this.actions$.pipe(
ofType(actions.getTablesFilterProjectsOptions),
debounceTime(300),
concatLatestFrom(() => [
this.store.select(selectShowHidden),
this.store.select(selectProjectsOptionsScrollId),
this.getRelevantTableFilters(this.store.select(selectRouterConfig))
]),
switchMap(([action, showHidden, scrollId, filters]) => forkJoin([
ofType(actions.getTablesFilterProjectsOptions),
debounceTime(300),
concatLatestFrom(() => [
this.store.select(selectShowHidden),
this.store.select(selectProjectsOptionsScrollId),
this.getRelevantTableFilters(this.store.select(selectRouterConfig))
]),
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'],
only_fields: ['name', 'company'],
search_hidden: showHidden,
_any_: {pattern: escapeRegex(action.searchString), fields: ['name']},
scroll_id: !!action.loadMore && scrollId ? scrollId : null
} as ProjectsGetAllExRequest),
!action.loadMore && action.searchString?.length > 2 ?
this.projectsApi.projectsGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */
allow_public: action.allowPublic,
page_size: rootProjectsPageSize,
size: rootProjectsPageSize,
order_by: ['name'],
page_size: 1,
only_fields: ['name', 'company'],
search_hidden: showHidden,
_any_: {pattern: escapeRegex(action.searchString), fields: ['name']},
scroll_id: !!action.loadMore && scrollId
} as ProjectsGetAllExRequest),
!action.loadMore && action.searchString?.length > 2 ?
this.projectsApi.projectsGetAllEx({
page_size: 1,
only_fields: ['name', 'company'],
search_hidden: showHidden,
_any_: {pattern: `^${escapeRegex(action.searchString)}$`, fields: ['name', 'id']},
/* eslint-enable @typescript-eslint/naming-convention */
} as ProjectsGetAllExRequest).pipe(map(res => res.projects)) :
of([]),
!action.loadMore && filters['project.name']?.value.length ?
this.projectsApi.projectsGetAllEx({
id: filters['project.name']?.value,
only_fields: ['name', 'company'],
/* eslint-enable @typescript-eslint/naming-convention */
} as ProjectsGetAllExRequest).pipe(map(res => res.projects)) :
of([]),
])
.pipe(map(([allProjects, specificProjects, selectedProjects]) => ({
projects: [
...(specificProjects.length > 0 && allProjects.projects.some(project => project.id === specificProjects[0]?.id) ? [] : specificProjects),
...allProjects.projects,
...selectedProjects
],
scrollId: allProjects.scroll_id,
loadMore: action.loadMore
})
))
),
mergeMap((projects: {
projects: ProjectsGetAllResponseSingle[];
scrollId: string;
}) => [setTablesFilterProjectsOptions({...projects})])
)
);
/* eslint-enable @typescript-eslint/naming-convention */
} as ProjectsGetAllExRequest).pipe(map(res => res.projects)) :
of([]),
!action.loadMore && filters['project.name']?.value.length ?
this.projectsApi.projectsGetAllEx({
id: filters['project.name']?.value,
only_fields: ['name', 'company'],
/* eslint-enable @typescript-eslint/naming-convention */
} as ProjectsGetAllExRequest).pipe(map(res => res.projects)) :
of([]),
])
.pipe(map(([allProjects, specificProjects, selectedProjects]) => ({
projects: [
...(specificProjects.length > 0 && allProjects.projects.some(project => project.id === specificProjects[0]?.id) ? [] : specificProjects),
...allProjects.projects,
...selectedProjects
],
scrollId: allProjects.scroll_id,
loadMore: action.loadMore
})
))
),
mergeMap((projects: {
projects: ProjectsGetAllResponseSingle[];
scrollId: string;
}) => [setTablesFilterProjectsOptions({...projects})])
));
resetProjects$ = createEffect(() => this.actions$.pipe(
@@ -362,7 +361,7 @@ export class ProjectsEffects {
/* eslint-disable @typescript-eslint/naming-convention */
include_subprojects: true
/* eslint-enable @typescript-eslint/naming-convention */
}, null, 'body', true).pipe(
}, {adminQuery: true}).pipe(
mergeMap(res => [actions.setAllProjectUsers(res)]),
catchError(error => [
requestFailed(error),
@@ -381,7 +380,7 @@ export class ProjectsEffects {
projects: [action.projectId],
// eslint-disable-next-line @typescript-eslint/naming-convention
include_subprojects: isDeep
}, null, 'body', true)).pipe(
}, {adminQuery: true})).pipe(
mergeMap(res => [actions.setProjectUsers(res)]),
catchError(error => [
requestFailed(error),
@@ -398,7 +397,7 @@ export class ProjectsEffects {
only_fields: ['name'],
id: action.filteredUsers || []
/* eslint-enable @typescript-eslint/naming-convention */
}, null, 'body', true).pipe(
}, {adminQuery: true}).pipe(
mergeMap(res => [
actions.setProjectExtraUsers(res),
deactivateLoader(action.type)

View File

@@ -1,4 +1,5 @@
import {Injectable} from '@angular/core';
import {LocationStrategy} from '@angular/common';
import {inject, Injectable} from '@angular/core';
import {Router} from '@angular/router';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {ApiUsersService} from '~/business-logic/api-services/users.service';
@@ -9,7 +10,7 @@ import {
setApiVersion, setCurrentUserName,
setUserWorkspacesFromUser, updateCurrentUser
} from '../actions/users.actions';
import {catchError, map, mergeMap} from 'rxjs/operators';
import {catchError, filter, map, mergeMap} from 'rxjs/operators';
import {addMessage} from '../actions/layout.actions';
import {ApiLoginService} from '~/business-logic/api-services/login.service';
import {LoginLogoutResponse} from '~/business-logic/model/login/loginLogoutResponse';
@@ -21,6 +22,7 @@ import {MESSAGES_SEVERITY} from '@common/constants';
@Injectable()
export class CommonUserEffects {
private locationStrategy = inject(LocationStrategy);
constructor(
private actions: Actions, private userService: ApiUsersService,
@@ -34,7 +36,7 @@ export class CommonUserEffects {
ofType(logout),
mergeMap(action => this.loginApi.loginLogout({
// eslint-disable-next-line @typescript-eslint/naming-convention
redirect_url: window.location.origin + '/login',
redirect_url: window.location.origin + (this.locationStrategy.getBaseHref() === '/' ? '' : this.locationStrategy.getBaseHref()) + '/login',
...(action.provider && {provider: action.provider})
}).pipe(
map((res: LoginLogoutResponse) => {
@@ -62,7 +64,8 @@ ${this.errorService.getErrorMsg(err?.error)}`)])
updateCurrentUser = createEffect(() => this.actions.pipe(
ofType(updateCurrentUser),
mergeMap(({user}) => this.userService.usersUpdate({...user}).pipe(
mergeMap((res: UsersUpdateResponse) => res.updated ? [setCurrentUserName({name: user.name})] : [])
filter((res: UsersUpdateResponse) => res.updated > 0),
map(() => setCurrentUserName({name: user.name}))
)),
catchError(err => [addMessage(MESSAGES_SEVERITY.ERROR, `Update User Failed ${this.errorService.getErrorMsg(err?.error)}`)])
));

View File

@@ -4,7 +4,7 @@ import {isEqual} from 'lodash-es';
import {
addCredential,
cancelS3Credentials,
removeCredential, removeSignedUrl, resetCredential,
removeCredential, removeSignedUrl, resetCredential, resetCredentials,
resetDontShowAgainForBucketEndpoint,
saveS3Credentials, setCredentialLabel, setS3Credentials, setSignedUrl,
showLocalFilePopUp,
@@ -118,6 +118,7 @@ export const commonAuthReducer = [
}
}),
on(resetCredential, state => ({...state, newCredential: initAuth.newCredential})),
on(resetCredentials, state => ({...state, credentials: initAuth.credentials})),
on(addCredential, (state, action) => ({
...state,
newCredential: {...action.newCredential, company: action.workspaceId},

View File

@@ -37,6 +37,7 @@ export interface ScatterPlotSeries {
y: number;
name: string;
description?: string;
extraParamsHoverInfo?: string[];
}[]
}
@@ -115,7 +116,7 @@ export const selectMainPageTagsFilterMatchMode = createSelector(projects, select
export const selectCompanyTags = createSelector(projects, state => state.companyTags);
// eslint-disable-next-line @typescript-eslint/naming-convention
export const selectProjectSystemTags = createSelector(projects, state => getSystemTags({system_tags: state.systemTags} as ITableExperiment));
export const selectTagsColors = createSelector(projects, state => state.tagsColors);
export const selectTagsColors = createSelector(projects, state => state?.tagsColors);
export const selectLastUpdate = createSelector(projects, state => state.lastUpdate);
export const selectTagColors = createSelector(selectTagsColors,
(tagsColors, props: { tag: string }) => tagsColors[props.tag]);

View File

@@ -15,6 +15,8 @@ import {
} from '~/business-logic/model/organization/organizationGetUserCompaniesResponseCompanies';
import {GettingStarted} from '~/core/actions/users.action';
import {UsersGetCurrentUserResponseSettings} from '~/business-logic/model/users/usersGetCurrentUserResponseSettings';
import {AuthEditUserRequest} from '~/business-logic/model/auth/authEditUserRequest';
import RoleEnum = AuthEditUserRequest.RoleEnum;
export interface UsersState {
currentUser: GetCurrentUserResponseUserObject;
@@ -44,6 +46,7 @@ export const users = state => state.users as UsersState;
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 selectIsAdmin = createSelector(users, state => state.currentUser.role === RoleEnum.Admin);
export const selectActiveWorkspace = createSelector(users, state => state.activeWorkspace);
export const selectActiveWorkspaceTier = createSelector(selectActiveWorkspace, workspace => workspace?.tier);
export const selectUserWorkspaces = createSelector(users, state => state.userWorkspaces);

View File

@@ -5,13 +5,11 @@ import {
getCurrentPageResults,
getResultsCount,
searchActivate,
searchClear,
searchExperiments,
searchModels,
searchOpenDatasets,
searchPipelines,
searchProjects, searchReports,
searchSetTerm,
searchStart,
setExperimentsResults,
setModelsResults, setOpenDatasetsResults,
@@ -27,7 +25,6 @@ import {ProjectsGetAllExRequest} from '~/business-logic/model/projects/projectsG
import {ApiTasksService} from '~/business-logic/api-services/tasks.service';
import {ApiModelsService} from '~/business-logic/api-services/models.service';
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';
import {escapeRegex} from '@common/shared/utils/escape-regex';
@@ -119,10 +116,10 @@ export class DashboardSearchEffects {
this.store.select(selectActiveSearch),
this.store.select(selectSearchTerm)
]),
mergeMap(([action, active, term]) => {
mergeMap(([, active, term]) => {
const actionsToFire = [];
if (!active) {
actionsToFire.push(searchClear());
// actionsToFire.push(searchClear());
actionsToFire.push(searchActivate());
}
actionsToFire.push(getResultsCount(term));

View File

@@ -1,4 +1,4 @@
import {createFeatureSelector, createSelector, ReducerTypes, on, createReducer} from '@ngrx/store';
import {createFeatureSelector, createSelector, ReducerTypes, on, createReducer, ActionCreator} from '@ngrx/store';
import {Project} from '~/business-logic/model/projects/project';
import {User} from '~/business-logic/model/users/user';
import {Task} from '~/business-logic/model/tasks/task';
@@ -15,6 +15,7 @@ import {
import {SearchState} from '../common-search/common-search.reducer';
import {ActiveSearchLink, activeSearchLink} from '~/features/dashboard-search/dashboard-search.consts';
import {IReport} from '@common/reports/reports.consts';
import {setFilterByUser} from '@common/core/actions/users.actions';
export interface DashboardSearchState {
projects: Project[];
@@ -48,8 +49,8 @@ export const searchInitialState: DashboardSearchState = {
};
export const dashboardSearchReducers = [
on(searchActivate, (state) => ({...state, active: true})),
on(searchDeactivate, (state) => ({
on(searchActivate, (state): DashboardSearchState => ({...state, active: true})),
on(searchDeactivate, (state): DashboardSearchState => ({
...state,
active: false,
term: searchInitialState.term,
@@ -57,47 +58,49 @@ export const dashboardSearchReducers = [
scrollIds: null,
resultsCount: null
})),
on(searchSetTerm, (state, action) => ({...state, term: action, forceSearch: action.force, scrollIds: null})),
on(setProjectsResults, (state, action) => ({
on(searchSetTerm, (state, action): DashboardSearchState => ({...state, term: action, forceSearch: action.force, scrollIds: null})),
on(setFilterByUser, (state): DashboardSearchState => ({...state, scrollIds: null})),
on(setProjectsResults, (state, action): DashboardSearchState => ({
...state,
projects: action.scrollId === state.scrollIds?.[activeSearchLink.projects] ? state.projects.concat(action.projects) : action.projects,
scrollIds: {...state.scrollIds, [activeSearchLink.projects]: action.scrollId}
})),
on(setPipelinesResults, (state, action) => ({
on(setPipelinesResults, (state, action): DashboardSearchState => ({
...state,
pipelines: action.scrollId === state.scrollIds?.[activeSearchLink.pipelines] ? state.pipelines.concat(action.pipelines) : action.pipelines,
scrollIds: {...state.scrollIds, [activeSearchLink.pipelines]: action.scrollId}
})),
on(setOpenDatasetsResults, (state, action) => ({
on(setOpenDatasetsResults, (state, action): DashboardSearchState => ({
...state,
openDatasets: action.scrollId === state.scrollIds?.[activeSearchLink.openDatasets] ? state.openDatasets.concat(action.openDatasets) : action.openDatasets,
scrollIds: {...state.scrollIds, [activeSearchLink.openDatasets]: action.scrollId}
})),
on(setExperimentsResults, (state, action) => ({
on(setExperimentsResults, (state, action): DashboardSearchState => ({
...state,
experiments: action.scrollId === state.scrollIds?.[activeSearchLink.experiments] ? state.experiments.concat(action.experiments) : action.experiments,
scrollIds: {...state.scrollIds, [activeSearchLink.experiments]: action.scrollId}
})),
on(setModelsResults, (state, action) => ({
on(setModelsResults, (state, action): DashboardSearchState => ({
...state,
models: action.scrollId === state.scrollIds?.[activeSearchLink.models] ? state.models.concat(action.models) : action.models,
scrollIds: {...state.scrollIds, [activeSearchLink.models]: action.scrollId}
})),
on(setReportsResults, (state, action) => ({
on(setReportsResults, (state, action): DashboardSearchState => ({
...state,
reports: action.scrollId === state.scrollIds?.[activeSearchLink.reports] ? state.reports.concat(action.reports) : action.reports,
scrollIds: {...state.scrollIds, [activeSearchLink.reports]: action.scrollId}
})),
on(setResultsCount, (state, action) => ({...state, resultsCount: action.counts})),
on(clearSearchResults, (state) => ({
on(setResultsCount, (state, action): DashboardSearchState => ({...state, resultsCount: action.counts})),
on(clearSearchResults, (state): DashboardSearchState => ({
...state,
[activeSearchLink.models]: [],
[activeSearchLink.experiments]: [],
[activeSearchLink.pipelines]: [],
[activeSearchLink.projects]: [],
})),
on(searchClear, (state) => ({...state, ...searchInitialState})),
] as ReducerTypes<DashboardSearchState, any>[];
on(searchClear, (state): DashboardSearchState => ({...state, ...searchInitialState})),
] as ReducerTypes<DashboardSearchState, ActionCreator[]>[];
export const dashboardSearchReducer = createReducer(
searchInitialState,

View File

@@ -5,11 +5,9 @@ import {DashboardExperimentsComponent} from './containers/dashboard-experiments/
import {RecentExperimentTableComponent} from './dumb/recent-experiment-table/recent-experiment-table.component';
import {CommonDashboardEffects} from './common-dashboard.effects';
import {CommonSearchModule} from '../common-search/common-search.module';
import {CommonLayoutModule} from '../layout/layout.module';
import {EffectsModule} from '@ngrx/effects';
import {ProjectsSharedModule} from '~/features/projects/shared/projects-shared.module';
import {CommonProjectsModule} from '../projects/common-projects.module';
import {SharedModule} from '~/shared/shared.module';
import {FormsModule} from '@angular/forms';
import {ExperimentCompareSharedModule} from '@common/experiments-compare/shared/experiment-compare-shared.module';
import {ProjectCardComponent} from '@common/shared/ui-components/panel/project-card/project-card.component';
@@ -32,9 +30,7 @@ import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indic
CommonSearchModule,
ProjectsSharedModule,
EffectsModule.forFeature([CommonDashboardEffects]),
CommonLayoutModule,
CommonProjectsModule,
SharedModule,
FormsModule,
ExperimentCompareSharedModule,
ProjectCardComponent,

View File

@@ -4,15 +4,8 @@ import {Task} from '~/business-logic/model/tasks/task';
import {User} from '~/business-logic/model/users/user';
import {setRecentExperiments, setRecentProjects} from './common-dashboard.actions';
export interface IRecentTask {
id: Task['id'];
name?: Task['name'];
export interface IRecentTask extends Omit<Task, 'user' | 'project'> {
user?: User;
type?: Task['type'];
status?: Task['status'];
created?: Task['created'];
started?: Task['started'];
completed?: Task['completed'];
project?: Project;
}
@@ -23,8 +16,8 @@ export interface DashboardState {
// Todo remove selectedProjectId
export const dashboardInitState: DashboardState = {
recentProjects: [],
recentTasks : [],
recentProjects: null,
recentTasks : null,
};
export const commonDashboardReducers = [
@@ -38,5 +31,6 @@ export const commonDashboardReducer = createReducer(
);
export const selectDashboard = createFeatureSelector<DashboardState>('dashboard');
export const selectRecentProjects = createSelector(selectDashboard, (state: DashboardState): Array<Project> => state ? state.recentProjects : []);
export const selectRecentTasks = createSelector(selectDashboard, (state: DashboardState): Array<IRecentTask> => state ? state.recentTasks : []);
export const selectRecentProjects = createSelector(selectDashboard, state => state.recentProjects);
export const selectRecentProjectsCount = createSelector(selectRecentProjects , projects => projects?.length);
export const selectRecentTasks = createSelector(selectDashboard, state => state.recentTasks);

View File

@@ -3,7 +3,9 @@
<ng-content select="[header-buttons]"></ng-content>
</div>
<div class="table-container">
<sm-recent-tasks-table [tasks]="recentTasks"
(taskSelected)="taskSelected($event)">
</sm-recent-tasks-table>
@if (recentTasks) {
<sm-recent-tasks-table [tasks]="recentTasks"
(taskSelected)="taskSelected($event)">
</sm-recent-tasks-table>
}
</div>

View File

@@ -1,10 +1,10 @@
import {Component, Input, OnInit} from '@angular/core';
import {Component, inject, Input} from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { IRecentTask} from '../../common-dashboard.reducer';
import {getRecentExperiments} from '../../common-dashboard.actions';
import { ITask } from '../../../../business-logic/model/al-task';
import {selectCurrentUser} from '../../../core/reducers/users-reducer';
import { ITask } from '~/business-logic/model/al-task';
import {selectCurrentUser} from '@common/core/reducers/users-reducer';
import {filter, take} from 'rxjs/operators';
@Component({
@@ -12,22 +12,20 @@ import {filter, take} from 'rxjs/operators';
templateUrl: './dashboard-experiments.component.html',
styleUrls: ['./dashboard-experiments.component.scss']
})
export class DashboardExperimentsComponent implements OnInit {
export class DashboardExperimentsComponent {
private store = inject(Store);
private router = inject(Router);
@Input() recentTasks: IRecentTask[];
constructor(private store: Store, private router: Router) {
}
ngOnInit() {
constructor() {
this.store.select(selectCurrentUser)
.pipe(filter(user => !!user), take(1))
.subscribe(() => this.store.dispatch((getRecentExperiments())));
}
public taskSelected(task: IRecentTask | ITask) {
// TODO ADD task.id to route
const projectId = task.project ? task.project.id : '*';
return this.router.navigateByUrl('projects/' + projectId + '/experiments/' + task.id);
return this.router.navigate(['projects', projectId, 'experiments', task.id]);
}
}

View File

@@ -4,22 +4,28 @@
<button class="btn btn-link view-all" (click)="router.navigateByUrl('/projects')">VIEW ALL</button>
</div>
<div>
<button *ngIf="(recentProjectsList$ | async).length >= cardsInRow || overflow"
class="btn btn-cml-primary d-flex align-items-center"
data-id="New Project"
(click)="openCreateProjectDialog()">
<i class="al-icon sm al-ico-add me-2"></i>NEW PROJECT
</button>
@if (recentProjectsListCount$() >= cardsInRow || overflow) {
<button
class="btn btn-cml-primary d-flex align-items-center"
data-id="New Project"
(click)="openCreateProjectDialog()">
<i class="al-icon sm al-ico-add me-2"></i>NEW PROJECT
</button>
}
</div>
</div>
<sm-project-card
*ngFor="let project of recentProjectsList$ | async; trackBy: trackById"
[project]="project" (projectCardClicked)="projectCardClicked($event)"
[hideMenu]="true"
></sm-project-card>
<sm-plus-card
*ngIf="(recentProjectsList$ | async).length < cardsInRow"
[folder]="true"
(plusCardClick)="openCreateProjectDialog()"
></sm-plus-card>
@if (recentProjectsList$(); as recentProjectsList) {
@for(project of recentProjectsList; track project.id) {
<sm-project-card
[project]="project" (projectCardClicked)="projectCardClicked($event)"
[hideMenu]="true"
></sm-project-card>
}
@if (recentProjectsList.length < cardsInRow) {
<sm-plus-card
[folder]="true"
(plusCardClick)="openCreateProjectDialog()"
></sm-plus-card>
}
}
</div>

View File

@@ -1,10 +1,20 @@
import {Component, OnInit, Output, EventEmitter, AfterViewInit, ViewChild, ElementRef, OnDestroy} from '@angular/core';
import {
Component,
OnInit,
Output,
EventEmitter,
AfterViewInit,
ViewChild,
ElementRef,
OnDestroy,
inject
} from '@angular/core';
import {Router} from '@angular/router';
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
import {fromEvent, Observable, Subscription} from 'rxjs';
import {Store} from '@ngrx/store';
import {Project} from '~/business-logic/model/projects/project';
import {selectRecentProjects} from '../../common-dashboard.reducer';
import {selectRecentProjects, selectRecentProjectsCount} from '../../common-dashboard.reducer';
import {getRecentProjects} from '../../common-dashboard.actions';
import {ProjectDialogComponent} from '@common/shared/project-dialog/project-dialog.component';
import {resetSelectedProject, setSelectedProjectId} from '@common/core/actions/projects.actions';
@@ -12,40 +22,34 @@ import {selectCurrentUser} from '@common/core/reducers/users-reducer';
import {filter, take, throttleTime} from 'rxjs/operators';
import {isExample} from '@common/shared/utils/shared-utils';
import { CARDS_IN_ROW } from '../../common-dashboard.const';
import {trackById} from '@common/shared/utils/forms-track-by';
@Component({
selector : 'sm-dashboard-projects',
templateUrl: './dashboard-projects.component.html',
styleUrls : ['./dashboard-projects.component.scss']
})
export class DashboardProjectsComponent implements OnInit, AfterViewInit, OnDestroy {
public recentProjectsList$: Observable<Array<Project>>;
export class DashboardProjectsComponent implements AfterViewInit, OnDestroy {
private store = inject(Store);
protected router = inject(Router);
private matDialog = inject(MatDialog);
public recentProjectsList$ = this.store.selectSignal(selectRecentProjects);
public recentProjectsListCount$ = this.store.selectSignal(selectRecentProjectsCount);
private dialog: MatDialogRef<ProjectDialogComponent>;
private sub: Subscription;
readonly cardsInRow = CARDS_IN_ROW;
overflow: boolean;
trackById = trackById;
@Output() width = new EventEmitter<number>();
constructor(
private store: Store,
public router: Router,
private matDialog: MatDialog
) {
this.recentProjectsList$ = this.store.select(selectRecentProjects);
}
@ViewChild('header') header: ElementRef<HTMLDivElement>;
ngOnInit() {
constructor() {
this.store.dispatch(resetSelectedProject());
this.store.select(selectCurrentUser)
.pipe(filter(user => !!user), take(1))
.subscribe(() => this.store.dispatch(getRecentProjects()));
}
@ViewChild('header') header: ElementRef<HTMLDivElement>;
ngAfterViewInit() {
window.setTimeout(() => this.width.emit(this.header.nativeElement.getBoundingClientRect().width));
this.sub = fromEvent(window, 'resize')

View File

@@ -8,7 +8,6 @@ import {combineLatest, Observable, Subscription} from 'rxjs';
import {SearchState, selectSearchQuery} from '../common-search/common-search.reducer';
import {Store} from '@ngrx/store';
import {
selectActiveSearch,
selectDatasetsResults,
selectExperimentsResults,
selectModelsResults,
@@ -28,37 +27,19 @@ 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';
import { Task } from '~/business-logic/model/tasks/task';
@Component({
selector: 'sm-dashboard-search-base',
template: `<sm-search-results-page
*ngIf="activeSearch$ | async"
(projectSelected)="projectCardClicked($event)"
(experimentSelected)="taskSelected($event)"
(modelSelected)="modelSelected($event)"
(pipelineSelected)="pipelineSelected($event)"
(activeLinkChanged)="changeActiveLink($event)"
(reportSelected)="reportSelected($event)"
(openDatasetSelected)="openDatasetCardClicked($event)"
(loadMoreClicked)="loadMore()"
[projectsList]="projectsResults$ | async"
[pipelinesList]="pipelinesResults$ | async"
[datasetsList]="datasetsResults$ | async"
[experimentsList]="experimentsResults$ | async"
[modelsList]="modelsResults$ | async"
[reportsList]="reportsResults$ | async"
[activeLink]="activeLink"
[resultsCount]="resultsCount$ | async">
</sm-search-results-page>`,
template: '',
})
export class DashboardSearchBaseComponent implements OnInit, OnDestroy{
export abstract class DashboardSearchBaseComponent implements OnInit, OnDestroy{
public activeLink = 'projects' as ActiveSearchLink;
private searchSubs;
public searchQuery$: Observable<SearchState['searchQuery']>;
public activeSearch$: Observable<boolean>;
public modelsResults$: Observable<Array<Model>>;
public projectsResults$: Observable<Array<Project>>;
public experimentsResults$: Observable<any>;
public experimentsResults$: Observable<Task[]>;
public searchTerm$: Observable<SearchState['searchQuery']>;
public pipelinesResults$: Observable<Project[]>;
public datasetsResults$: Observable<Project[]>;
@@ -73,7 +54,6 @@ export class DashboardSearchBaseComponent implements OnInit, OnDestroy{
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);

View File

@@ -36,7 +36,7 @@ export class SimpleDatasetVersionContentComponent {
@Input() set data(csv: string) {
const lines = csv?.trimEnd().split('\n') ?? [];
const header = lines.splice(0, 1)[0] ?? '';
const colWidth = (this.ref.nativeElement.getBoundingClientRect().width - 150) / 2 ?? 300;
const colWidth = (this.ref.nativeElement.getBoundingClientRect().width - 150) / 2;
this.columns = header.split(/, ?/)
.map((caption, index) => {
const width = this.colSizes?.[columnIds[index]] ? `${this.colSizes[columnIds[index]]}px` : null;

View File

@@ -1,4 +1,4 @@
@import "src/app/webapp-common/shared/ui-components/styles/variables";
@import "variables";
:host {
.header {

View File

@@ -1,6 +1,7 @@
import {createAction, props} from '@ngrx/store';
import {GroupedHyperParams, MetricOption} from '../reducers/experiments-compare-charts.reducer';
import {MetricValueType} from '@common/experiments-compare/experiments-compare.constants';
import {MetricValueType, SelectedMetricVariant} from '@common/experiments-compare/experiments-compare.constants';
import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult';
export const EXPERIMENTS_COMPARE_SCALARS_GRAPH = 'EXPERIMENTS_COMPARE_SCALARS_GRAPH_';
@@ -13,14 +14,29 @@ export const setMetricsList = createAction(
EXPERIMENTS_COMPARE_SCALARS_GRAPH + 'SET_METRICS_LIST',
props<{ metricsList: MetricOption[] }>()
);
export const setMetricsResults = createAction(
EXPERIMENTS_COMPARE_SCALARS_GRAPH + 'SET_METRICS_RESULTS',
props<{ metricVariantsResults: Array<MetricVariantResult> }>()
);
export const setTasks = createAction(
EXPERIMENTS_COMPARE_SCALARS_GRAPH + 'SET_TASKS',
props<{ tasks: any }>()
);
export const setvalueType = createAction(
export const setValueType = createAction(
EXPERIMENTS_COMPARE_SCALARS_GRAPH + 'SET_VALUE_TYPE',
props<{ valueType: MetricValueType }>()
);
export const setParamsHoverInfo = createAction(
EXPERIMENTS_COMPARE_SCALARS_GRAPH + 'SET_PARAMS_HOVER_INFO',
props<{ paramsHoverInfo: string[] }>()
);
export const setMetricsHoverInfo = createAction(
EXPERIMENTS_COMPARE_SCALARS_GRAPH + 'SET_METRICS_HOVER_INFO',
props<{ metricsHoverInfo: SelectedMetricVariant[] }>()
);
export const setHyperParamsList = createAction(
EXPERIMENTS_COMPARE_SCALARS_GRAPH + 'SET_PARAMS_LIST',
props<{ hyperParams: GroupedHyperParams }>()

View File

@@ -1,9 +1,10 @@
<div *ngIf="graphData?.length > 0" class="buttons">
<button class="btn btn-icon" (click)="downloadImage()"><i class="al-icon al-ico-download"></i></button>
<button class="btn btn-icon" smTooltip="Download" (click)="downloadImage()"><i class="al-icon al-ico-download"></i></button>
</div>
<sm-scatter-plot
[xAxisType]="scalar ? 'linear' : 'category'"
[xAxisLabel]="params?.[0] ?? $any(params)"
[yAxisLabel]="metricName"
[extraHoverInfoParams]="extraHoverInfoParams"
[data]="graphData"
></sm-scatter-plot>

View File

@@ -1,12 +1,14 @@
import {Component, ElementRef, inject, Input, OnChanges} from '@angular/core';
import {Component, ElementRef, inject, Input, OnChanges, SimpleChanges} from '@angular/core';
import {ScatterPlotSeries} from '@common/core/reducers/projects.reducer';
import {
ExtraTask
} from '@common/experiments-compare/dumbs/parallel-coordinates-graph/parallel-coordinates-graph.component';
import {get} from 'lodash-es';
import {get, isEqual} from 'lodash-es';
import {from} from 'rxjs';
import domtoimage from 'dom-to-image';
import {take} from 'rxjs/operators';
import {SelectedMetricVariant} from '@common/experiments-compare/experiments-compare.constants';
import {MetricVariantToPathPipe} from '@common/shared/pipes/metric-variant-to-path.pipe';
@Component({
@@ -15,22 +17,24 @@ import {take} from 'rxjs/operators';
styleUrls: ['./compare-scatter-plot.component.scss']
})
export class CompareScatterPlotComponent implements OnChanges {
public metricVariantToPathPipe = new MetricVariantToPathPipe;
public graphData: ScatterPlotSeries[];
public scalar: boolean;
@Input() metric: string;
@Input() metricName!: string;
@Input() params: string | string[];
@Input() extraHoverInfoParams: string[] = [];
@Input() extraHoverInfoMetrics: SelectedMetricVariant[] = [];
@Input() experiments: ExtraTask[];
private ref = inject(ElementRef);
ngOnChanges(): void {
ngOnChanges(changes:SimpleChanges): void {
this.scalar = true;
if (this.experiments && this.params && this.metric) {
this.graphData = [{
const newGraphData = [{
label: '',
backgroundColor: '#14aa8c',
data: this.experiments
@@ -46,9 +50,18 @@ export class CompareScatterPlotComponent implements OnChanges {
y: get(point.last_metrics, this.metric),
id: point.id,
name: point.name,
extraParamsHoverInfo: this.extraHoverInfoParams.map(param => `${param}: ${get(point.hyperparams, param)?.value}`).concat(
this.extraHoverInfoMetrics.map(metric => {
const metricVar = get(point.last_metrics, this.metricVariantToPathPipe.transform(metric));
return `${metric?.metric}/${metric?.variant}: value: ${metricVar?.value}, min: ${metricVar?.min_value}, max: ${metricVar?.max_value}`;
})
)
};
}),
} as ScatterPlotSeries];
if (!isEqual(newGraphData, this.graphData) || (changes.metricName?.currentValue!== changes.metricName?.previousValue) || (changes.params?.currentValue!== changes.params?.previousValue)) {
this.graphData = newGraphData;
}
}
}

View File

@@ -80,7 +80,7 @@ $extra-header-min-height: 50px;
cursor: default;
.fas {
width: 3px;
width: 6px;
visibility: hidden;
}

View File

@@ -90,7 +90,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
public experimentTags: { [experimentId: string]: string[] } = {};
private timeoutIndex: number;
private originalScrolledElement: EventTarget;
private treeCardBody: HTMLDivElement;
protected treeCardBody: HTMLDivElement;
protected entityType = EntityTypeEnum.experiment;
protected router: Router;
protected store: Store;
@@ -112,7 +112,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
afterResize() {
window.setTimeout(() => {
this.nativeWidth = Math.max(this.treeCardBody?.getBoundingClientRect().width, 410);
this.cdr.detectChanges();
this.cdr.markForCheck();
});
}

View File

@@ -64,10 +64,10 @@
'hide-identical-mode': hideIdenticalFields
}" data-id="selectedRowHighlighter">
<div>
<pre *ngIf="(node.data.value !== undefined) || (node.data.existOnOrigin && node.data.existOnCompared)"
[class.no-ellipsis]="((node.data.key | hideHash) + node.data.value).length < 45"
<pre #row *ngIf="(node.data.value !== undefined) || (node.data.existOnOrigin && node.data.existOnCompared)"
[class.no-ellipsis]="(nativeWidth -2 > row.scrollWidth) && (row.scrollWidth === row.clientWidth)"
[class.with-ellipsis]="showEllipsis"
[style.width.px]="showEllipsis ? nativeWidth - 55 - node.level * 20 : null" data-id="diffDataRow"
[style.width.px]="showEllipsis ? nativeWidth - 45 - node.level * 20 : null" data-id="diffDataRow"
><ng-container
*ngIf="!!node.data.value?.dataDictionary && !!node.data.value?.link; else simple">{{node.data.key |
hideHash}}<span

View File

@@ -51,6 +51,7 @@ export class ExperimentCompareDetailsComponent extends ExperimentCompareBase imp
this.resetComponentState(experiments);
this.calculateTree(experiments);
this.nativeWidth = Math.max(this.treeCardBody?.getBoundingClientRect().width, 410);
});
}

View File

@@ -1,103 +1,83 @@
<div class="list-container light-theme">
<!--####### Metrics Autocomplete ######-->
<div smClickStopPropagation class="metrics-container" tabindex="1" (blur)="listOpen = false">
<div class="metric-title">Performance Metric</div>
<div class="metrics-search" (click)="openList()">
<input #searchMetric
type="text"
(keydown.escape)="clearMetricSearch(); searchMetric.value= ''"
placeholder="Search metric"
data-id="searchField"
[smTooltip]="selectedMetric?.name"
[matTooltipShowDelay]="500"
[value]="(!metrics || listOpen) ? '' : selectedMetric?.name"
(input)="updateMetricsList($event)"
>
<i *ngIf="searchMetric.value.length === 0" class="fa fa-search pe-2"></i>
<i *ngIf="searchMetric.value.length > 0" class="fa fa-times pointer pe-2" (click)="clearMetricSearchAndSelected(); searchMetric.value= ''"></i>
</div>
<mat-radio-group
*ngIf="!listOpen"
class="value-types"
[value]="valueType"
[disabled]="searchMetric.value=== ''"
(change)="valueTypeChange($event)">
<mat-radio-button class="sm" [value]="'value'">LAST</mat-radio-button>
<mat-radio-button class="sm" [value]="'min_value'">MIN</mat-radio-button>
<mat-radio-button class="sm" [value]="'max_value'">MAX</mat-radio-button>
</mat-radio-group>
<div class="metric-list" [ngClass]="{'metric-list--show': listOpen}">
<mat-expansion-panel *ngFor="let metricGroup of metricsOptions; trackBy: trackMetricByFn"
class="metric-list__panel"
[expanded]="listOpen && searchMetric.value.length > 0"
togglePosition="before">
<mat-expansion-panel-header (click)="searchMetric.focus()" class="metric-list__header" expandedHeight="40px" collapsedHeight="40px">
<mat-panel-title class="metric-list__title">
{{metricGroup.metricName}}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div *ngFor="let variant of metricGroup.variants; trackBy: trackVariantByFn"
class="metric-list__item"
[class.selected]="$any(variant).value.name === selectedMetric?.name"
(click)="metricSelected($any(variant))">
<span class="ellipsis">{{$any(variant).name}}</span>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<hr class="separate-margins">
<!--####### Hyper Params Checked List ######-->
<sm-grouped-checked-filter-list
titleText="Parameters"
[itemsList]="hyperParams"
[selectedItemsList]="selectedHyperParams"
[selectFilteredItems]="selectShowIdenticalHyperParams$ | async"
[selectedItemsListMapper]="selectedItemsListMapper"
selectedItemsListPrefix=""
[limitSelection]="50"
[single]="scatter"
(selectedItems)="selectedParamsChanged($event)"
(clearSelection)="clearSelection()"
>
<mat-slide-toggle
(change)="showIdenticalParamsToggled()"
[checked]="!showIdenticalParamsActive">Hide identical fields
</mat-slide-toggle>
</sm-grouped-checked-filter-list>
@if (scatter) {
<div class="metric-title">Plot axes</div>
<div class="label">Y-axis</div>
<sm-metric-variant-selector [title]="scatter? 'Metric' :'Select Performance Metric'" class="param-selector"
[selectedMetricVariants]="selectedMetric? [selectedMetric]: []"
[metricVariants]="metricsResults$ | async"
(selectMetricVariant)="metricVariantSelected($event)"
(clearSelection)="clearMetricsSelection()"
></sm-metric-variant-selector>
} @else {
<div class="metric-title">Coordinates</div>
<sm-metric-variant-selector class="param-selector" [title]="'Performance Metrics'"
[selectedMetricVariants]="selectedMetrics" [multiSelect]="true" [skipValueType]="false"
[metricVariants]="metricsResults$ | async"
(selectMetricVariant)="multiMetricVariantSelected($event)"
(removeMetric)="multiMetricVariantSelected({addCol: false, variant: $event, valueType:$event.valueType})"
(clearSelection)="clearMetricsSelection()"
></sm-metric-variant-selector>
}
@if (scatter) {
<div class="label">X-axis</div>
}
<sm-param-selector class="param-selector"
[itemsList]="hyperParams"
[title]="'Select Parameters'"
[selectedHyperParams]="selectedHyperParams"
[single]="scatter"
[selectFilteredItems]="selectHideIdenticalHyperParams$ | async"
[selectedItemsListMapper]="selectedItemsListMapper"
(selectedItems)="selectedParamsChanged($event)"
(clearSelection)="clearParamsSelection()"></sm-param-selector>
@if (scatter) {
<hr class="separate-margins">
<div class="metric-title">Additional data point information</div>
<sm-metric-variant-selector class="param-selector"
[title]="'Select Metrics'"
[selectedMetricVariants]="(selectedMetricsHoverInfo$ | async)"
[multiSelect]="true"
[skipValueType]="true"
[metricVariants]="metricsResults$ | async"
(selectMetricVariant)="metricVariantForHoverSelected($event)"
(removeMetric)="metricVariantForHoverSelected({addCol: false, variant: $event, valueType:null})"
(clearSelection)="clearMetricsSelectionForHover()"
></sm-metric-variant-selector>
<sm-param-selector class="param-selector"
[title]="'Select Parameters'"
[itemsList]="hyperParams"
[selectedHyperParams]="selectedParamsHoverInfo$ | async"
[single]="false"
[selectFilteredItems]="selectHideIdenticalHyperParams$ | async"
[selectedItemsListMapper]="selectedItemsListMapper"
(selectedItems)="selectedParamsForHoverChanged($event)"
(clearSelection)="clearParamsForHoverSelection()"></sm-param-selector>
}
</div>
<div class="graphs-container h-100">
<ng-container
*ngIf="(experiments$ | async).length > 1 && selectedHyperParams?.length > 0 && !!selectedMetric; else no_data"
>
<sm-compare-scatter-plot
*ngIf="scatter; else parallel"
[params]="selectedHyperParams"
[metric]="selectedMetric.path + '.' + valueType"
[metricName]="selectedMetric.name"
[experiments]="experiments$ | async"
></sm-compare-scatter-plot>
<ng-template #parallel>
<sm-parallel-coordinates-graph
*ngIf="plotlyReady$ | async"
[experiments]="experiments$ | async"
[parameters]="selectedHyperParams"
[metric]="selectedMetric"
[metricValueType]="valueType"
(createEmbedCode)="createEmbedCode($event)"
></sm-parallel-coordinates-graph>
</ng-template>
</ng-container>
<ng-template #no_data>
<div class="d-flex align-items-center justify-content-center flex-column h-100 no-data">
<div class="al-icon al-ico-no-data-graph"></div>
<h4 class="no-data-title">NO DATA TO SHOW</h4>
<div>Please select parameters & metric</div>
</div>
</ng-template>
@if ((experiments$ | async).length > 1) {
@if (scatter && !!selectedMetric && selectedHyperParams?.length > 0) {
<sm-compare-scatter-plot [params]="selectedHyperParams"
[metric]="selectedMetric | metricVariantToPath: true"
[metricName]="selectedMetric | metricVariantToName: true"
[experiments]="experiments$ | async"
[extraHoverInfoParams]="selectedParamsHoverInfo$ |async"
[extraHoverInfoMetrics]="selectedMetricsHoverInfo$ | async"></sm-compare-scatter-plot>
}
@else if (!scatter && selectedMetrics.length > 0 && selectedHyperParams?.length > 0) {
@if (plotlyReady$ | async) {
<sm-parallel-coordinates-graph [experiments]="experiments$ | async"
[metrics]="selectedMetrics"
[parameters]="selectedHyperParams"
(createEmbedCode)="createEmbedCode($event)"></sm-parallel-coordinates-graph>
}
} @else {
<div class="d-flex align-items-center justify-content-center flex-column h-100 no-data">
<div class="al-icon al-ico-no-data-graph"></div>
<h4 class="no-data-title">NO DATA TO SHOW</h4>
<div>Please select parameters & metrics</div>
</div>
}
}
</div>

View File

@@ -5,6 +5,7 @@
height: 50%;
flex-grow: 1;
::ng-deep .mat-expansion-panel-header.mat-expansion-toggle-indicator-before {
.mat-expansion-indicator {
margin: 0 12px 0 2px;
@@ -19,6 +20,8 @@
width: 360px;
height: 100%;
border-right: 1px solid #efefef;
overflow-y: auto;
overflow-x: hidden;
input {
padding-left: 0;
@@ -28,6 +31,7 @@
::ng-deep .mat-expansion-panel-body {
padding: 0 12px 0 24px;
}
::ng-deep sm-search {
margin-top: 12px;
padding-right: 24px;
@@ -76,9 +80,17 @@
}
.metric-title {
font-size: 16px;
text-transform: uppercase;
font-size: 12px;
font-weight: 500;
color: $blue-400;
padding: 12px 24px 6px 0;
border-bottom: 1px solid $blue-100;
margin: 0 24px 12px;
}
.label{
padding-left: 24px;
color: $blue-400;
}
.metrics-search {
@@ -100,7 +112,7 @@
}
.separate-margins {
margin: 24px 0 20px 0;
margin: 12px 0 6px 0;
}
.metric-list {
@@ -189,9 +201,13 @@
font-size: 140px;
width: 140px;
height: auto;
color: rgba(0,0,0,0.1);
color: rgba(0, 0, 0, 0.1);
}
sm-grouped-checked-filter-list {
margin: 0 0 0 24px;
}
.param-selector {
padding: 0 24px 12px;
}

View File

@@ -1,46 +1,49 @@
import {ChangeDetectorRef, Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {combineLatest, Observable, Subscription} from 'rxjs';
import { Store} from '@ngrx/store';
import {debounceTime, distinctUntilChanged, filter, map, take, withLatestFrom} from 'rxjs/operators';
import {selectRouterParams, selectRouterQueryParams} from '@common/core/reducers/router-reducer';
import {has} from 'lodash-es';
import {combineLatest, Observable, Subscription, take} from 'rxjs';
import {Store} from '@ngrx/store';
import {debounceTime, distinctUntilChanged, filter} from 'rxjs/operators';
import {selectRouterQueryParams} from '@common/core/reducers/router-reducer';
import {flatten, has, isArray, isEqual} from 'lodash-es';
import {setExperimentSettings, setSelectedExperiments} from '../../actions/experiments-compare-charts.actions';
import {
selectCompareIdsFromRoute,
selectHideIdenticalFields,
selectScalarsGraphHyperParams,
selectScalarsGraphMetrics,
selectScalarsGraphShowIdenticalHyperParams,
selectScalarsGraphMetricsResults,
selectScalarsGraphTasks,
selectScalarsMetricsHoverInfo,
selectScalarsParamsHoverInfo,
selectSelectedSettingsHyperParams,
selectSelectedSettingsHyperParamsHoverInfo,
selectSelectedSettingsMetric,
selectSelectedSettingsValueType
selectSelectedSettingsMetrics,
selectSelectedSettingsMetricsHoverInfo
} from '../../reducers';
import {
getExperimentsHyperParams,
setMetricsHoverInfo,
setParamsHoverInfo,
setShowIdenticalHyperParams,
} from '../../actions/experiments-compare-scalars-graph.actions';
import {
GroupedHyperParams,
MetricOption,
VariantOption
} from '../../reducers/experiments-compare-charts.reducer';
import {MatRadioChange} from '@angular/material/radio';
import {GroupedHyperParams, MetricOption} from '../../reducers/experiments-compare-charts.reducer';
import {selectPlotlyReady} from '@common/core/reducers/view.reducer';
import {ExtFrame} from '@common/shared/single-graph/plotly-graph-base';
import {RefreshService} from '@common/core/services/refresh.service';
import {MetricValueType, SelectedMetric} from '@common/experiments-compare/experiments-compare.constants';
import {MetricValueType, SelectedMetricVariant} from '@common/experiments-compare/experiments-compare.constants';
import {ReportCodeEmbedService} from '~/shared/services/report-code-embed.service';
import {ActivatedRoute, Router} from '@angular/router';
import {
ExtraTask
} from '@common/experiments-compare/dumbs/parallel-coordinates-graph/parallel-coordinates-graph.component';
import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult';
import {
SelectionEvent
} from '@common/experiments/dumb/select-metric-for-custom-col/select-metric-for-custom-col.component';
import {MetricResultToSelectedMetricPipe} from '@common/shared/pipes/metric-result-to-selected-metric.pipe';
import {MetricVariantToPathPipe} from '@common/shared/pipes/metric-variant-to-path.pipe';
export const _filter = (opt: VariantOption[], value: string): VariantOption[] => {
const filterValue = value.toLowerCase();
return opt.filter(item => item.name.toLowerCase().includes(filterValue));
};
@Component({
selector: 'sm-experiment-compare-hyper-params-graph',
templateUrl: './experiment-compare-hyper-params-graph.component.html',
@@ -49,36 +52,47 @@ export const _filter = (opt: VariantOption[], value: string): VariantOption[] =>
export class ExperimentCompareHyperParamsGraphComponent implements OnInit, OnDestroy {
private subs = new Subscription();
public selectShowIdenticalHyperParams$: Observable<boolean>;
public selectHideIdenticalHyperParams$: Observable<boolean>;
public hyperParams$: Observable<GroupedHyperParams>;
public metrics$: Observable<MetricOption[]>;
public selectedHyperParams$: Observable<string[]>;
private selectedMetric$: Observable<SelectedMetric>;
public metricsOptions$: Observable<MetricOption[]>;
public selectedHyperParamsSettings$: Observable<string[]>;
public experiments$: Observable<ExtraTask[]>;
public graphs: { [key: string]: ExtFrame };
public selectedHyperParams: string[];
public selectedMetric: SelectedMetric;
public selectedHyperParams: string[] =[];
public selectedMetric: SelectedMetricVariant;
public hyperParams: { [section: string]: any };
public showIdenticalParamsActive: boolean;
public plotlyReady$ = this.store.select(selectPlotlyReady);
public metricResultToSelectedMetricPipe = new MetricResultToSelectedMetricPipe;
public metricVariantToPathPipe = new MetricVariantToPathPipe;
public metrics: MetricOption[];
public metricsOptions: MetricOption[];
public listOpen = true;
private initView = true;
private taskIds: string[];
public metricValueType$: Observable<MetricValueType>;
public valueType: 'min_value' | 'max_value' | 'value';
public scatter: boolean;
private settingsKey: string;
public metricsResults$: Observable<MetricVariantResult[]>;
public selectedMetricSettings$: Observable<SelectedMetricVariant>;
public selectedParamsHoverInfo$: Observable<string[]>;
public selectedMetricsHoverInfo$: Observable<SelectedMetricVariant[]>;
private: string[] = [];
private selectedParamsHoverInfo: string[];
private selectedMetricsHoverInfo: SelectedMetricVariant[];
private compareIdsFromRoute$: Observable<string>;
private selectedHyperParamsHoverInfoSettings$: Observable<Array<string>>;
private selectedMetricsHoverInfoSettings$: Observable<SelectedMetricVariant[]>;
private selectedMetricsSettings$: Observable<SelectedMetricVariant[]>;
private routeWasLoaded: boolean;
private settingsLoaded: boolean;
public selectedItemsListMapper(data) {
return data;
}
@ViewChild('searchMetric') searchMetricRef: ElementRef;
selectedMetrics: SelectedMetricVariant[] = [];
@HostListener('document:click', [])
clickOut() {
@@ -92,43 +106,40 @@ export class ExperimentCompareHyperParamsGraphComponent implements OnInit, OnDes
}
constructor(private store: Store,
private route: ActivatedRoute,
private router: Router,
private refresh: RefreshService,
private reportEmbed: ReportCodeEmbedService,
private cdr: ChangeDetectorRef) {
this.metrics$ = this.store.select(selectScalarsGraphMetrics);
private route: ActivatedRoute,
private router: Router,
private refresh: RefreshService,
private reportEmbed: ReportCodeEmbedService,
private cdr: ChangeDetectorRef) {
this.metricsOptions$ = this.store.select(selectScalarsGraphMetrics);
this.metricsResults$ = this.store.select(selectScalarsGraphMetricsResults);
this.hyperParams$ = this.store.select(selectScalarsGraphHyperParams);
this.selectedHyperParams$ = this.store.select(selectSelectedSettingsHyperParams);
this.selectedMetric$ = this.store.select(selectSelectedSettingsMetric);
this.selectShowIdenticalHyperParams$ = this.store.select(selectScalarsGraphShowIdenticalHyperParams);
this.selectedHyperParamsSettings$ = this.store.select(selectSelectedSettingsHyperParams);
this.selectedHyperParamsHoverInfoSettings$ = this.store.select(selectSelectedSettingsHyperParamsHoverInfo);
this.selectedParamsHoverInfo$ = this.store.select(selectScalarsParamsHoverInfo);
this.selectedMetricsHoverInfo$ = this.store.select(selectScalarsMetricsHoverInfo);
this.selectedMetricSettings$ = this.store.select(selectSelectedSettingsMetric);
this.selectedMetricsSettings$ = this.store.select(selectSelectedSettingsMetrics);
this.selectedMetricsHoverInfoSettings$ = this.store.select(selectSelectedSettingsMetricsHoverInfo);
this.selectHideIdenticalHyperParams$ = this.store.select(selectHideIdenticalFields);
this.experiments$ = this.store.select(selectScalarsGraphTasks);
this.metricValueType$ = this.store.select(selectSelectedSettingsValueType);
this.scatter = this.route.snapshot.data?.scatter;
this.settingsKey = this.scatter ? 'scatter-param-graph' : 'hyper-param-graph';
this.compareIdsFromRoute$ = this.store.select(selectCompareIdsFromRoute);
}
ngOnInit() {
this.subs.add(this.metrics$.pipe(
filter(metrics => !!metrics)
).subscribe((metrics) => {
this.metrics = metrics;
this.metricsOptions = [...metrics];
}));
this.subs.add(combineLatest([this.hyperParams$, this.selectShowIdenticalHyperParams$])
this.scatter = this.route.snapshot.data?.scatter;
this.subs.add(combineLatest([this.hyperParams$, this.selectHideIdenticalHyperParams$])
.pipe(
filter(([allParams]) => !!allParams),
)
.subscribe(([allParams, showIdentical]) => {
this.showIdenticalParamsActive = showIdentical;
.subscribe(([allParams, hideIdentical]) => {
this.showIdenticalParamsActive = !hideIdentical;
this.hyperParams = Object.entries(allParams)
.reduce((acc, [sectionKey, params]) => {
const section = Object.keys(params)
.sort((a, b) => a.toLowerCase() > b.toLowerCase() ? 1 : -1)
.reduce((acc2, paramKey) => {
if (showIdentical || params[paramKey]) {
if (!hideIdentical || params[paramKey]) {
acc2[paramKey] = true;
}
return acc2;
@@ -139,146 +150,144 @@ export class ExperimentCompareHyperParamsGraphComponent implements OnInit, OnDes
return acc;
}, {});
const selectedHyperParams = this.selectedHyperParams?.filter(selectedParam => has(this.hyperParams, selectedParam));
selectedHyperParams && this.updateServer(this.selectedMetric, selectedHyperParams, this.valueType)
selectedHyperParams && this.updateServer(this.selectedMetric, selectedHyperParams);
this.cdr.detectChanges();
}));
this.subs.add(combineLatest([this.metrics$, this.hyperParams$]).pipe(
this.subs.add(combineLatest([this.metricsOptions$, this.hyperParams$, this.store.select(selectRouterQueryParams)]).pipe(
debounceTime(0),
filter(([metircs, hyperparams]) => metircs?.length > 0 && Object.keys(hyperparams || {})?.length > 0),
withLatestFrom(this.store.select(selectRouterQueryParams))
).subscribe(([[metircs], queryParams]) => {
filter(([metrics, hyperparams,]) => ((metrics?.length > 0 || Object.keys(hyperparams || {})?.length > 0) && this.settingsLoaded)),
).subscribe(([metrics, , queryParams]) => {
this.routeWasLoaded = true;
const flatVariants = flatten(metrics.map(m => m.variants)).map(mv => mv.value);
if (queryParams.metricPath) {
const selectedMetric = metircs.map(a => a.variants).flat(2).find(variant => variant.value.path === queryParams.metricPath)?.value ?? null;
const params = Array.isArray(queryParams.params) ? queryParams.params : [queryParams.params];
this.updateServer(selectedMetric, params, queryParams.valueType, true);
this.listOpen = false;
this.cdr.detectChanges();
if (this.scatter) {
this.selectedMetric = {
metric: queryParams.metricName.split('/')[0],
variant: queryParams.metricName.split('/')[1],
metric_hash: queryParams.metricPath.split('.')[0],
variant_hash: queryParams.metricPath.split('.')[1],
valueType: queryParams.valueType ?? 'value'
};
} else {
// backwards compatibility 3.21-> 3.20
this.selectedMetrics= [{
metric: queryParams.metricName.split('/')[0],
variant: queryParams.metricName.split('/')[1],
metric_hash: queryParams.metricPath.split('.')[0],
variant_hash: queryParams.metricPath.split('.')[1],
valueType: queryParams.valueType ?? 'value'
}]
}
}
else {
this.selectedMetric = undefined;
}
if (queryParams.metricVariants !== undefined) {
this.selectedMetrics = !queryParams.metricVariants ? [] : queryParams.metricVariants?.split(',').map(path => {
const variant = flatVariants.find(m => path.startsWith(m.path));
return {
metric: variant.name.split('/')[0],
variant: variant.name.split('/')[1],
metric_hash: variant.path.split('.')[0],
variant_hash: variant.path.split('.')[1],
valueType: path.split('.')[2] ?? 'value'
};
});
}
if (queryParams.params) {
this.selectedHyperParams = Array.isArray(queryParams.params) ? queryParams.params : [queryParams.params];
}
this.cdr.detectChanges();
}));
this.subs.add(this.store.select(selectRouterParams).pipe(
map(params => params?.ids),
distinctUntilChanged(),
filter(ids => !!ids),
this.subs.add(this.compareIdsFromRoute$.pipe(
filter((ids) => !!ids),
distinctUntilChanged()
)
.subscribe((ids) => {
this.taskIds = ids.split(',');
this.store.dispatch(setSelectedExperiments({selectedExperiments: [this.settingsKey].concat(this.taskIds)}));
this.store.dispatch(setSelectedExperiments({selectedExperiments: [this.scatter ? 'scatter-param-graph' : 'hyper-param-graph'].concat(this.taskIds)}));
this.store.dispatch(getExperimentsHyperParams({experimentsIds: this.taskIds, scatter: this.scatter}));
}));
this.subs.add(this.refresh.tick
.pipe(filter(auto => auto !== null))
.subscribe(autoRefresh =>
this.store.dispatch(getExperimentsHyperParams({experimentsIds: this.taskIds, autoRefresh}))
));
this.subs.add(this.metricValueType$.pipe(take(1))
.subscribe(valueType => this.valueType = valueType));
this.subs.add(this.selectedHyperParams$.pipe(take(1))
.subscribe(p => this.selectedHyperParams = p));
this.subs.add(this.selectedMetric$.pipe(take(1))
.subscribe(selectedMetric => this.selectedMetric = selectedMetric?.path ? {...selectedMetric} : null));
this.listOpen = false;
if (!this.selectedMetric) {
this.listOpen = true;
window.setTimeout(() => {
this.searchMetricRef.nativeElement.focus();
this.initView = false;
this.cdr.detectChanges();
}, 200);
}
this.subs.add(combineLatest([this.selectedMetricSettings$, this.selectedHyperParamsSettings$,
this.selectedMetricsHoverInfoSettings$, this.selectedHyperParamsHoverInfoSettings$, this.selectedMetricsSettings$]).pipe(
take(1)
)
.subscribe(([selectedMetric, selectedParams, selectedMetricsHoverInfo, selectedParamsHoverInfo, selectedMetrics]) => {
selectedMetricsHoverInfo?.length > 0 && this.store.dispatch(setMetricsHoverInfo({metricsHoverInfo: selectedMetricsHoverInfo as SelectedMetricVariant[]}));
selectedParamsHoverInfo?.length > 0 && this.store.dispatch(setParamsHoverInfo({paramsHoverInfo: selectedParamsHoverInfo}));
this.updateServer(selectedMetric, selectedParams, false, null, selectedMetrics, true);
this.settingsLoaded = true;
}));
this.subs.add(this.selectedParamsHoverInfo$
.subscribe(p => this.selectedParamsHoverInfo = p));
this.subs.add(this.selectedMetricsHoverInfo$
.subscribe(p => this.selectedMetricsHoverInfo = p));
}
ngOnDestroy() {
this.saveSettingsState();
this.subs.unsubscribe();
this.saveSettingsState();
this.clearParamsForHoverSelection();
this.store.dispatch(setMetricsHoverInfo({metricsHoverInfo: []}));
}
private _filterGroup(value: string): MetricOption[] {
if (value) {
return this.metrics
.map(group => ({metricName: group.metricName, variants: _filter(group.variants, value)}))
.filter(group => group.variants.length > 0);
}
return this.metrics;
}
metricSelected(metric: VariantOption) {
this.updateServer(metric.value, this.selectedHyperParams, this.valueType);
this.listOpen = false;
metricVariantSelected($event?: SelectionEvent) {
this.updateServer({...$event.variant, valueType: $event.valueType}, this.selectedHyperParams);
}
selectedParamsChanged({param}) {
if(this.scatter) {
this.updateServer(this.selectedMetric, [param], this.valueType);
if (this.scatter) {
this.updateServer(this.selectedMetric, this.selectedHyperParams.includes(param) ? [] : [param]);
} else {
const newSelectedParamsList = this.selectedHyperParams.includes(param) ? this.selectedHyperParams.filter(i => i !== param) : [...this.selectedHyperParams, param];
this.updateServer(this.selectedMetric, newSelectedParamsList, this.valueType);
this.updateServer(this.selectedMetric, newSelectedParamsList);
}
}
clearSelection() {
this.updateServer(this.selectedMetric, [], this.valueType);
this.updateServer(this.selectedMetric, []);
}
showIdenticalParamsToggled() {
this.store.dispatch(setShowIdenticalHyperParams());
}
updateServer(selectedMetric: SelectedMetric, selectedParams: string[], valueType, skipNavigation?: boolean) {
!skipNavigation && this.router.navigate([], {
updateServer(selectedMetric?: SelectedMetricVariant, selectedParams?: string[], skipNavigation?: boolean, valueType?: SelectedMetricVariant['valueType'], selectedMetrics?: SelectedMetricVariant[], force?: boolean) {
(this.routeWasLoaded || force) && !skipNavigation && this.router.navigate([], {
queryParams: {
metricPath: selectedMetric?.path || undefined,
metricName: selectedMetric?.name || undefined,
params: selectedParams,
valueType: valueType || 'value'
metricPath: selectedMetric ? `${selectedMetric?.metric_hash}.${selectedMetric?.variant_hash}` : undefined,
...(isArray(selectedMetrics) && {metricVariants: selectedMetrics.map(mv => this.metricVariantToPathPipe.transform(mv, true)).toString()}),
metricName: selectedMetric ? `${selectedMetric?.metric}/${selectedMetric?.variant}` : undefined,
...(selectedParams && {params: selectedParams}),
valueType: selectedMetric ? selectedMetric?.valueType || valueType || 'value' : undefined
},
queryParamsHandling: 'merge',
replaceUrl: true
});
this.selectedMetric = selectedMetric;
this.selectedHyperParams = selectedParams;
this.valueType = valueType;
}
updateMetricsList(event: Event) {
this.metricsOptions = this._filterGroup((event.target as HTMLInputElement).value);
}
clearMetricSearchAndSelected() {
this.updateServer(null, this.selectedHyperParams, this.valueType);
this.selectedMetric = null;
this.metricsOptions = this._filterGroup('');
}
clearMetricSearch() {
this.metricsOptions = this._filterGroup('');
}
openList() {
this.listOpen = true;
}
trackMetricByFn(index: number, item: MetricOption): string {
return item.metricName;
}
trackVariantByFn(index: number, item: VariantOption): string {
// TODO: validate with @nirla
return item.value.path;
}
valueTypeChange($event: MatRadioChange) {
this.valueType = $event.value;
this.updateServer(this.selectedMetric, this.selectedHyperParams, $event.value, false);
}
createEmbedCode(event: { tasks: string[]; valueType: MetricValueType; metrics?: string[]; variants?: string[]; domRect: DOMRect }) {
createEmbedCode(event: {
tasks: string[];
valueType: MetricValueType;
metrics?: string[];
variants?: string[];
domRect: DOMRect
}) {
this.reportEmbed.createCode({
type: 'parcoords',
objects: event.tasks,
@@ -289,12 +298,51 @@ export class ExperimentCompareHyperParamsGraphComponent implements OnInit, OnDes
saveSettingsState() {
this.store.dispatch(setExperimentSettings({
id: [this.settingsKey].concat(this.taskIds),
id: [this.scatter ? 'scatter-param-graph' : 'hyper-param-graph'].concat(this.taskIds),
changes: {
selectedMetric: this.selectedMetric,
selectedMetrics: this.selectedMetrics,
selectedHyperParams: this.selectedHyperParams,
valueType: this.valueType
selectedParamsHoverInfo: this.selectedParamsHoverInfo,
selectedMetricsHoverInfo: this.selectedMetricsHoverInfo
}
}));
}
clearParamsSelection() {
this.updateServer(this.selectedMetric, []);
}
clearMetricsSelection() {
this.updateServer(null, undefined,false, undefined, [] );
}
selectedParamsForHoverChanged({param}) {
const newSelectedParamsList = this.selectedParamsHoverInfo.includes(param) ? this.selectedParamsHoverInfo.filter(i => i !== param) : [...this.selectedParamsHoverInfo, param];
this.store.dispatch(setParamsHoverInfo({paramsHoverInfo: newSelectedParamsList}));
}
clearParamsForHoverSelection() {
this.store.dispatch(setParamsHoverInfo({paramsHoverInfo: []}));
}
metricVariantForHoverSelected($event: SelectionEvent) {
const newSelectedVariantList = $event.addCol ? [...this.selectedMetricsHoverInfo, $event.variant] : [...this.selectedMetricsHoverInfo.filter(metricVar => !isEqual(metricVar, $event.variant))];
this.store.dispatch(setMetricsHoverInfo({metricsHoverInfo: newSelectedVariantList as SelectedMetricVariant[]}));
}
multiMetricVariantSelected($event: SelectionEvent) {
if ($event.valueType) {
const selectedVariant = {...$event.variant, valueType: $event.valueType};
const newSelectedVariantList = $event.addCol ? [...this.selectedMetrics, selectedVariant] :
[...this.selectedMetrics.filter(metricVar => !isEqual(metricVar, selectedVariant))];
this.updateServer(null, this.selectedHyperParams, false, undefined, newSelectedVariantList);
}
}
clearMetricsSelectionForHover() {
this.store.dispatch(setMetricsHoverInfo({metricsHoverInfo: [] as SelectedMetricVariant[]}));
}
}

View File

@@ -7,7 +7,7 @@ import {createMultiSingleValuesChart, mergeMultiMetrics, mergeMultiMetricsGroupe
import {getMultiScalarCharts, getMultiSingleScalars, resetExperimentMetrics, setExperimentMetricsSearchTerm, setExperimentSettings, setScalarsHoverMode, setSelectedExperiments} from '../../actions/experiments-compare-charts.actions';
import {selectCompareTasksScalarCharts, selectExperimentMetricsSearchTerm, selectMultiSingleValues, selectScalarsHoverMode, selectSelectedExperiments, selectSelectedExperimentSettings} from '../../reducers';
import {ScalarKeyEnum} from '~/business-logic/model/events/scalarKeyEnum';
import {GroupByCharts, groupByCharts} from '@common/experiments/reducers/experiment-output.reducer';
import {GroupByCharts, groupByCharts} from '@common/experiments/actions/common-experiment-output.actions';
import {ExtFrame} from '@common/shared/single-graph/plotly-graph-base';
import {RefreshService} from '@common/core/services/refresh.service';
import {selectRouterParams} from '@common/core/reducers/router-reducer';
@@ -81,7 +81,7 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
smoothType: smoothTypeEnum.exponential,
xAxisType: ScalarKeyEnum.Iter,
selectedMetricsScalar: []
}
};
private originalSettings: ExperimentCompareSettings;
public minimized = false;
public splitSize$: Observable<number>;
@@ -115,7 +115,7 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
distinctUntilChanged()
);
this.routerParams$ = this.store.select(selectRouterParams).pipe(
filter(params => !!params.ids),
filter(params => params.ids !== undefined),
distinctUntilChanged()
);
@@ -135,7 +135,7 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
if (singleValues) {
const visibles = this.graphsComponent?.singleValueGraph.first?.chart.data.reduce((curr, data) => {
curr[data.task] = data.visible
curr[data.task] = data.visible;
return curr;
}, {}) ?? {};
const singleValuesData = createMultiSingleValuesChart(singleValues, visibles);
@@ -150,15 +150,15 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
this.store.select(selectMetricVariants),
this.store.select(selectSelectedExperiments)))
.subscribe(([params, metrics, selectedExperiments]) => {
if (!this.taskIds || this.taskIds.join(',') !== params.ids) {
const previousTaskIds = this.taskIds;
this.taskIds = params.ids.split(',').sort();
this.store.dispatch(setSelectedExperiments({selectedExperiments: this.taskIds}));
if (metrics.length === 0 || (metrics.length > 0 && previousTaskIds !== undefined) || !isEqual(selectedExperiments, this.taskIds)) {
this.store.dispatch(getCustomMetricsPerType({ids: this.taskIds, metricsType: EventTypeEnum.TrainingStatsScalar, isModel: this.entityType === EntityTypeEnum.model}));
if (!this.taskIds || this.taskIds.join(',') !== params.ids) {
const previousTaskIds = this.taskIds;
this.taskIds = params.ids.split(',').sort().filter(id => !!id);
this.store.dispatch(setSelectedExperiments({selectedExperiments: this.taskIds}));
if ((metrics.length === 0 || (metrics.length > 0 && previousTaskIds !== undefined) || !isEqual(selectedExperiments, this.taskIds))) {
this.store.dispatch(getCustomMetricsPerType({ids: this.taskIds, metricsType: EventTypeEnum.TrainingStatsScalar, isModel: this.entityType === EntityTypeEnum.model}));
}
}
}
}));
}));
this.subs.add(this.store.select(selectCompareSelectedMetrics('scalars'))
.pipe(
@@ -178,7 +178,7 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
const newSelectedMetricsScalar = selectedMetrics.filter(m => !m.hidden).map(m => m.metricName + m.variantName);
const VariantWasAdded = newSelectedMetricsScalar?.length > this.settings.selectedMetricsScalar?.length;
this.settings.selectedMetricsScalar = newSelectedMetricsScalar;
const variants = Object.entries(metricsVariants).map(([metricName, variants]) => ({metric: metricName, variants}))
const variants = Object.entries(metricsVariants).map(([metricName, variants]) => ({metric: metricName, variants}));
this.selectedVariants = variants;
if (variants.length > 0) {
if (this.firstTime || VariantWasAdded && this.missingVariantGraphInStore()) {
@@ -218,8 +218,8 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
selectedMetricsCols = metrics.filter(metric => this.settings.selectedMetricsScalar.includes(metric.metric + metric.variant) || this.settings.selectedMetricsScalar.includes(metric.metric));
} else {
if (this.settings.groupBy === 'metric') {
const uniqueMetrics = Array.from( new Set(metrics.map(a => a.metric)))
const FifthMetric = uniqueMetrics[5] ?? uniqueMetrics.at(-1)
const uniqueMetrics = Array.from(new Set(metrics.map(a => a.metric)));
const FifthMetric = uniqueMetrics[5] ?? uniqueMetrics.at(-1);
selectedMetricsCols = metrics.slice(0, metrics.findIndex(metric => metric.metric === FifthMetric) ?? 5);
} else {
@@ -250,8 +250,8 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
return acc;
}, {} as { [metric: string]: string[] });
return Object.entries(selectedMetricsVariants).map(([metricName, variants]) => ({metric: metricName, variants}))
}
return Object.entries(selectedMetricsVariants).map(([metricName, variants]) => ({metric: metricName, variants}));
};
private prepareGraphsAndUpdate(metrics, singleValues) {
if (metrics || singleValues) {
@@ -298,7 +298,7 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
}
this.settings = {...this.settings, selectedMetricsScalar: selectedList ?? []};
if (!this.minimized) {
const selectedMetricsCols = this.originMetrics.filter(metric => this.settings.selectedMetricsScalar.includes(metric.metric + metric.variant) || this.settings.selectedMetricsScalar.includes(metric.metric))
const selectedMetricsCols = this.originMetrics.filter(metric => this.settings.selectedMetricsScalar.includes(metric.metric + metric.variant) || this.settings.selectedMetricsScalar.includes(metric.metric));
this.selectedVariants = this.buildMetricVariants(selectedMetricsCols);
if (this.selectedVariants.length > 0) {
this.store.dispatch(getMultiScalarCharts({taskIds: this.taskIds, entity: this.entityType, metrics: this.selectedVariants, xAxisType: this.settings.xAxisType}));
@@ -372,6 +372,6 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
private missingVariantGraphInStore() {
const graphs = this.graphs ? Object.keys(this.graphs) : [];
return this.settings.selectedMetricsScalar.filter(metric => !metric.startsWith(' Summary')).some( graph => !graphs.includes(graph));
return this.settings.selectedMetricsScalar.filter(metric => !metric.startsWith(' Summary')).some(graph => !graphs.includes(graph));
}
}

View File

@@ -1,13 +1,15 @@
<mat-drawer-container autosize class="h-100">
<mat-drawer #drawer [opened]="filterOpen" mode="over" (closedStart)="filterOpen = false">
<i class="close al-icon al-ico-dialog-x" (click)="drawer.close()" data-id="closeToggleGraph"></i>
<div class="list-container mt-3">
<mat-drawer #drawer [opened]="filterOpen" mode="over" (closedStart)="filterOpen = false; filterValue = ''" class="mat-elevation-z0">
<div class="head">
<i class="close al-icon al-ico-dialog-x" (click)="drawer.close(); filterValue = ''" data-id="closeToggleGraph"></i>
</div>
<div class="list-container">
<sm-selectable-grouped-filter-list
[list]="metricVariantList"
[checkedList]="settings.selectedMetricsScalar"
[searchTerm]="listSearchTerm"
(hiddenChanged)="selectedMetricsChanged($event)"
(searchTermChanged)="searchTermChanged($event)"
[list]="metricVariantList"
[checkedList]="settings.selectedMetricsScalar"
[searchTerm]="listSearchTerm"
(hiddenChanged)="selectedMetricsChanged($event)"
(searchTermChanged)="searchTermChanged($event)"
>
</sm-selectable-grouped-filter-list>
</div>
@@ -15,7 +17,7 @@
<mat-drawer-content>
<div *ngIf="dataTable?.length === 0" class="no-output" >
<div *ngIf="dataTable?.length === 0" class="no-output">
<i class="i-no-table-data"></i>
<h4>NO METRICS</h4>
</div>
@@ -25,23 +27,29 @@
</div>
<p-table *ngIf="experiments.length > 0"
[value]="dataTableFiltered"
[scrollable]="true"
[virtualScroll]="true"
[virtualScrollItemSize]="50"
[rowTrackBy]="trackByFunction"
styleClass="p-mt-3"
scrollHeight="flex"
[class.empty-state]="dataTable?.length < 1"
[class.no-rows]="dataTableFiltered?.length < 1">
<p-table
[value]="dataTableFiltered"
[scrollable]="true"
[virtualScroll]="true"
[virtualScrollItemSize]="50"
[rowTrackBy]="trackByFunction"
styleClass="p-mt-3"
scrollHeight="flex"
[class.empty-state]="dataTable?.length < 1"
[class.no-rows]="dataTableFiltered?.length < 1">
<ng-template pTemplate="header">
<tr>
<th class="filter-header-cell" [class.freeze-divider]="scrolled" pFrozenColumn [colSpan]="2">
<div class="filter-header">
<i [class]="'al-icon pointer ' + (settings.hiddenMetricsScalar?.length > 0 ? 'al-ico-filter-on':'al-ico-filter-off')" (click)="filterOpen = !filterOpen">
<span class="path1"></span><span class="path2"></span>
</i>
<button class="btn btn-icon" [class.is-filtered]="dataTable?.length > dataTableFiltered?.length">
<i [class]="'al-icon pointer ' + (dataTable?.length > dataTableFiltered?.length ? 'al-ico-filter-on':'al-ico-filter-off')"
[smTooltip]="dataTableFiltered?.length + '/' + dataTable?.length + ' filtered'"
[matTooltipDisabled]="dataTableFiltered?.length === dataTable?.length"
[showTooltip]="showFilterTooltip"
(click)="filterOpen = !filterOpen">
<span class="path1"></span><span class="path2"></span>
</i>
</button>
<mat-form-field appearance="outline" hideRequiredMarker class="light-theme mat-light no-bottom">
<input #filterRef
name="filter"
@@ -49,7 +57,7 @@
placeholder="Search metric"
matInput
autocomplete="off">
<i matSuffix class="al-icon sm-md search-icon me-2" [ngClass]="variantFilter.value? 'al-ico-dialog-x pointer' : 'al-ico-search'"
<i matSuffix class="al-icon sm-md search-icon me-2" [class]="variantFilter.value? 'al-ico-dialog-x pointer' : 'al-ico-search'"
(click)="variantFilter.value && clear(); filterRef.focus()"
smClickStopPropagation></i>
</mat-form-field>
@@ -66,18 +74,18 @@
[smTooltip]="exp.name"
[delay]="0"
[triggerEllipsis]="experiments.length"
>{{exp.name}}</span>
>{{ exp.name }}</span>
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-metricVariant>
<tr [class.first-row]="metricVariant.firstMetricRow">
<td class="metrics-column" pFrozenColumn>
<span class="ellipsis" [smTooltip]="metricVariant.firstMetricRow ? metricVariant.metric : ''" smShowTooltipIfEllipsis>{{metricVariant.firstMetricRow ? metricVariant.metric : ''}}</span>
<span class="ellipsis" [smTooltip]="metricVariant.firstMetricRow ? metricVariant.metric : ''" smShowTooltipIfEllipsis>{{ metricVariant.firstMetricRow ? metricVariant.metric : '' }}</span>
</td>
<td class="variants-column" [class.freeze-divider]="scrolled" pFrozenColumn>
<div class="variant-cell">
<span class="ellipsis variant-name" [smTooltip]="metricVariant.variant" smShowTooltipIfEllipsis>{{metricVariant.variant}}</span>
<span class="ellipsis variant-name" [smTooltip]="metricVariant.variant" smShowTooltipIfEllipsis>{{ metricVariant.variant }}</span>
<i *ngIf="metricVariant.min === metricVariant.max"
class="al-icon al-ico-equal-outline sm row-info-icon"
smTooltip="All scalars have the same value in this row"></i>
@@ -85,7 +93,7 @@
</td>
<td class="value-cell" *ngFor="let exp of experiments; trackBy: trackById">
<div class="value">
<span class="value-text" smShowTooltipIfEllipsis [smTooltip]="metricVariant.values[exp.id]?.[valuesMode.key]">{{metricVariant.values[exp.id]?.[valuesMode.key]}}</span>
<span class="value-text" smShowTooltipIfEllipsis [smTooltip]="metricVariant.values[exp.id]?.[valuesMode.key]">{{ metricVariant.values[exp.id]?.[valuesMode.key] }}</span>
<ng-container *ngIf="(selectShowRowExtremes$ | async) && metricVariant.min !== metricVariant.max">
<span *ngIf="metricVariant.values[exp.id]?.[valuesMode.key] === metricVariant.max" class="tag tag-max">MAX</span>
<span *ngIf="metricVariant.values[exp.id]?.[valuesMode.key] === metricVariant.min" class="tag tag-min">MIN</span>

View File

@@ -91,7 +91,6 @@ $tableBottomPaginatorBorderWidth: 0 0 1px 0 !default;
i {
color: $blue-400;
margin-left: 8px;
&:hover {
color: $blue-300;
@@ -103,7 +102,24 @@ $tableBottomPaginatorBorderWidth: 0 0 1px 0 !default;
display: flex;
width: 401px;
align-items: center;
gap: 12px;
gap: 24px;
padding-left: 8px;
.btn-icon {
border: 1px solid transparent;
background-color: transparent;
padding: 5px;
&:hover {
border-color: $blue-250;
background-color: white;
}
}
.is-filtered {
border-color: $cloudy-blue;
background-color: white;
}
mat-form-field {
flex-grow: 1;
@@ -216,11 +232,15 @@ tr.first-row {
}
}
.head {
padding: 12px 24px 0;
height: 24px;
display: flex;
justify-content: end;
}
.close {
cursor: pointer;
position: absolute;
right: 10px;
top: 10px;
z-index: 1;
}

View File

@@ -20,6 +20,7 @@ import {setExportTable} from '@common/experiments-compare/actions/compare-header
import {Table} from 'primeng/table';
import {ExperimentCompareSettings} from '@common/experiments-compare/reducers/experiments-compare-charts.reducer';
import {GroupedList} from '@common/tasks/tasks.model';
import {sortMetricsList} from '@common/tasks/tasks.utils';
interface ValueMode {
key: string;
@@ -90,11 +91,12 @@ export class ExperimentCompareMetricValuesComponent implements OnInit, OnDestroy
private subs = new Subscription();
private selectExportTable$: Observable<boolean>;
public showFilterTooltip = false;
@ViewChildren(Table) public tableComp: QueryList<Table>;
private scrollContainer: HTMLDivElement;
public scrolled: boolean;
private filterValue: string;
public filterValue: string;
public settings: ExperimentCompareSettings = {} as ExperimentCompareSettings;
private initialSettings = {
selectedMetricsScalar: []
@@ -220,7 +222,7 @@ export class ExperimentCompareMetricValuesComponent implements OnInit, OnDestroy
allMetricsVariants = mergeWith(allMetricsVariants, ...lastMetrics);
this.metricVariantList = {};
const dataTable = Object.entries(allMetricsVariants).map(([metricId, metricsVar]) => Object.keys(metricsVar).map(variantId => {
this.dataTable = sortMetricsList(Object.keys(allMetricsVariants)).map((metricId) => Object.keys(allMetricsVariants[metricId]).map(variantId => {
let metric, variant;
return {
metricId,
@@ -249,8 +251,6 @@ export class ExperimentCompareMetricValuesComponent implements OnInit, OnDestroy
}, {values: {}} as { metric, variant, firstMetricRow: boolean, min: number, max: number, values: { [expId: string]: Task['last_metrics'] } })
};
})).flat(1);
dataTable.sort((a) => a.metric.startsWith(':') ? 1 : -1);
this.dataTable = dataTable.sort((a) => a.metric === ' Summary' ? -1 : 1);
if (!this.settings.selectedMetricsScalar || this.settings.selectedMetricsScalar?.length === 0) {
this.settings.selectedMetricsScalar = this.getFirstMetrics(10);
}
@@ -263,6 +263,7 @@ export class ExperimentCompareMetricValuesComponent implements OnInit, OnDestroy
return;
}
this.dataTableFiltered = this.dataTable.filter(row => this.settings.selectedMetricsScalar?.includes(`${row.metric}${row.variant}`)).filter(row => row.metric.includes(value) || row.variant.includes(value));
this.showFilterTooltip = true;
let previousMetric: string;
this.dataTableFiltered.forEach(row => {
row.firstMetricRow = row.metric !== previousMetric;

View File

@@ -79,6 +79,7 @@ export class ExperimentComparePlotsComponent implements OnInit, OnDestroy {
public splitSize$: Observable<number>;
private originMetrics: Array<MetricVariantResult>;
private firstTime = true;
private previousTaskIds: Array<string>;
@HostListener('window:beforeunload', ['$event']) unloadHandler() {
this.saveSettingsState();
@@ -101,7 +102,7 @@ export class ExperimentComparePlotsComponent implements OnInit, OnDestroy {
);
this.routerParams$ = this.store.select(selectRouterParams).pipe(
filter(params => !!params.ids),
filter(params => params.ids!== undefined),
distinctUntilChanged(),
tap(() => this.refreshDisabled = true)
);
@@ -135,7 +136,7 @@ export class ExperimentComparePlotsComponent implements OnInit, OnDestroy {
.subscribe(([params, metrics, selectedExperiments]) => {
if (!this.taskIds || this.taskIds.join(',') !== params.ids) {
const previousTaskIds = this.taskIds;
this.taskIds = params.ids.split(',').sort();
this.taskIds = params.ids.split(',').sort().filter(id => !!id);
this.store.dispatch(setSelectedExperiments({selectedExperiments: this.taskIds}));
if (metrics.length === 0 || (metrics.length > 0 && previousTaskIds !== undefined) || !isEqual(selectedExperiments, this.taskIds)) {
this.store.dispatch(getCustomMetricsPerType({ids: this.taskIds, metricsType: EventTypeEnum.Plot, isModel: this.entityType === EntityTypeEnum.model}));
@@ -146,7 +147,8 @@ export class ExperimentComparePlotsComponent implements OnInit, OnDestroy {
this.subs.add(this.store.select(selectCompareSelectedMetrics('plots'))
.pipe(
filter(metrics => !!metrics && this.minimized),
distinctUntilChanged((prev, curr) => isEqual(prev, curr)))
// distinctUntilChanged((prev, curr) => isEqual(prev, curr))
)
.subscribe(selectedMetrics => {
const metricsVariants = selectedMetrics.filter(m => !m.hidden).reduce((acc, curr) => {
if (acc[curr.metricName]) {
@@ -159,11 +161,12 @@ export class ExperimentComparePlotsComponent implements OnInit, OnDestroy {
this.settings.selectedMetricsPlot = selectedMetrics.filter(m => !m.hidden).map(m => `${m.metricName} - ${m.variantName}`);
const variants = Object.entries(metricsVariants).map(([metricName, variants]) => ({metric: metricName, variants}))
this.selectedVariants = variants;
if (this.firstTime || this.previousSelectedMetrics?.length !== selectedMetrics?.length) {
if (this.firstTime || this.previousSelectedMetrics?.length !== selectedMetrics?.length || this.taskIds.length !== this.previousTaskIds.length) {
this.firstTime = false;
this.store.dispatch(getMultiPlotCharts({taskIds: this.taskIds, entity: this.entityType, metrics: variants}));
}
this.previousSelectedMetrics = selectedMetrics;
this.previousTaskIds = this.taskIds;
}));
this.subs.add(this.refresh.tick

View File

@@ -64,10 +64,10 @@
'hide-identical-mode': hideIdenticalFields
}">
<div>
<pre *ngIf="(node.data.value !== undefined) || (node.data.existOnOrigin && node.data.existOnCompared)"
[class.no-ellipsis]="((node.data.key | hideHash) + node.data.value).length < 45"
<pre #row *ngIf="(node.data.value !== undefined) || (node.data.existOnOrigin && node.data.existOnCompared)"
[class.no-ellipsis]="(nativeWidth -2 > row.scrollWidth) && (row.scrollWidth === row.clientWidth)"
[class.with-ellipsis]="showEllipsis"
[style.width.px]="showEllipsis ? nativeWidth - 55 - node.level * 20 : null"
[style.width.px]="showEllipsis ? nativeWidth - 45 - node.level * 20 : null"
><ng-container
*ngIf="!!node.data.value?.dataDictionary && !!node.data.value?.link; else simple">{{node.data.key |
hideHash}}<span

View File

@@ -1,14 +1,11 @@
import {AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit,} from '@angular/core';
import {select, Store} from '@ngrx/store';
import {AfterViewInit, ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
import {experimentListUpdated} from '../../actions/experiments-compare-details.actions';
import {selectModelsDetails} from '../../reducers';
import {filter, tap} from 'rxjs/operators';
import {ExperimentCompareTree,} from '~/features/experiments-compare/experiments-compare-models';
import {ExperimentCompareTree} from '~/features/experiments-compare/experiments-compare-models';
import {convertmodelsArrays, getAllKeysEmptyObject, isDetailsConverted} from '../../jsonToDiffConvertor';
import {ExperimentCompareBase} from '../experiment-compare-base';
import {ActivatedRoute, Router} from '@angular/router';
import {ConfigurationItem} from '~/business-logic/model/tasks/configurationItem';
import {RefreshService} from '@common/core/services/refresh.service';
import {LIMITED_VIEW_LIMIT} from '@common/experiments-compare/experiments-compare.constants';
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
import {ModelDetail} from '@common/experiments-compare/shared/experiments-compare-details.model';
@@ -50,6 +47,7 @@ export class ModelCompareDetailsComponent extends ExperimentCompareBase implemen
this.resetComponentState(models);
this.calculateTree(models);
this.nativeWidth = Math.max(this.treeCardBody?.getBoundingClientRect().width, 410);
});
}
@@ -72,6 +70,7 @@ export class ModelCompareDetailsComponent extends ExperimentCompareBase implemen
general: this.buildSectionTree(experiment, 'general', mergedExperiment),
labels: this.buildSectionTree(experiment, 'labels', mergedExperiment),
metadata: this.buildSectionTree(experiment, 'metadata', mergedExperiment),
lineage: this.buildSectionTree(experiment, 'lineage', mergedExperiment),
};
}
}

View File

@@ -2,7 +2,7 @@
<div class="actions-container">
<span class="d-flex">
<button class="btn btn-secondary btn-add-experiment" (click)="showGlobalLegend()">
<span>{{entityType | uppercase}}S</span>
<span>{{ entityType | uppercase }}S</span>
</button>
<button [smTooltip]="(allowAddExperiment$ | async) ?
'Add/Remove ' + entityType + 's to comparison' :
@@ -19,37 +19,37 @@
(selectionChange)="changeView($event)"
>
<mat-select-trigger>
<i data-id="viewTypeMenuOption" class="fas me-2"
[ngClass]="{'fa-align-left': viewMode.endsWith('values'), 'fa-chart-area': viewMode === 'graph'}"></i>
<i data-id="viewTypeMenuOption" class="al-icon sm-md me-2"
[ngClass]="{'al-ico-description': viewMode.endsWith('values'), 'al-ico-charts-view': viewMode === 'graph', 'al-ico-scatter-view': viewMode === 'scatter'}"></i>
<ng-container [ngSwitch]="true">
<span *ngSwitchCase="currentPage === 'hyper-params' && viewMode === 'graph'">Parallel Coordinates</span>
<span *ngSwitchCase="currentPage === 'hyper-params' && viewMode === 'scatter'"><i class="al-icon al-ico-no-scatter-graph sm me-2"></i>Scatter Plot</span>
<span *ngSwitchCase="currentPage === 'hyper-params' && viewMode === 'scatter'">Scatter Plot</span>
<span *ngSwitchCase="currentPage === 'scalars' && viewMode === 'graph'">Graph</span>
<span *ngSwitchCase="currentPage === 'scalars' && viewMode === 'values'">Last Values</span>
<span *ngSwitchDefault>{{(queryParamsViewMode$ | async) || viewMode | noUnderscore | titlecase}}</span>
<span *ngSwitchDefault>{{ (queryParamsViewMode$ | async) || viewMode | noUnderscore | titlecase }}</span>
</ng-container>
</mat-select-trigger>
<ng-container *ngIf="currentPage === 'scalars'; else: otherOptions">
<mat-option value="values" class="compare-mat-option">
<i class="fas fa-align-left me-2"></i>Last Values
<i class="al-icon al-ico-description sm-md me-2"></i>Last Values
</mat-option>
<mat-option *ngIf="currentPage === 'scalars'" value="min-values" class="compare-mat-option">
<i class="fas fa-align-left me-2"></i>Min Values
<i class="al-icon al-ico-description sm-md me-2"></i>Min Values
</mat-option>
<mat-option *ngIf="currentPage === 'scalars'" value="max-values" class="compare-mat-option">
<i class="fas fa-align-left me-2"></i>Max Values
<i class="al-icon al-ico-description sm-md me-2"></i>Max Values
</mat-option>
</ng-container>
<ng-template #otherOptions>
<mat-option value="values" class="compare-mat-option">
<i class="fas fa-align-left me-2"></i>Values
<i class="al-icon al-ico-description sm-md me-2"></i>Values
</mat-option>
</ng-template>
<mat-option value="graph" class="compare-mat-option">
<i class="fas fa-chart-area me-2"></i>{{currentPage === 'hyper-params' ? 'Parallel Coordinates' : 'Graph'}}
<i class="al-icon al-ico-charts-view sm-md me-2"></i>{{ currentPage === 'hyper-params' ? 'Parallel Coordinates' : 'Graph' }}
</mat-option>
<mat-option *ngIf="currentPage === 'hyper-params'" value="scatter" class="compare-mat-option">
<i class="al-icon al-ico-no-scatter-graph sm me-2"></i>Scatter Plot
<i class="al-icon al-ico-scatter-view sm-md me-2"></i>Scatter Plot
</mat-option>
</mat-select>
</mat-form-field>
@@ -57,9 +57,9 @@
<div id="nextDiff" class="next-diff"></div>
<mat-slide-toggle
*ngIf="['hyper-params', 'details', 'models-details', 'network'].includes(currentPage) && viewMode !== 'graph' && viewMode !== 'scatter'"
*ngIf="['hyper-params', 'details', 'models-details', 'network'].includes(currentPage)"
(change)="hideIdenticalFieldsToggled($event)"
[checked]="selectHideIdenticalFields$ | async">Hide Identical Fields
[checked]="selectHideIdenticalFields$ | async">Hide Identical {{ (viewMode === 'graph' || viewMode === 'scatter') ? 'Parameters' : 'Fields' }}
</mat-slide-toggle>
<mat-slide-toggle color="primary"

View File

@@ -102,8 +102,13 @@
}
::ng-deep .light-theme .mat-mdc-select-value-text {
i {vertical-align: middle;}
}
::ng-deep .light-theme .mat-mdc-option .mdc-list-item__primary-text {
white-space: nowrap;
i {vertical-align: middle;}
}
::ng-deep .mat-mdc-menu-content {

View File

@@ -0,0 +1,41 @@
@if (multiSelect) {
<div class="d-flex title-container align-items-center" (click)="metricSelect.openMenu()">
<span class="param-selector-title pointer">{{ title }}</span>
<sm-menu #metricSelect iconClass="al-icon al-ico-dropdown-arrow pointer" buttonTooltip="Select metric" smMenuClass="light-theme custom-columns" (menuClosed)="selectMetric.resetSearch()">
<sm-select-metric-for-custom-col #selectMetric class="normal-size"
[metricVariants]="metricVariantsSet"
[tableCols]="selectedMetricVariants ? selectedMetricVariants : []"
[multiSelect]="multiSelect"
[skipValueType]="skipValueType"
[enableClearSelection]="true"
(selectedMetricToShow)="selectMetricVariant.emit($event)"
(clearSelection)="clearSelection.emit()"></sm-select-metric-for-custom-col>
</sm-menu>
</div>
} @else {
<button class="btn btn-outline d-flex justify-content-between align-items-center" (click)="metricSelect.openMenu()">
<span class="selected-parameter ellipsis" smShowTooltipIfEllipsis
[smTooltip]="(selectedMetricVariants[0] |metricVariantToName) + ' ' + (MetricValueTypeStrings[selectedMetricVariants[0]?.valueType] ?? '')">{{ selectedMetricVariants[0] ? (selectedMetricVariants[0] |metricVariantToName) : 'Select Metric' }} {{ (MetricValueTypeStrings[selectedMetricVariants[0]?.valueType] ?? '') }}</span>
<sm-menu #metricSelect iconClass="al-icon al-ico-dropdown-arrow pointer al-color blue-400" buttonTooltip="Select metric" smMenuClass="light-theme custom-columns" (menuClosed)="selectMetric.resetSearch()">
<sm-select-metric-for-custom-col #selectMetric class="normal-size"
[metricVariants]="metricVariantsSet"
[tableCols]="selectedMetricVariants ? selectedMetricVariants : []"
[multiSelect]="multiSelect"
[skipValueType]="skipValueType"
[enableClearSelection]="true"
(selectedMetricToShow)="selectMetricVariant.emit($event)"
(clearSelection)="clearSelection.emit()"></sm-select-metric-for-custom-col>
</sm-menu>
</button>
}
@if (multiSelect) {
@if (selectedMetricVariants.length > 0) {
@for (selectedMetricVariant of selectedMetricVariants; track trackByIndex($index)) {
<div class="multi-selected-parameter" smShowTooltipIfEllipsis [smTooltip]="(selectedMetricVariant |metricVariantToName) + ' ' + (MetricValueTypeStrings[selectedMetricVariant?.valueType] ?? '')">
<span class="multi-selected-parameter-name ellipsis">{{ selectedMetricVariant |metricVariantToName }} {{ (MetricValueTypeStrings[selectedMetricVariant?.valueType] ?? '') }}</span>
<i class="al-icon sm al-ico-dialog-x pointer" (click)="removeMetric.emit(selectedMetricVariant)"></i></div>
}
} @else {
<div class="no-data">No metrics selected</div>
}
}

View File

@@ -0,0 +1,79 @@
@import "variables";
:host {
display: flex;
flex-direction: column;
.btn.btn-outline {
color: $blue-500;
border: 1px solid $cloudy-blue;
font-size: 14px;
font-weight: normal;
&:hover {
border-color: $blue-250;
}
}
sm-menu {
height: 24px;
width: 24px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
}
.multi-selected-parameter{
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
border-radius: 4px;
background-color: transparent;
&:hover {
background-color: $blue-50;
}
.multi-selected-parameter-name{
max-width: 280px;
}
.al-ico-dialog-x {
visibility: hidden
}
&:hover {
.al-ico-dialog-x {
visibility: visible;
}
}
.selected-parameter {
align-items: center;
justify-content: space-between;
max-width: 260px;
}
}
.title-container {
margin-bottom: 12px;
border-bottom: 1px solid $cloudy-blue;
padding: 6px 12px 6px 0;
&:hover {
border-color: $blue-250;
cursor: pointer;
}
.param-selector-title {
color: $blue-500;
font-size: 14px;
}
sm-menu {
margin-left: auto;
}
}
.no-data{
text-align: center;
color: $blue-300;
}
}

View File

@@ -0,0 +1,59 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {MenuComponent} from '@common/shared/ui-components/panel/menu/menu.component';
import {ClickStopPropagationDirective} from '@common/shared/ui-components/directives/click-stop-propagation.directive';
import {CustomColumnsListComponent} from '@common/shared/components/custom-columns-list/custom-columns-list.component';
import {ExperimentCompareSharedModule} from '@common/experiments-compare/shared/experiment-compare-shared.module';
import {
SelectMetadataKeysCustomColsComponent
} from '@common/shared/components/select-metadata-keys-custom-cols/select-metadata-keys-custom-cols.component';
import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult';
import {uniqBy} from 'lodash-es';
import {
SelectionEvent
} from '@common/experiments/dumb/select-metric-for-custom-col/select-metric-for-custom-col.component';
import {SelectedMetricVariant} from '@common/experiments-compare/experiments-compare.constants';
import {MetricVariantToNamePipe} from '@common/shared/pipes/metric-variant-to-name.pipe';
import {trackByIndex} from '@common/shared/utils/forms-track-by';
import {TooltipDirective} from '@common/shared/ui-components/indicators/tooltip/tooltip.directive';
import {
ShowTooltipIfEllipsisDirective
} from '@common/shared/ui-components/indicators/tooltip/show-tooltip-if-ellipsis.directive';
import {MetricValueTypeStrings} from '@common/shared/utils/tableParamEncode';
@Component({
selector: 'sm-metric-variant-selector',
standalone: true,
imports: [
MenuComponent,
ClickStopPropagationDirective,
CustomColumnsListComponent,
ExperimentCompareSharedModule,
SelectMetadataKeysCustomColsComponent,
MetricVariantToNamePipe,
TooltipDirective,
ShowTooltipIfEllipsisDirective
],
templateUrl: './metric-variant-selector.component.html',
styleUrl: './metric-variant-selector.component.scss'
})
export class MetricVariantSelectorComponent {
public metricVariantsSet: Array<MetricVariantResult>;
@Input() selectedMetricVariants: SelectedMetricVariant[];
@Input() multiSelect: boolean = false;
@Input() skipValueType: boolean = false;
@Input() title: string;
@Input() set metricVariants(metricVariants: Array<MetricVariantResult>) {
this.metricVariantsSet = uniqBy(metricVariants?.map(metricVariant => {
return {...metricVariant, key: metricVariant.metric_hash + metricVariant.variant_hash};
}), 'key')?.map(({key, ...metricVariant}) => metricVariant);
}
@Output() selectMetricVariant = new EventEmitter<SelectionEvent>();
@Output() removeMetric = new EventEmitter<SelectedMetricVariant>();
@Output() clearSelection = new EventEmitter();
protected readonly trackByIndex = trackByIndex;
protected readonly MetricValueTypeStrings = MetricValueTypeStrings;
searchText: string;
}

View File

@@ -1,42 +1,45 @@
<div class="actions" *ngIf="!darkTheme">
<button
(click)="maximize()"
class="btn btn-icon"
smTooltip="Maximize"
><i class="al-icon al-ico-fit sm-md"></i></button>
<button
(click)="creatingEmbedCode($any($event.target).getBoundingClientRect())"
class="btn btn-icon"
smTooltip="Copy embed code"
><i class="al-icon al-ico-code sm-md"></i></button>
<button
(click)="downloadImage()"
class="btn btn-icon"
smTooltip="Download as PNG"
><i class="al-icon al-ico-download sm-md"></i></button>
</div>
<div #container class="graph-container" [class.dark-theme]="darkTheme">
<div class="graph-title">Hyperparameters Impact on {{metric?.name}}</div>
<div #parallelGraph>
</div>
<div #legend class="d-flex legend-container">
<div *ngFor="let experiment of experiments; trackBy: trackById" class="experiment-name">
<span class="dot-container">
<span #dot class="dot pallete-cursor"
[style.background-color]="experimentsColors[experiment.id]"
[colorButtonRef]="dot"
[smChooseColor]="experimentsColors[getExperimentNameForColor(experiment)]"
[stringToColor]="getExperimentNameForColor(experiment)">
</span>
</span>
<span
class="task-name pointer"
(click)="toggleHideExperiment(experiment.id)"
(mouseover)="highlightExperiment(experiment)"
(mouseout)="removeHighlightExperiment()"
[class.hide]="experiment.hidden">
{{experiment.name + (experiment.duplicateName ? ('.' + (experiment.id|slice:0:5)) : '')}}
</span>
</div>
</div>
</div>
@if (!darkTheme && !reportMode) {
<div class="actions">
<button
(click)="maximize()"
class="btn btn-icon"
smTooltip="Maximize"
><i class="al-icon al-ico-fit sm-md"></i></button>
<button
(click)="creatingEmbedCode($any($event.target).getBoundingClientRect())"
class="btn btn-icon"
smTooltip="Copy embed code"
><i class="al-icon al-ico-code sm-md"></i></button>
<button
(click)="downloadImage()"
class="btn btn-icon"
smTooltip="Download as PNG"
><i class="al-icon al-ico-download sm-md"></i></button>
</div>
}
<div #container class="graph-container" [class.dark-theme]="darkTheme">
<div #parallelGraph>
</div>
<div #legend class="d-flex legend-container">
@for (experiment of experiments; track experiment.id) {
<div class="experiment-name">
<span class="dot-container">
<span #dot class="dot pallete-cursor"
[style.background-color]="experimentsColors[experiment.id]"
[colorButtonRef]="dot"
[smChooseColor]="experimentsColors[getExperimentNameForColor(experiment)]"
[stringToColor]="getExperimentNameForColor(experiment)">
</span>
</span>
<span
class="task-name pointer"
(click)="toggleHideExperiment(experiment.id)"
(mouseover)="highlightExperiment(experiment)"
(mouseout)="removeHighlightExperiment()"
[class.hide]="experiment.hidden">
{{experiment.name + (experiment.duplicateName ? ('.' + (experiment.id|slice:0:5)) : '')}}
</span>
</div>
}
</div>
</div>

View File

@@ -1,4 +1,4 @@
import {ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, ViewChild} from '@angular/core';
import {ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
import {
DARK_THEME_GRAPH_LINES_COLOR,
DARK_THEME_GRAPH_TICK_COLOR, ExtData,
@@ -6,7 +6,7 @@ import {
ExtLayout,
PlotlyGraphBaseComponent
} from '@common/shared/single-graph/plotly-graph-base';
import {NgForOf, NgIf, SlicePipe} from '@angular/common';
import { SlicePipe } from '@angular/common';
import {debounceTime, filter, take} from 'rxjs/operators';
import {from} from 'rxjs';
import {cloneDeep, get, isEqual, max, min, uniq} from 'lodash-es';
@@ -16,12 +16,13 @@ import {select} from 'd3-selection';
import {ColorHashService} from '@common/shared/services/color-hash/color-hash.service';
import {Task} from '~/business-logic/model/tasks/task';
import {sortCol} from '@common/shared/utils/sortCol';
import {MetricValueType, SelectedMetric} from '@common/experiments-compare/experiments-compare.constants';
import {MetricValueType, SelectedMetricVariant} from '@common/experiments-compare/experiments-compare.constants';
import {TooltipDirective} from '@common/shared/ui-components/indicators/tooltip/tooltip.directive';
import {trackById} from '@common/shared/utils/forms-track-by';
import {GraphViewerComponent, GraphViewerData} from '@common/shared/single-graph/graph-viewer/graph-viewer.component';
import {MatDialog} from '@angular/material/dialog';
import {ChooseColorModule} from '@common/shared/ui-components/directives/choose-color/choose-color.module';
import {MetricVariantToPathPipe} from '@common/shared/pipes/metric-variant-to-path.pipe';
import {MetricVariantToNamePipe} from '@common/shared/pipes/metric-variant-to-name.pipe';
// eslint-disable-next-line @typescript-eslint/naming-convention
declare let Plotly;
@@ -49,6 +50,7 @@ interface ParaPlotData {
};
}
@Component({
selector: 'sm-parallel-coordinates-graph',
templateUrl: './parallel-coordinates-graph.component.html',
@@ -56,18 +58,16 @@ interface ParaPlotData {
imports: [
SlicePipe,
TooltipDirective,
NgForOf,
NgIf,
ChooseColorModule
],
ChooseColorModule,
MetricVariantToNamePipe
],
standalone: true
})
export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent implements OnInit {
public trackById = trackById;
export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent implements OnInit, OnChanges {
private metricVariantToPathPipe = new MetricVariantToPathPipe();
private metricVariantToNamePipe = new MetricVariantToNamePipe();
private data: ParaPlotData[];
private _experiments: ExtraTask[];
private _metric: SelectedMetric;
public experimentsColors = {};
public filteredExperiments = [];
private timer: number;
@@ -99,20 +99,6 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
return this._metricValueType;
}
@Input() set metric(metric) {
if (metric != this.metric) {
this._metric = metric;
if (this.experiments) {
clearTimeout(this.timer);
this.timer = window.setTimeout(() => this.prepareGraph(), 200);
}
}
}
get metric(): SelectedMetric {
return this._metric;
}
@Input() set parameters(parameters) {
if (!isEqual(parameters, this.parameters)) {
this._parameters = parameters;
@@ -127,6 +113,7 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
return this._parameters;
}
@Input() metrics: SelectedMetricVariant[];
@Input() set experiments(experiments) {
let experimentsCopy = cloneDeep(experiments) ?? [];
@@ -163,6 +150,13 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
super();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.metrics && this.experiments) {
this.prepareGraph();
}
}
ngOnInit(): void {
this.initColorSubscription();
}
@@ -224,7 +218,7 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
parameter = `${parameter}.value`;
const allValuesIncludingNull = this.experiments.map(experiment => get(experiment.hyperparams, parameter));
const allValues = allValuesIncludingNull.filter(value => (value !== undefined)).filter(value => (value !== ''));
const textVal = {} as {[key: string]: number};
const textVal = {} as { [key: string]: number };
let ticktext = this.naturalCompare(uniq(allValues).filter(text => text !== ''));
(allValuesIncludingNull.length > allValues.length) && (ticktext = ['N/A'].concat(ticktext));
const tickvals = ticktext.map((text, index) => {
@@ -244,7 +238,7 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
tickvals,
values: filteredExperiments.map((experiment) => (textVal[['', undefined].includes(get(experiment.hyperparams, parameter)) ? 'N/A' : get(experiment.hyperparams, parameter)])),
range: [0, max(tickvals)],
constraintrange,
constraintrange
};
})
@@ -253,7 +247,7 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
trace.line = {
color: this.getColorsArray(filteredExperiments),
colorscale: filteredExperiments.map((experiment, index) =>
[index / (filteredExperiments.length - 1), this.getStringColor(experiment)] as [number, string]),
[index / (filteredExperiments.length - 1), this.getStringColor(experiment)] as [number, string])
};
} else {
trace.line = {
@@ -265,40 +259,43 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
// this.data = [trace];
// this.drawChart();
if (this.metric) {
const allValuesIncludingNull = this.experiments.map(experiment => get(experiment.last_metrics, `${this.metric.path}.${this.metricValueType}`));
const allValues = allValuesIncludingNull.filter(value => value !== undefined);
const naVal = this.getNAValue(allValues);
const ticktext = uniq(allValuesIncludingNull.map(value => value !== undefined ? value : 'N/A'));
const tickvals = ticktext.map(text => text === 'N/A' ? naVal : text);
let constraintrange;
if (this.parallelGraph.nativeElement?.data?.[0]?.dimensions) {
const currDimention = this.parallelGraph.nativeElement.data[0].dimensions.find(d => d.label === this.metric.name);
if (currDimention?.constraintrange) {
constraintrange = currDimention.constraintrange;
if (this.metrics) {
this.metrics.map(metric => {
const allValuesIncludingNull = this.experiments.map(experiment => get(experiment.last_metrics, this.metricVariantToPathPipe.transform(metric, true)));
const allValues = allValuesIncludingNull.filter(value => value !== undefined);
const naVal = this.getNAValue(allValues);
const ticktext = uniq(allValuesIncludingNull.map(value => value !== undefined ? value : 'N/A'));
const tickvals = ticktext.map(text => text === 'N/A' ? naVal : text);
let constraintrange;
if (this.parallelGraph.nativeElement?.data?.[0]?.dimensions) {
const currDimention = this.parallelGraph.nativeElement.data[0].dimensions.find(d => d.label === this.metricVariantToNamePipe.transform(metric, true));
if (currDimention?.constraintrange) {
constraintrange = currDimention.constraintrange;
}
}
}
trace.dimensions.push({
label: this.metric.name,
ticktext,
tickvals,
values: filteredExperiments.map((experiment) =>
parseFloat(get(experiment.last_metrics, `${this.metric.path}.${this.metricValueType}`, naVal))
),
range: [min(tickvals), max(tickvals)],
constraintrange,
color: 'red',
gridcolor: 'red',
linecolor: 'green',
tickcolor: 'red',
dividercolor: 'green',
spikecolor: 'yellow',
dividerwidth: 3,
tickwidth: 2,
linewidth: 4,
trace.dimensions.push({
label: this.metricVariantToNamePipe.transform(metric, true),
ticktext,
tickvals,
values: filteredExperiments.map((experiment) =>
parseFloat(get(experiment.last_metrics, this.metricVariantToPathPipe.transform(metric, true), naVal))
),
range: [min(tickvals), max(tickvals)],
constraintrange,
color: 'red',
gridcolor: 'red',
linecolor: 'green',
tickcolor: 'red',
dividercolor: 'green',
spikecolor: 'yellow',
dividerwidth: 3,
tickwidth: 2,
linewidth: 4
});
});
}
this.data = [trace];
if (this.dimensionsOrder) {
this.data[0].dimensions.sort((a, b) => sortCol(a.label, b.label, this.dimensionsOrder));
@@ -321,7 +318,7 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
margin: {l: 120, r: 120},
...(setDimentiosn && {
height: 500,
width: this.parallelGraph.nativeElement.offsetWidth,
width: this.parallelGraph.nativeElement.offsetWidth
}),
...(this.darkTheme ? {
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -330,7 +327,7 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
margin: {l: 120, r: 120, b: 20},
font: {
color: DARK_THEME_GRAPH_TICK_COLOR
},
}
} : {})
} as ExtLayout;
}
@@ -349,12 +346,12 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
const graph = select(this.parallelGraph.nativeElement);
this.graphWidth = graph.node().getBoundingClientRect().width;
graph.selectAll('.y-axis')
.filter((d: {key: string}) => d.key === this.metric.name)
.filter((d: { key: string }) => this.metrics?.map(mv => this.metricVariantToNamePipe.transform(mv, true)).includes(d.key))
.classed('metric-column', true);
graph.selectAll('.axis-title')
.text((d: {key: string}) => this.wrap(d.key))
.text((d: { key: string }) => this.wrap(d.key))
.append('title')
.text(d => (d as {key: string}).key);
.text(d => (d as { key: string }).key);
graph.selectAll('.axis .tick text').text((d: string) => this.wrap(d)).append('title').text((d: string) => d);
graph.selectAll('.axis .tick text').style('pointer-events', 'auto');
this.darkTheme && graph.selectAll('.dark-theme .axis .domain').style('stroke', DARK_THEME_GRAPH_LINES_COLOR).style('stroke-opacity', 1);
@@ -419,13 +416,18 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
const a = document.createElement('a');
a.href = url;
a.target = '_blank';
a.download = `Hyperparameters ${this._metric.name}.png`;
a.download = `Hyperparameters ${this.metrics[0].metric} ${this.metrics[0].variant}.png`;
a.click();
});
}
creatingEmbedCode(domRect: DOMRect) {
this.createEmbedCode.emit({tasks: this.experiments.map(exp => exp.id), valueType: this.metricValueType, metrics: [this.metric.path], variants: this.parameters, domRect});
this.createEmbedCode.emit({
tasks: this.experiments.map(exp => exp.id),
valueType: this.metricValueType,
metrics: this.metrics.map(mv => this.metricVariantToPathPipe.transform(mv, true)),
variants: this.parameters, domRect
});
}
maximize() {
@@ -435,15 +437,18 @@ export class ParallelCoordinatesGraphComponent extends PlotlyGraphBaseComponent
// signed url are updated after originChart was cloned - need to update images urls!
chart: cloneDeep({
data: this.data as unknown as ExtData[],
layout: {...this.getLayout(false), title: this.metric?.name || ''},
layout: {
...this.getLayout(false),
title: ''
},
config: {
displaylogo: false,
displayModeBar: false,
displayModeBar: false
}
} as ExtFrame),
id: `compare params ${this.experiments?.map(e => e.id).join(',')}`,
darkTheme: false,
isCompare: true,
isCompare: true
} as GraphViewerData,
panelClass: ['image-viewer-dialog', 'light-theme'],
height: '100%',

View File

@@ -0,0 +1,52 @@
@if (!single) {
<div class="d-flex align-items-center title-container" (click)="paramSelect.openMenu()">
<span class="param-selector-title pointer">{{ title }}</span>
<sm-menu #paramSelect iconClass="al-icon al-ico-dropdown-arrow pointer" (menuClosed)="checklist.searchQ('')"
buttonTooltip="Select parameter" smMenuClass="light-theme custom-columns">
<sm-grouped-checked-filter-list
#checklist
smClickStopPropagation class="filtered-list"
[itemsList]="itemsList"
[selectedItemsList]="selectedHyperParams"
[selectFilteredItems]="selectFilteredItems"
[selectedItemsListMapper]="selectedItemsListMapper"
selectedItemsListPrefix=""
[limitSelection]="50"
[single]="false"
(selectedItems)="selectedItems.emit($event)"
(clearSelection)="clearSelection.emit()"></sm-grouped-checked-filter-list>
</sm-menu>
</div>
@if (selectedHyperParams?.length > 0) {
<div>
@for (hyperParam of selectedHyperParams; track trackByIndex($index)) {
<div class="selected-parameter">
<span class="ellipsis parameter-name" smShowTooltipIfEllipsis [smTooltip]="hyperParam">{{ hyperParam }}</span>
<i class="al-icon sm al-ico-dialog-x pointer" (click)="removeHyperParam(hyperParam)"></i></div>
}
</div>
} @else {
<div class="no-data">No parameters selected</div>
}
} @else {
<button class="btn btn-outline d-flex justify-content-between align-items-center" (click)="paramSelect.openMenu()">
<span class="ellipsis select" smShowTooltipIfEllipsis [smTooltip]="selectedHyperParams?.[0] ?? ''">{{ selectedHyperParams?.[0] ?? 'Select Parameter' }}</span>
<sm-menu #paramSelect
(menuClosed)="checklist.searchQ('')"
iconClass="al-icon al-ico-dropdown-arrow pointer al-color blue-400"
buttonTooltip="Select parameter"
smMenuClass="light-theme custom-columns">
<sm-grouped-checked-filter-list #checklist
smClickStopPropagation
class="filtered-list"
[itemsList]="itemsList"
[selectedItemsList]="selectedHyperParams"
[selectFilteredItems]="selectFilteredItems"
[selectedItemsListMapper]="selectedItemsListMapper"
selectedItemsListPrefix=""
[single]="true"
(selectedItems)="selectedItems.emit($event)"
(clearSelection)="clearSelection.emit()"></sm-grouped-checked-filter-list>
</sm-menu>
</button>
}

View File

@@ -0,0 +1,90 @@
@import "variables";
:host {
display: flex;
flex-direction: column;
.no-data {
text-align: center;
color: $blue-300;
}
.btn.btn-outline {
color: $blue-500;
border: 1px solid $cloudy-blue;
font-size: 14px;
font-weight: normal;
&:hover {
border-color: $blue-250;
}
}
sm-menu {
height: 24px;
width: 24px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
}
}
.filtered-list {
padding: 6px;
height: 480px;
::ng-deep .actions {
padding-top: 10px;
}
}
.selected-parameter {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
border-radius: 4px;
background-color: transparent;
&:hover {
background-color: $blue-50;
}
span {
max-width: 280px;
}
.al-ico-dialog-x {
visibility: hidden
}
&:hover {
.al-ico-dialog-x {
visibility: visible;
}
}
}
.title-container {
margin-bottom: 12px;
border-bottom: 1px solid $cloudy-blue;
padding: 6px 12px 6px 0;
&:hover {
border-color: $blue-250;
cursor: pointer;
}
.param-selector-title {
color: $blue-500;
font-size: 14px;
}
sm-menu {
margin-left: auto;
}
}

View File

@@ -0,0 +1,46 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {MenuComponent} from '@common/shared/ui-components/panel/menu/menu.component';
import {ExperimentSharedModule} from '~/features/experiments/shared/experiment-shared.module';
import {ClickStopPropagationDirective} from '@common/shared/ui-components/directives/click-stop-propagation.directive';
import {
GroupedCheckedFilterListComponent
} from '@common/shared/ui-components/data/grouped-checked-filter-list/grouped-checked-filter-list.component';
import { AsyncPipe } from '@angular/common';
import { trackByIndex } from '@common/shared/utils/forms-track-by';
import {TooltipDirective} from '@common/shared/ui-components/indicators/tooltip/tooltip.directive';
import {
ShowTooltipIfEllipsisDirective
} from '@common/shared/ui-components/indicators/tooltip/show-tooltip-if-ellipsis.directive';
@Component({
selector: 'sm-param-selector',
standalone: true,
imports: [
MenuComponent,
ExperimentSharedModule,
ClickStopPropagationDirective,
GroupedCheckedFilterListComponent,
AsyncPipe,
TooltipDirective,
ShowTooltipIfEllipsisDirective
],
templateUrl: './param-selector.component.html',
styleUrl: './param-selector.component.scss'
})
export class ParamSelectorComponent {
public trackByIndex = trackByIndex;
@Input() selectedHyperParams: string[];
@Input() title: string;
@Input() itemsList: { [section: string]: any };
@Input() single: boolean;
@Input() selectFilteredItems: boolean;
@Input() selectedItemsListMapper: (data) => string;
@Output() selectedItems = new EventEmitter<{ param: string }>();
@Output() clearSelection = new EventEmitter();
removeHyperParam(hyperParam: string) {
this.selectedItems.emit({param:hyperParam})
}
}

View File

@@ -20,7 +20,7 @@ import {selectCompareHistogramCacheAxisType} from '../reducers';
import {ScalarKeyEnum} from '~/business-logic/model/events/scalarKeyEnum';
import {selectActiveWorkspaceReady} from '~/core/reducers/view.reducer';
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
import {EMPTY, iif} from 'rxjs';
import {EMPTY, iif, of} from 'rxjs';
import {merge} from 'lodash-es';
import {ApiModelsService} from '~/business-logic/api-services/models.service';
import {setAxisCache, setGlobalLegendData} from '../actions/experiments-compare-charts.actions';
@@ -117,33 +117,38 @@ export class ExperimentsCompareChartsEffects {
getMultiPlotCharts = createEffect(() => this.actions$.pipe(
ofType(chartActions.getMultiPlotCharts),
debounceTime(200),
switchMap(action => this.eventsApi.eventsGetMultiTaskPlots({
tasks: action.taskIds,
// eslint-disable-next-line @typescript-eslint/naming-convention
model_events: action.entity === EntityTypeEnum.model,
metrics: action.metrics,
last_iters_per_task_metric: true,
iters: 1
}).pipe(
map((res: EventsGetMultiTaskPlotsResponse) => [res.returned, res] as [number, EventsGetMultiTaskPlotsResponse]),
expand(([plotsLength, data]) => (data.total < 10000 && data.returned > 0)
switchMap(action => { if(action.taskIds.length > 0 ){
return this.eventsApi.eventsGetMultiTaskPlots({
tasks: action.taskIds,
// eslint-disable-next-line @typescript-eslint/naming-convention
? this.eventsApi.eventsGetMultiTaskPlots({
tasks: action.taskIds,
model_events: action.entity === EntityTypeEnum.model,
metrics: action.metrics,
last_iters_per_task_metric: true,
iters: 1
}).pipe(
map((res: EventsGetMultiTaskPlotsResponse) => [res.returned, res] as [number, EventsGetMultiTaskPlotsResponse]),
expand(([plotsLength, data]) => (data.total < 10000 && data.returned > 0)
// eslint-disable-next-line @typescript-eslint/naming-convention
model_events: action.entity === EntityTypeEnum.model,
// eslint-disable-next-line @typescript-eslint/naming-convention
scroll_id: data.scroll_id,
metrics: action.metrics,
last_iters_per_task_metric: true,
iters: 1
}).pipe(
map((res: EventsGetMultiTaskPlotsResponse) => [plotsLength + res.returned, res] as [number, EventsGetTaskPlotsResponse])
)
: EMPTY
),
reduce((acc, [, data]) => merge(acc, data.plots), {})
)),
? this.eventsApi.eventsGetMultiTaskPlots({
tasks: action.taskIds,
// eslint-disable-next-line @typescript-eslint/naming-convention
model_events: action.entity === EntityTypeEnum.model,
// eslint-disable-next-line @typescript-eslint/naming-convention
scroll_id: data.scroll_id,
metrics: action.metrics,
last_iters_per_task_metric: true,
iters: 1
}).pipe(
map((res: EventsGetMultiTaskPlotsResponse) => [plotsLength + res.returned, res] as [number, EventsGetTaskPlotsResponse])
)
: EMPTY
),
reduce((acc, [, data]) => merge(acc, data.plots), {})
)
} else {
return of({plots:[]})
}
}),
mergeMap(plots => [
chartActions.setExperimentPlots({plots}),
deactivateLoader(chartActions.getMultiPlotCharts.type)]),

View File

@@ -7,10 +7,9 @@ import {ApiTasksService} from '~/business-logic/api-services/tasks.service';
import {ExperimentDetailsReverterService} from '../services/experiment-details-reverter.service';
import {requestFailed} from '../../core/actions/http.actions';
import {selectExperimentIdsDetails, selectExperimentsDetails, selectModelIdsDetails} from '../reducers';
import {Observable, of} from 'rxjs';
import {forkJoin, Observable, of} from 'rxjs';
import {IExperimentDetail} from '~/features/experiments-compare/experiments-compare-models';
import {REFETCH_EXPERIMENT_REQUESTED, refetchExperimentRequested} from '../actions/compare-header.actions';
import {ExperimentCompareDetailsState} from '../reducers/experiments-compare-details.reducer';
import {experimentListUpdated, setExperiments, setModels} from '../actions/experiments-compare-details.actions';
import {getCompareDetailsOnlyFields} from '~/features/experiments-compare/experiments-compare-consts';
import {selectHasDataFeature} from '~/core/reducers/users.reducer';
@@ -105,14 +104,19 @@ export class ExperimentsCompareDetailsEffects {
: of([]);
}
fetchModelDetails$(ids): Observable<Array<IExperimentDetail>> {
fetchModelDetails$(ids: string[]): Observable<Array<IExperimentDetail>> {
return ids.length > 0 ?
this.modelsApi.modelsGetAllEx({
forkJoin([
this.modelsApi.modelsGetAllEx({
id: ids,
// eslint-disable-next-line @typescript-eslint/naming-convention
only_fields: ['company', 'created', 'last_update', 'last_iteration', 'framework', 'id', 'labels', 'name', 'ready', 'tags', 'system_tags', 'task.name', 'task.project', 'uri', 'user.name', 'parent', 'project.name', 'metadata']
}).pipe(
map(res => this.modelDetailsReverter.revertModels(ids, res.models))
}),
this.tasksApi.tasksGetAllEx({
'models.input.model': ids,
only_fields: ['name', 'models.input.model']} as any)
]).pipe(
map(([modelsRes, tasksRes]) => this.modelDetailsReverter.revertModels(ids, modelsRes.models, tasksRes.tasks))
)
: of([]);
}

View File

@@ -4,10 +4,17 @@ import {activeLoader, deactivateLoader, setServerError} from '../../core/actions
import {catchError, mergeMap, map} from 'rxjs/operators';
import {requestFailed} from '../../core/actions/http.actions';
import {ApiTasksService} from '~/business-logic/api-services/tasks.service';
import {getExperimentsHyperParams, setHyperParamsList, setMetricsList, setTasks} from '../actions/experiments-compare-scalars-graph.actions';
import {
getExperimentsHyperParams,
setHyperParamsList,
setMetricsList,
setMetricsResults,
setTasks
} from '../actions/experiments-compare-scalars-graph.actions';
import {GroupedHyperParams, HyperParams} from '../reducers/experiments-compare-charts.reducer';
import {Store} from '@ngrx/store';
import {selectSelectedSettingsMetric} from '@common/experiments-compare/reducers';
import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult';
@Injectable()
export class ExperimentsCompareScalarsGraphEffects {
@@ -20,34 +27,69 @@ export class ExperimentsCompareScalarsGraphEffects {
map(action => activeLoader(action.type))
));
loadMovies$ = createEffect(() => this.actions$.pipe(
getExperimentsHyperParams$ = createEffect(() => this.actions$.pipe(
ofType(getExperimentsHyperParams),
concatLatestFrom(() => this.store.select(selectSelectedSettingsMetric)),
mergeMap(([action, /*metric*/]) => this.tasksApiService.tasksGetAllEx({
id: action.experimentsIds,
id: action.experimentsIds,
// eslint-disable-next-line @typescript-eslint/naming-convention
only_fields: ['last_metrics', 'name', 'last_iteration', 'hyperparams'],
// ...(action.scatter && metric && {[`last_metrics.${metric.path}.value`]: [Number.MIN_VALUE, null]})
})
.pipe(
// map(res => res.tasks)),
mergeMap(res => {
const metricsList = this.getMetricOptions(res.tasks);
const paramsHasDiffs = this.getParametersHasDiffs(res.tasks);
return [
setTasks({tasks: res.tasks}),
setMetricsList({metricsList}),
setHyperParamsList({hyperParams: paramsHasDiffs}),
deactivateLoader(action.type)];
}),
catchError(error => [
requestFailed(error), deactivateLoader(action.type),
setServerError(error, null, 'Failed to get Compared Experiments')
])
)
only_fields: ['last_metrics', 'name', 'last_iteration', 'hyperparams'],
// ...(action.scatter && metric && {[`last_metrics.${metric.path}.value`]: [Number.MIN_VALUE, null]})
})
.pipe(
// map(res => res.tasks)),
mergeMap(res => {
const metricsList = this.getMetricOptions(res.tasks);
const metricVariantsResults = this.getMetricResults(res.tasks);
const paramsHasDiffs = this.getParametersHasDiffs(res.tasks);
return [
setTasks({tasks: res.tasks}),
setMetricsList({metricsList}),
setMetricsResults({metricVariantsResults: metricVariantsResults}),
setHyperParamsList({hyperParams: paramsHasDiffs}),
deactivateLoader(action.type)];
}),
catchError(error => [
requestFailed(error), deactivateLoader(action.type),
setServerError(error, null, 'Failed to get Compared Experiments')
])
)
))
);
getMetricResults(tasks): Array<MetricVariantResult> {
const metrics = [];
for (const task of tasks) {
for (const metric in task.last_metrics) {
for (const variant in task.last_metrics[metric]) {
const metricName = task.last_metrics[metric][variant].metric;
const variantName = task.last_metrics[metric][variant].variant;
metrics.push({
metric: metricName,
metric_hash: metric,
variant: variantName,
variant_hash: variant
});
// !metrics[metricName] && (metrics[metricName] = {});
// if (!metrics[metricName][variantName]) {
// metrics[metricName][variantName] = {};
// metrics[metricName][variantName]['name'] = `${metricName}/${variantName}`;
// metrics[metricName][variantName]['path'] = `${metric}.${variant}`;
}
}
}
// return Object.keys(metrics).sort((a, b) => a.toLowerCase() > b.toLowerCase() ? 1 : -1).map(metricName => ({
// metricName,
// variants: Object.keys(metrics[metricName]).sort().map(variant => ({
// name: variant,
// value: metrics[metricName][variant]
// }))
// }));
return metrics;
}
getMetricOptions(tasks) {
const metrics = {};
for (const task of tasks) {

View File

@@ -15,7 +15,7 @@ import {selectProjectType} from '~/core/reducers/view.reducer';
import {ALL_PROJECTS_OBJECT} from '@common/core/effects/projects.effects';
import {trackById} from '@common/shared/utils/forms-track-by';
import {selectRouterConfig, selectRouterParams} from '@common/core/reducers/router-reducer';
import {getGlobalLegendData} from '@common/experiments-compare/actions/experiments-compare-charts.actions';
import {getGlobalLegendData, setGlobalLegendData} from '@common/experiments-compare/actions/experiments-compare-charts.actions';
import {rgbList2Hex} from '@common/shared/services/color-hash/color-hash.utils';
import {ColorHashService} from '@common/shared/services/color-hash/color-hash.service';
import {SelectModelComponent} from '@common/select-model/select-model.component';
@@ -61,11 +61,11 @@ export class ExperimentsCompareComponent implements OnInit, OnDestroy {
private ids: string[];
public duplicateNamesObject: { [name: string]: boolean };
private routeConfig$: Observable<string[]>;
private titleCasePipe = new TitleCasePipe();
constructor(private store: Store,
private router: Router,
private activatedRoute: ActivatedRoute,
private titleCasePipe: TitleCasePipe,
private colorHash: ColorHashService,
private dialog: MatDialog,
private cdr: ChangeDetectorRef,
@@ -86,6 +86,7 @@ export class ExperimentsCompareComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.subs.unsubscribe();
this.store.dispatch(resetSelectCompareHeader({fullReset: true}));
this.store.dispatch(setGlobalLegendData({data: null}));
this.store.dispatch(setContextMenu({contextMenu: null}));
this.store.dispatch(resetSelectModelState({fullReset: true}));
}
@@ -97,7 +98,7 @@ export class ExperimentsCompareComponent implements OnInit, OnDestroy {
this.subs.add(combineLatest([this.routeConfig$,
this.store.select(selectRouterParams),
this.store.select(selectSelectedProject)]).pipe(filter(([conf, params, project]) => !!params.ids && !!project?.id)).subscribe(([conf, params, project]) => {
this.store.select(selectSelectedProject)]).pipe(filter(([, params, project]) => !!params.ids && !!project?.id)).subscribe(([conf, params, project]) => {
this.setupCompareContextMenu(toCompareEntityType[this.entityType] ?? this.entityType, conf[conf[0] === 'datasets' ? 4 : 3], project?.id, params.ids, conf[0]);
}));
@@ -216,7 +217,7 @@ export class ExperimentsCompareComponent implements OnInit, OnDestroy {
updateUrl(ids: string[]) {
this.router.navigate(
[{ids}, ...this.activatedRoute.firstChild?.snapshot.url.map(segment => segment.path)],
[{ids}, ...(this.activatedRoute.firstChild?.snapshot.url.map(segment => segment.path) ?? [])],
{
queryParamsHandling: 'preserve',
relativeTo: this.activatedRoute,

View File

@@ -1,4 +1,5 @@
import {HeaderNavbarTabConfig} from '@common/layout/header-navbar-tabs/header-navbar-tabs-config.types';
import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult';
export type MetricValueType = 'min_value' | 'max_value' | 'value';
@@ -8,6 +9,10 @@ export interface SelectedMetric {
valueType?: 'min_value' | 'max_value' | 'value';
}
export interface SelectedMetricVariant extends MetricVariantResult{
valueType?: 'min_value' | 'max_value' | 'value';
}
export interface DataDictionary {
dataDictionary: boolean;
link: string;

View File

@@ -32,7 +32,6 @@ import {CompareCardExtraHeaderDirective} from './dumbs/compare-card-extra-header
import {CompareCardHeaderDirective} from './dumbs/compare-card-header.directive';
import {TableDiffModule} from '../shared/ui-components/data/table-diff/table-diff.module';
import {CardModule} from '../shared/ui-components/panel/card2';
import {DrawerModule} from '../shared/ui-components/panel/drawer';
import {
ParallelCoordinatesGraphComponent
} from './dumbs/parallel-coordinates-graph/parallel-coordinates-graph.component';
@@ -47,16 +46,20 @@ import {
} from './containers/experiment-compare-params/experiment-compare-params.component';
import {ExperimentsCompareParamsEffects} from './effects/experiments-compare-params.effects';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {ModelCompareDetailsComponent} from '@common/experiments-compare/containers/model-compare-details/model-compare-details.component';
import {
ModelCompareDetailsComponent
} from '@common/experiments-compare/containers/model-compare-details/model-compare-details.component';
import {IExperimentCompareChartsState} from '@common/experiments-compare/reducers/experiments-compare-charts.reducer';
import {UserPreferences} from '@common/user-preferences';
import {merge, pick} from 'lodash-es';
import {createUserPrefFeatureReducer} from '@common/core/meta-reducers/user-pref-reducer';
import {EXPERIMENTS_COMPARE_METRICS_CHARTS_} from '@common/experiments-compare/actions/experiments-compare-charts.actions';
import {
EXPERIMENTS_COMPARE_METRICS_CHARTS_
} from '@common/experiments-compare/actions/experiments-compare-charts.actions';
import {EXPERIMENTS_COMPARE_SELECT_EXPERIMENT_} from '@common/experiments-compare/actions/compare-header.actions';
import {LabeledFormFieldDirective} from '@common/shared/directive/labeled-form-field.directive';
import {EllipsisMiddleDirective} from '@common/shared/ui-components/directives/ellipsis-middle.directive';
import { CompareScatterPlotComponent } from './containers/compare-scatter-plot/compare-scatter-plot.component';
import {CompareScatterPlotComponent} from './containers/compare-scatter-plot/compare-scatter-plot.component';
import {ScatterPlotComponent} from '@common/shared/components/charts/scatter-plot/scatter-plot.component';
import {NoUnderscorePipe} from '@common/shared/pipes/no-underscore.pipe';
import {HideHashPipe} from '@common/shared/pipes/hide-hash.pipe';
@@ -92,7 +95,17 @@ import {MatSelectModule} from '@angular/material/select';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatExpansionModule} from '@angular/material/expansion';
import {MatInputModule} from '@angular/material/input';
import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indicators/tooltip/show-tooltip-if-ellipsis.directive';
import {
MetricVariantSelectorComponent
} from '@common/experiments-compare/dumbs/metric-param-selector/metric-variant-selector.component';
import {ParamSelectorComponent} from '@common/experiments-compare/dumbs/param-selector/param-selector.component';
import {
ShowTooltipIfEllipsisDirective
} from '@common/shared/ui-components/indicators/tooltip/show-tooltip-if-ellipsis.directive';
import {MetricVariantToPathPipe} from '@common/shared/pipes/metric-variant-to-path.pipe';
import {MetricResultToSelectedMetricPipe} from '@common/shared/pipes/metric-result-to-selected-metric.pipe';
import {MetricVariantToNamePipe} from '@common/shared/pipes/metric-variant-to-name.pipe';
import {DrawerComponent} from '@common/shared/ui-components/panel/drawer/drawer.component';
export const COMPARE_STORE_KEY = 'experimentsCompare';
export const COMPARE_CONFIG_TOKEN =
@@ -115,7 +128,7 @@ export const getCompareConfig = (userPreferences: UserPreferences) => ({
return merge({}, nextState, savedState);
}
if (action.type.startsWith('EXPERIMENTS_COMPARE_')) {
localStorage.setItem(localStorageKey, JSON.stringify(pick(nextState, ['charts.settingsList', 'charts.scalarsHoverMode'])));
localStorage.setItem(localStorageKey, JSON.stringify(pick(nextState, ['charts.settingsList', 'charts.scalarsHoverMode', 'compareHeader.hideIdenticalRows'])));
}
return nextState;
};
@@ -152,7 +165,6 @@ export const getCompareConfig = (userPreferences: UserPreferences) => ({
TableDiffModule,
ScrollingModule,
CardModule,
DrawerModule,
ExperimentSharedModule,
ExperimentsCompareRoutingModule,
ExperimentGraphsModule,
@@ -197,7 +209,14 @@ export const getCompareConfig = (userPreferences: UserPreferences) => ({
MatSidenavModule,
MatExpansionModule,
MatInputModule,
ShowTooltipIfEllipsisDirective
MetricVariantSelectorComponent,
ParamSelectorComponent,
MatInputModule,
ShowTooltipIfEllipsisDirective,
MetricVariantToPathPipe,
MetricResultToSelectedMetricPipe,
MetricVariantToNamePipe,
DrawerComponent,
],
providers: [
{provide: COMPARE_CONFIG_TOKEN, useFactory: getCompareConfig, deps: [UserPreferences]},

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