mirror of
https://github.com/clearml/clearml-web
synced 2025-06-26 18:27:02 +00:00
release v1.16
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"userKey": "EYVQ385RW7Y2QQUH88CZ7DWIQ1WUHP",
|
||||
"userSecret": "yfc8KQo*GMXb*9p((qcYC7ByFIpF7I&4VH3BfUYXH%o9vX1ZUZQEEw1Inc)S",
|
||||
"userSecret": "XhkH6a6ds9JBnM_MrahYyYdO-wS2bqFSm8gl-V0UZXH26Ydd6Eyi28TeBEoSr6Z3Bes",
|
||||
"companyID": "d1bd92a3b039400cbafc60a7a5b1e52b"
|
||||
}
|
||||
|
||||
5405
package-lock.json
generated
5405
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
123
package.json
123
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "clearml-webapp",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0",
|
||||
"license": "",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "npx ng serve",
|
||||
"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": "npx ng build --configuration production --source-map --vendor-chunk",
|
||||
"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,53 +19,52 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@angular/animations": "^17.3.7",
|
||||
"@angular/cdk": "^17.3.7",
|
||||
"@angular/common": "^17.3.7",
|
||||
"@angular/compiler": "^17.3.7",
|
||||
"@angular/core": "^17.3.7",
|
||||
"@angular/forms": "^17.3.7",
|
||||
"@angular/material": "^17.3.7",
|
||||
"@angular/platform-browser": "^17.3.7",
|
||||
"@angular/platform-browser-dynamic": "^17.3.7",
|
||||
"@angular/platform-server": "^17.3.7",
|
||||
"@angular/router": "^17.3.7",
|
||||
"@angular/service-worker": "^17.3.7",
|
||||
"@angular/youtube-player": "^17.3.7",
|
||||
"@aws-sdk/client-s3": "^3.569.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.569.0",
|
||||
"@ctrl/ngx-github-buttons": "^9.0.0",
|
||||
"@ctrl/tinycolor": "^4.0.3",
|
||||
"@ctrl/tinycolor": "^4.1.0",
|
||||
"@ngneat/dag": "^2.0.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",
|
||||
"@ngrx/effects": "^17.2.0",
|
||||
"@ngrx/entity": "^17.2.0",
|
||||
"@ngrx/router-store": "^17.2.0",
|
||||
"@ngrx/store": "^17.2.0",
|
||||
"ace-builds": "^1.33.1",
|
||||
"angular-resizable-element": "^7.0.2",
|
||||
"angular-split": "^17.1.1",
|
||||
"angular-split": "^17.2.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"bootstrap": "^5.3.2",
|
||||
"chart.js": "^4.4.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"chart.js": "^4.4.2",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chartjs-plugin-annotation": "^3.0.1",
|
||||
"chartjs-plugin-zoom": "^2.0.1",
|
||||
"curved-arrows": "^0.1.0",
|
||||
"curved-arrows": "^0.2.0",
|
||||
"d3-selection": "^3.0.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"diff": "^5.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"diff": "^5.2.0",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"export-to-csv": "^1.2.2",
|
||||
"filesize": "^10.1.0",
|
||||
"dompurify": "^3.0.8",
|
||||
"dompurify": "^3.1.2",
|
||||
"export-to-csv": "^1.3.0",
|
||||
"filesize": "^10.1.1",
|
||||
"has-ansi": "^5.0.1",
|
||||
"hocon-parser": "^1.0.1",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucene": "^2.1.1",
|
||||
"marked": "^11.1.1",
|
||||
"ng2-charts": "^5.0.4",
|
||||
"marked": "^12.0.2",
|
||||
"ng2-charts": "^6.0.1",
|
||||
"ngx-clipboard": "^16.0.0",
|
||||
"ngx-color-picker": "^16.0.0",
|
||||
"ngx-device-detector": "^7.0.0",
|
||||
@@ -73,48 +72,48 @@
|
||||
"ngx-print": "^1.5.1",
|
||||
"ngx-window-token": "^7.0.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"primeicons": "^6.0.1",
|
||||
"primeng": "^17.4.0",
|
||||
"primeicons": "^7.0.0",
|
||||
"primeng": "^17.16.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.3"
|
||||
"zone.js": "~0.14.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@angular-devkit/build-angular": "^17.3.6",
|
||||
"@angular-devkit/core": "^17.3.6",
|
||||
"@angular-devkit/schematics": "^17.3.6",
|
||||
"@angular-devkit/schematics-cli": "^17.3.6",
|
||||
"@angular-eslint/builder": "17.3.0",
|
||||
"@angular-eslint/eslint-plugin": "17.3.0",
|
||||
"@angular-eslint/eslint-plugin-template": "17.3.0",
|
||||
"@angular-eslint/schematics": "17.3.0",
|
||||
"@angular-eslint/template-parser": "17.3.0",
|
||||
"@angular/cli": "^17.3.6",
|
||||
"@angular/compiler-cli": "^17.3.7",
|
||||
"@angular/language-service": "^17.3.7",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@ngrx/eslint-plugin": "^17.2.0",
|
||||
"@ngrx/schematics": "^17.2.0",
|
||||
"@ngrx/store-devtools": "^17.2.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/node": "^20.12.10",
|
||||
"@types/plotly.js": "^2.29.3",
|
||||
"@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",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
||||
"@typescript-eslint/parser": "^7.8.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsdoc": "^48.0.3",
|
||||
"eslint-plugin-jsdoc": "^48.2.3",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"typescript": "~5.3.3"
|
||||
"typescript": "~5.4.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,8 @@ export interface PipelinesDeleteRunsRequest {
|
||||
* Pipeline project ids. When deleting at least one run should be left
|
||||
*/
|
||||
project: string;
|
||||
/**
|
||||
* If set then for the passed pipeline controller tasks also delete the pipeline steps
|
||||
*/
|
||||
include_pipeline_steps?: boolean;
|
||||
}
|
||||
|
||||
@@ -25,4 +25,8 @@ export interface TasksArchiveManyRequest {
|
||||
* Extra information regarding status change
|
||||
*/
|
||||
status_message?: string;
|
||||
/**
|
||||
* If set then for the passed pipeline controller tasks also delete the pipeline steps
|
||||
*/
|
||||
include_pipeline_steps?: boolean;
|
||||
}
|
||||
|
||||
@@ -29,4 +29,12 @@ export interface TasksEnqueueRequest {
|
||||
* Extra information regarding status change
|
||||
*/
|
||||
status_message?: string;
|
||||
/**
|
||||
* The name of the queue. If the queue does not exist then it is auto-created. Cannot be used together with the queue id
|
||||
*/
|
||||
queue_name?: string;
|
||||
/**
|
||||
* If passed then check wheter there are any workers watiching the queue
|
||||
*/
|
||||
verify_watched_queue?: boolean;
|
||||
}
|
||||
|
||||
@@ -29,4 +29,8 @@ export interface TasksStopManyRequest {
|
||||
* If not true, call fails if the task status is not \'in_progress\'
|
||||
*/
|
||||
force?: boolean;
|
||||
/**
|
||||
* If set then for the passed pipeline controller tasks also delete the pipeline steps
|
||||
*/
|
||||
include_pipeline_steps?: boolean;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import {projectSyncedKeys} from '~/features/projects/projects.module';
|
||||
import {authReducer} from '~/features/settings/containers/admin/auth.reducers';
|
||||
import {AdminService} from '~/shared/services/admin.service';
|
||||
import {UserEffects} from './effects/users.effects';
|
||||
import {sourcesReducer} from './reducers/sources-reducer';
|
||||
import {usageStatsReducer} from './reducers/usage-stats.reducer';
|
||||
import {usersReducer} from './reducers/users.reducer';
|
||||
import {viewReducer} from './reducers/view.reducer';
|
||||
@@ -47,7 +46,6 @@ export const reducers = {
|
||||
messages: messagesReducer,
|
||||
recentTasks: recentTasksReducer,
|
||||
views: viewReducer,
|
||||
sources: sourcesReducer,
|
||||
users: usersReducer,
|
||||
login: loginReducer,
|
||||
rootProjects: projectsReducer,
|
||||
@@ -67,18 +65,18 @@ const syncedKeys = [
|
||||
'rootProjects.defaultNestedModeForFeature',
|
||||
'views.availableUpdates',
|
||||
'views.showSurvey',
|
||||
'views.neverShowPopupAgain',
|
||||
'views.tableCardsCollapsed'
|
||||
'views.tableCardsCollapsed',
|
||||
'views.contextMenuActiveFeature',
|
||||
];
|
||||
const key = '_saved_state_';
|
||||
|
||||
const actionsPrefix = [AUTH_PREFIX, USERS_PREFIX, VIEW_PREFIX, ROOT_PROJECTS_PREFIX];
|
||||
const actionsPrefix = [AUTH_PREFIX, USERS_PREFIX, ROOT_PROJECTS_PREFIX, VIEW_PREFIX];
|
||||
|
||||
if (!localStorage.getItem(key)) {
|
||||
localStorage.setItem(key, '{}');
|
||||
}
|
||||
|
||||
export const localStorageReducer = (reducer: ActionReducer<any>): ActionReducer<any> =>
|
||||
export const localStorageReducer = (reducer: ActionReducer<string>): ActionReducer<any> =>
|
||||
(state, action) => {
|
||||
let nextState = reducer(state, action);
|
||||
// TODO: lil hack to fix ngrx bug in preload strategy that dispatch store/init multiple times.
|
||||
@@ -100,14 +98,12 @@ const userPrefMetaFactory = (userPreferences: UserPreferences): MetaReducer[] =>
|
||||
(reducer: ActionReducer<any>) =>
|
||||
createUserPrefReducer('users', ['activeWorkspace', 'showOnlyUserWork'], [USERS_PREFIX], userPreferences, reducer),
|
||||
(reducer: ActionReducer<any>) =>
|
||||
createUserPrefReducer('rootProjects', ['tagsColors', 'graphVariant', 'showHidden', 'hideExamples', 'defaultNestedModeForFeature'], [ROOT_PROJECTS_PREFIX], userPreferences, reducer),
|
||||
createUserPrefReducer('rootProjects', ['tagsColors', 'graphVariant', 'showHidden', 'hideExamples', 'defaultNestedModeForFeature', 'blockUserScript'], [ROOT_PROJECTS_PREFIX], userPreferences, reducer),
|
||||
(reducer: ActionReducer<any>) =>
|
||||
createUserPrefReducer('views', ['autoRefresh', 'neverShowPopupAgain', 'redactedArguments', 'hideRedactedArguments'], [VIEW_PREFIX], userPreferences, reducer),
|
||||
localStorageReducer,
|
||||
(reducer: ActionReducer<any>) =>
|
||||
createUserPrefReducer('projects', projectSyncedKeys, [PROJECTS_PREFIX], userPreferences, reducer),
|
||||
(reducer: ActionReducer<any>) =>
|
||||
createUserPrefReducer('compare-experiments', compareSyncedKeys, [EXPERIMENTS_COMPARE_METRICS_CHARTS_], userPreferences, reducer),
|
||||
(reducer: ActionReducer<any>) =>
|
||||
createUserPrefReducer('colorsPreference', colorSyncedKeys, [CHOOSE_COLOR_PREFIX], userPreferences, reducer)
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import {deactivateLoader} from '@common/core/actions/layout.actions';
|
||||
import {ALL_PROJECTS_OBJECT} from '@common/core/effects/projects.effects';
|
||||
import {requestFailed} from '@common/core/actions/http.actions';
|
||||
import {ApiProjectsService} from '~/business-logic/api-services/projects.service';
|
||||
import {selectCurrentUser, selectShowOnlyUserWork} from '@common/core/reducers/users-reducer';
|
||||
import {selectCurrentUser, selectShowOnlyUserWork,} from '@common/core/reducers/users-reducer';
|
||||
import {ProjectsGetAllExRequest} from '~/business-logic/model/projects/projectsGetAllExRequest';
|
||||
import {selectRouterConfig} from "@common/core/reducers/router-reducer";
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export function sourcesReducer(sources = {}, action) {
|
||||
|
||||
switch (action.type) {
|
||||
default:
|
||||
return sources;
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ import {MatInputModule} from '@angular/material/input';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
import {HesitateDirective} from '@common/shared/ui-components/directives/hesitate.directive';
|
||||
import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indicators/tooltip/show-tooltip-if-ellipsis.directive';
|
||||
import {SelectQueueModule} from '@common/experiments/shared/components/select-queue/select-queue.module';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -149,7 +150,8 @@ import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indic
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
HesitateDirective,
|
||||
ShowTooltipIfEllipsisDirective
|
||||
ShowTooltipIfEllipsisDirective,
|
||||
SelectQueueModule,
|
||||
],
|
||||
declarations: [
|
||||
ExperimentsComponent,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {ExperimentConverterService} from './services/experiment-converter.servic
|
||||
import { ExperimentMenuComponent } from '@common/experiments/shared/components/experiment-menu/experiment-menu.component';
|
||||
import {ExperimentMenuExtendedComponent} from '../containers/experiment-menu-extended/experiment-menu-extended.component';
|
||||
import {ExperimentHeaderComponent} from '@common/experiments/dumb/experiment-header/experiment-header.component';
|
||||
import {SelectHyperParamsForCustomColComponent} from '@common/experiments/dumb/select-hyper-params-for-custom-col/select-hyper-params-for-custom-col.component';
|
||||
import {ExperimentExecutionParametersComponent} from '@common/experiments/dumb/experiment-execution-parameters/experiment-execution-parameters.component';
|
||||
import {CloneDialogComponent} from '@common/experiments/shared/components/clone-dialog/clone-dialog.component';
|
||||
import {ExperimentSystemTagsComponent} from '@common/experiments/shared/components/experiments-system-tags/experiment-system-tags.component';
|
||||
@@ -12,7 +11,6 @@ import {AbortAllChildrenDialogComponent} from '@common/experiments/shared/compon
|
||||
import {ExperimentsTableComponent} from '@common/experiments/dumb/experiments-table/experiments-table.component';
|
||||
import {ChangeProjectDialogComponent} from '@common/experiments/shared/components/change-project-dialog/change-project-dialog.component';
|
||||
import {ExperimentOutputPlotsComponent} from '@common/experiments/containers/experiment-output-plots/experiment-output-plots.component';
|
||||
import {ExperimentCustomColsMenuComponent} from '@common/experiments/dumb/experiment-custom-cols-menu/experiment-custom-cols-menu.component';
|
||||
import {EffectsModule} from '@ngrx/effects';
|
||||
import {CommonExperimentsMenuEffects} from '@common/experiments/effects/common-experiments-menu.effects';
|
||||
import {CommonExperimentOutputEffects} from '@common/experiments/effects/common-experiment-output.effects';
|
||||
@@ -78,6 +76,9 @@ 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';
|
||||
import {ExperimentCustomColsMenuComponent} from '@common/experiments/dumb/experiment-custom-cols-menu/experiment-custom-cols-menu.component';
|
||||
import {SelectMetricForCustomColComponent} from '@common/experiments/dumb/select-metric-for-custom-col/select-metric-for-custom-col.component';
|
||||
import {SelectHyperParamsForCustomColComponent} from '@common/experiments/dumb/select-hyper-params-for-custom-col/select-hyper-params-for-custom-col.component';
|
||||
|
||||
export const experimentSyncedKeys = [
|
||||
'view.projectColumnsSortOrder',
|
||||
@@ -132,8 +133,6 @@ const DECLARATIONS = [
|
||||
ExperimentExecutionParametersComponent,
|
||||
ExperimentsTableComponent,
|
||||
ExperimentHeaderComponent,
|
||||
ExperimentCustomColsMenuComponent,
|
||||
SelectHyperParamsForCustomColComponent,
|
||||
ExperimentOutputPlotsComponent,
|
||||
];
|
||||
|
||||
@@ -196,6 +195,9 @@ const DECLARATIONS = [
|
||||
ShowTooltipIfEllipsisDirective,
|
||||
MatCheckboxModule,
|
||||
DotsLoadMoreComponent,
|
||||
ExperimentCustomColsMenuComponent,
|
||||
SelectMetricForCustomColComponent,
|
||||
SelectHyperParamsForCustomColComponent,
|
||||
],
|
||||
declarations : [...DECLARATIONS],
|
||||
providers : [
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {createReducer, on} from '@ngrx/store';
|
||||
import {initLogin, loginReducers, LoginState as CommonLoginState} from '../../webapp-common/login/login-reducer';
|
||||
import {initCommonLoginState, loginReducers, CommonLoginState} from '@common/login/login-reducer';
|
||||
import {setLoginError} from '~/features/login/login.actions';
|
||||
|
||||
|
||||
export const login = state => state.login as CommonLoginState;
|
||||
|
||||
export const loginReducer = createReducer(
|
||||
initLogin,
|
||||
on(setLoginError, (state, action) => ({...state,error: action.error})),
|
||||
initCommonLoginState,
|
||||
on(setLoginError, (state, action): CommonLoginState => ({...state,error: action.error})),
|
||||
...loginReducers
|
||||
);
|
||||
|
||||
@@ -44,6 +44,6 @@ export const getFeatureProjectRequest = (snapshot: ActivatedRouteSnapshot, neste
|
||||
...(datasets && getDatasetsRequest(nested, searchQuery, selectedProjectName, selectedProjectId)),
|
||||
};
|
||||
};
|
||||
export const activeFeatureToProjectType = (activeFeature: string) => activeFeature === 'simple' ? 'datasets' : null;
|
||||
|
||||
export const getSelfFeatureProjectRequest = (snapshot: ActivatedRouteSnapshot) => ({
|
||||
});
|
||||
export const getSelfFeatureProjectRequest = (snapshot: ActivatedRouteSnapshot) => ({ });
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {StoreModule} from '@ngrx/store';
|
||||
import {ProjectRouterModule} from './projects-routing.module';
|
||||
import {projectsReducer} from './projects.reducer';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {CommonProjectsModule} from '@common/projects/common-projects.module';
|
||||
|
||||
@@ -15,7 +13,6 @@ export const projectSyncedKeys = ['showHidden', 'tableModeAwareness', 'orderBy',
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CommonProjectsModule,
|
||||
StoreModule.forFeature('projects', projectsReducer),
|
||||
],
|
||||
declarations : []
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="!demo">
|
||||
@if(!demo) {
|
||||
<mat-slide-toggle
|
||||
(change)="statsChange($event)"
|
||||
[checked]="allowed$ | async">Send anonymous usage data (Global setting)</mat-slide-toggle>
|
||||
</ng-container>
|
||||
[checked]="allowed()">Send anonymous usage data (Global setting)</mat-slide-toggle>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {MatSlideToggle, MatSlideToggleChange} from '@angular/material/slide-toggle';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {selectAllowed} from '~/core/reducers/usage-stats.reducer';
|
||||
import {Observable} from 'rxjs';
|
||||
import { updateUsageStats } from '~/core/actions/usage-stats.actions';
|
||||
import {ConfigurationService} from '@common/shared/services/configuration.service';
|
||||
|
||||
@@ -10,19 +9,17 @@ import {ConfigurationService} from '@common/shared/services/configuration.servic
|
||||
@Component({
|
||||
selector: 'sm-usage-stats',
|
||||
templateUrl: './usage-stats.component.html',
|
||||
styleUrls: ['./usage-stats.component.scss']
|
||||
styleUrls: ['./usage-stats.component.scss'],
|
||||
imports: [
|
||||
MatSlideToggle
|
||||
],
|
||||
standalone: true
|
||||
})
|
||||
export class UsageStatsComponent implements OnInit {
|
||||
export class UsageStatsComponent {
|
||||
private store =inject(Store);
|
||||
public shown = true;
|
||||
public demo = ConfigurationService.globalEnvironment.demo;
|
||||
public allowed$: Observable<boolean>;
|
||||
|
||||
constructor(private store: Store<any>) {
|
||||
this.allowed$ = this.store.select(selectAllowed);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
protected allowed = this.store.selectSignal(selectAllowed);
|
||||
|
||||
statsChange(toggle: MatSlideToggleChange) {
|
||||
this.store.dispatch(updateUsageStats({allowed: toggle.checked}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {setContextMenu} from '@common/core/actions/router.actions';
|
||||
import {headerActions} from '@common/core/actions/router.actions';
|
||||
import {Store} from '@ngrx/store';
|
||||
|
||||
@Component({
|
||||
@@ -10,7 +10,7 @@ import {Store} from '@ngrx/store';
|
||||
export class SettingsComponent {
|
||||
private store = inject(Store)
|
||||
constructor() {
|
||||
this.store.dispatch(setContextMenu({contextMenu: null}));
|
||||
this.store.dispatch(headerActions.setTabs({contextMenu: null}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {MatExpansionModule} from '@angular/material/expansion';
|
||||
import {WebappConfigurationComponent} from '@common/settings/webapp-configuration/webapp-configuration.component';
|
||||
import {WorkspaceConfigurationComponent} from '@common/settings/workspace-configuration/workspace-configuration.component';
|
||||
import {ProfileKeyStorageComponent} from '@common/settings/admin/profile-key-storage/profile-key-storage.component';
|
||||
import {ProfilePreferencesComponent} from '@common/settings/admin/profile-preferences/profile-preferences.component';
|
||||
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';
|
||||
@@ -16,7 +15,6 @@ import {AdminCredentialTableComponent} from '@common/settings/admin/admin-creden
|
||||
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';
|
||||
import {UserDataComponent} from '~/features/settings/containers/admin/user-data/user-data.component';
|
||||
import {UsageStatsComponent} from '~/features/settings/containers/admin/usage-stats/usage-stats.component';
|
||||
import {CreateCredentialDialogComponent} from '~/features/settings/containers/admin/create-credential-dialog/create-credential-dialog.component';
|
||||
import {RedactedArgumentsDialogComponent} from '@common/settings/admin/redacted-arguments-dialog/redacted-arguments-dialog.component';
|
||||
import {LayoutModule} from '~/layout/layout.module';
|
||||
@@ -39,13 +37,13 @@ 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';
|
||||
import {ProfilePreferencesComponent} from '@common/settings/admin/profile-preferences/profile-preferences.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
SettingsComponent,
|
||||
UsageStatsComponent,
|
||||
UserDataComponent,
|
||||
UserCredentialsComponent,
|
||||
AdminFooterActionsComponent,
|
||||
@@ -54,7 +52,6 @@ import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indic
|
||||
CreateCredentialDialogComponent,
|
||||
AdminFooterComponent,
|
||||
ProfileNameComponent,
|
||||
ProfilePreferencesComponent,
|
||||
ProfileKeyStorageComponent,
|
||||
WorkspaceConfigurationComponent,
|
||||
WebappConfigurationComponent,
|
||||
@@ -87,11 +84,11 @@ import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indic
|
||||
AdminDialogTemplateComponent,
|
||||
TimeAgoPipe,
|
||||
ShowTooltipIfEllipsisDirective,
|
||||
ProfilePreferencesComponent,
|
||||
],
|
||||
exports: [
|
||||
UserCredentialsComponent,
|
||||
AdminFooterComponent,
|
||||
ProfilePreferencesComponent,
|
||||
ProfileNameComponent,
|
||||
WebappConfigurationComponent,
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ import {ContextMenuService} from '@common/shared/services/context-menu.service';
|
||||
import {selectRouterConfig} from '@common/core/reducers/router-reducer';
|
||||
import {Observable, Subscription} from 'rxjs';
|
||||
import {ORCHESTRATION_ROUTES} from '~/features/workers-and-queues/workers-and-queues.consts';
|
||||
import {setContextMenu} from '@common/core/actions/router.actions';
|
||||
import {headerActions} from '@common/core/actions/router.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-orchestration',
|
||||
@@ -44,12 +44,12 @@ export class OrchestrationComponent implements OnInit, OnDestroy {
|
||||
isActive: route.header === entitiesType
|
||||
};
|
||||
});
|
||||
this.store.dispatch(setContextMenu({contextMenu}));
|
||||
this.store.dispatch(headerActions.setTabs({contextMenu}));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.unsubscribe();
|
||||
this.store.dispatch(setContextMenu({contextMenu: null}));
|
||||
this.store.dispatch(headerActions.setTabs({contextMenu: null}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import {TableModule} from 'primeng/table';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {OrchestrationComponent} from "~/features/workers-and-queues/orchestration.component";
|
||||
import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indicators/tooltip/show-tooltip-if-ellipsis.directive';
|
||||
import {RefreshButtonComponent} from '@common/shared/components/refresh-button/refresh-button.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -84,7 +85,8 @@ import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indic
|
||||
VerticalLabeledRowComponent,
|
||||
TableModule,
|
||||
MatInputModule,
|
||||
ShowTooltipIfEllipsisDirective
|
||||
ShowTooltipIfEllipsisDirective,
|
||||
RefreshButtonComponent
|
||||
],
|
||||
declarations: [
|
||||
OrchestrationComponent,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: '#{$icomoon-font-family}';
|
||||
src: url('./#{$icomoon-font-family}.ttf?hr04a1') format('truetype');
|
||||
src: url('./#{$icomoon-font-family}.ttf?luezm6') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
@@ -23,30 +23,49 @@
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.al-ico-queue {
|
||||
.al-ico-link-off {
|
||||
&:before {
|
||||
content: $al-ico-queue;
|
||||
content: $al-ico-link-off;
|
||||
}
|
||||
}
|
||||
.al-ico-weight {
|
||||
.al-ico-policy {
|
||||
&:before {
|
||||
content: $al-ico-weight;
|
||||
content: $al-ico-policy;
|
||||
}
|
||||
}
|
||||
.al-ico-info-group {
|
||||
&:before {
|
||||
content: $al-ico-info-group;
|
||||
}
|
||||
}
|
||||
.al-ico-advanced-filters {
|
||||
&:before {
|
||||
content: $al-ico-advanced-filters;
|
||||
}
|
||||
}
|
||||
.al-ico-triggers-scheduled {
|
||||
&:before {
|
||||
content: $al-ico-triggers-scheduled;
|
||||
}
|
||||
}
|
||||
.al-ico-queue {
|
||||
&:before {
|
||||
content: $al-ico-queue;
|
||||
}
|
||||
}
|
||||
.al-ico-link-plus {
|
||||
&:before {
|
||||
content: $al-ico-link-plus;
|
||||
content: $al-ico-link-plus;
|
||||
}
|
||||
}
|
||||
.al-ico-drag-vertical {
|
||||
&:before {
|
||||
content: $al-ico-drag-vertical;
|
||||
content: $al-ico-drag-vertical;
|
||||
}
|
||||
}
|
||||
.al-ico-drag-horizontal {
|
||||
&:before {
|
||||
content: $al-ico-drag-horizontal;
|
||||
content: $al-ico-drag-horizontal;
|
||||
}
|
||||
}
|
||||
.al-ico-admin-support {
|
||||
@@ -480,11 +499,6 @@
|
||||
content: $al-ico-model;
|
||||
}
|
||||
}
|
||||
.al-ico-temp-edit {
|
||||
&:before {
|
||||
content: $al-ico-temp-edit;
|
||||
}
|
||||
}
|
||||
.al-ico-dialog-x {
|
||||
&:before {
|
||||
content: $al-ico-dialog-x;
|
||||
@@ -615,11 +629,6 @@
|
||||
content: $al-ico-pytorch-icon;
|
||||
}
|
||||
}
|
||||
.al-ico-question-mark {
|
||||
&:before {
|
||||
content: $al-ico-question-mark;
|
||||
}
|
||||
}
|
||||
.al-ico-rectangle {
|
||||
&:before {
|
||||
content: $al-ico-rectangle;
|
||||
@@ -1147,11 +1156,6 @@
|
||||
content: $al-ico-no-scatter-graph;
|
||||
}
|
||||
}
|
||||
.al-ico-applications-exp {
|
||||
&:before {
|
||||
content: $al-ico-applications-exp;
|
||||
}
|
||||
}
|
||||
.al-ico-auto-refresh-play .path1 {
|
||||
&:before {
|
||||
content: $al-ico-auto-refresh-play-path1;
|
||||
|
||||
Binary file not shown.
@@ -1,8 +1,12 @@
|
||||
$icomoon-font-family: "trains" !default;
|
||||
$icomoon-font-path: "fonts" !default;
|
||||
|
||||
$al-ico-link-off: "\ea05";
|
||||
$al-ico-policy: "\e9d5";
|
||||
$al-ico-info-group: "\e944";
|
||||
$al-ico-advanced-filters: "\e911";
|
||||
$al-ico-triggers-scheduled: "\ea06";
|
||||
$al-ico-queue: "\ea01";
|
||||
$al-ico-weight: "\ea05";
|
||||
$al-ico-link-plus: "\ea02";
|
||||
$al-ico-drag-vertical: "\ea03";
|
||||
$al-ico-drag-horizontal: "\ea04";
|
||||
@@ -92,7 +96,6 @@ $al-ico-type-controller: "\ea2a";
|
||||
$al-ico-type-custom: "\ea2b";
|
||||
$al-ico-how-to1: "\e90f";
|
||||
$al-ico-model: "\e910";
|
||||
$al-ico-temp-edit: "\e911";
|
||||
$al-ico-dialog-x: "\e980";
|
||||
$al-ico-temp-image: "\e912";
|
||||
$al-ico-temp-list-alt: "\e913";
|
||||
@@ -119,7 +122,6 @@ $al-ico-next: "\e9af";
|
||||
$al-ico-plus: "\e9b8";
|
||||
$al-ico-polygon: "\e9b9";
|
||||
$al-ico-pytorch-icon: "\e9d4";
|
||||
$al-ico-question-mark: "\e9d5";
|
||||
$al-ico-rectangle: "\e9db";
|
||||
$al-ico-running: "\e9df";
|
||||
$al-ico-setup: "\ea0d";
|
||||
@@ -223,7 +225,6 @@ $al-ico-ico-chevron-up: "\e973";
|
||||
$al-ico-ico-chevron-down: "\e974";
|
||||
$al-ico-no-data-graph: "\e975";
|
||||
$al-ico-no-scatter-graph: "\e976";
|
||||
$al-ico-applications-exp: "\e944";
|
||||
$al-ico-auto-refresh-play-path1: "\e977";
|
||||
$al-ico-auto-refresh-play-path2: "\e978";
|
||||
$al-ico-auto-refresh-pause-path1: "\e979";
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
20
src/app/webapp-common/assets/icons/pallete-cursor.svg
Normal file
20
src/app/webapp-common/assets/icons/pallete-cursor.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<g>
|
||||
<rect x="4" y="19" width="26" height="11" rx="1" ry="1" fill="#fff" stroke-width="0"/>
|
||||
<path d="M29,19c.55,0,1,.45,1,1v9c0,.55-.45,1-1,1H5c-.55,0-1-.45-1-1v-9c0-.55.45-1,1-1h24M29,18H5c-1.1,0-2,.9-2,2v9c0,1.1.9,2,2,2h24c1.1,0,2-.9,2-2v-9c0-1.1-.9-2-2-2h0Z" fill="#2c3246" stroke-width="0"/>
|
||||
</g>
|
||||
<rect x="15" y="20" width="4" height="4" fill="#ff4949" stroke-width="0"/>
|
||||
<rect x="10" y="20" width="4" height="4" fill="#ff9100" stroke-width="0"/>
|
||||
<rect x="20" y="20" width="4" height="4" fill="#008adf" stroke-width="0"/>
|
||||
<rect x="25" y="20" width="4" height="4" fill="#54e360" stroke-width="0"/>
|
||||
<rect x="15" y="25" width="4" height="4" fill="#e80048" stroke-width="0"/>
|
||||
<rect x="10" y="25" width="4" height="4" fill="#ff4b00" stroke-width="0"/>
|
||||
<rect x="20" y="25" width="4" height="4" fill="#0065a3" stroke-width="0"/>
|
||||
<rect x="25" y="25" width="4" height="4" fill="#00ab5e" stroke-width="0"/>
|
||||
<rect x="5" y="20" width="4" height="4" fill="#ffd400" stroke-width="0"/>
|
||||
<rect x="5" y="25" width="4" height="4" fill="#ff9f04" stroke-width="0"/>
|
||||
<g>
|
||||
<path d="M3.51,11.52l1.23,1.73c.41.57,1.32.29,1.32-.42V2.86c0-1.14,1.66-1.14,1.66,0v6.95c0,.4.33.73.73.73h.31c.4,0,.73-.33.73-.73v-1.59c0-1.14,1.66-1.14,1.66,0v1.59c0,.4.33.73.73.73h.31c.4,0,.73-.33.73-.73v-.95c0-1.14,1.66-1.14,1.66,0v.95c0,.4.33.73.73.73h.31c.4,0,.73-.33.73-.73,0-1.14,1.66-1.14,1.66,0v3.82c0,.07,0,.14-.02.2l-1.25,5.4c-.09.39-.42.66-.81.66H7.83c-.26,0-.51-.13-.67-.35l-4.98-6.99c-.65-.92.68-1.94,1.34-1.03h0Z" fill="#fff" stroke-width="0"/>
|
||||
<path d="M6.89,2c.42,0,.83.29.83.86v6.95c0,.4.33.73.73.73h.31c.4,0,.73-.33.73-.73v-1.59c0-.57.42-.86.83-.86s.83.29.83.86v1.59c0,.4.33.73.73.73h.31c.4,0,.73-.33.73-.73v-.95c0-.57.42-.86.83-.86s.83.29.83.86v.95c0,.4.33.73.73.73h.31c.4,0,.73-.33.73-.73,0-.57.42-.86.83-.86s.83.29.83.86v3.82c0,.07,0,.14-.02.2l-1.25,5.4c-.09.39-.42.66-.81.66H7.83c-.26,0-.51-.13-.67-.35l-4.98-6.99c-.47-.66.09-1.39.69-1.39.23,0,.46.11.64.36l1.23,1.73c.15.21.37.31.58.31.37,0,.73-.28.73-.73V2.86c0-.57.42-.86.83-.86M6.89,1c-1.04,0-1.83.8-1.83,1.86v9.11l-.74-1.03c-.35-.49-.88-.78-1.46-.78-.69,0-1.34.41-1.66,1.03-.32.62-.26,1.34.16,1.93l4.98,6.99c.34.48.9.77,1.48.77h8.1c.85,0,1.59-.59,1.78-1.44l1.25-5.4c.03-.14.05-.28.05-.43v-3.82c0-1.06-.79-1.86-1.83-1.86-.7,0-1.29.36-1.6.92v-.02c0-1.06-.79-1.86-1.83-1.86-.72,0-1.31.37-1.61.95-.12-.92-.86-1.59-1.81-1.59-.7,0-1.29.36-1.6.92V2.86c0-1.06-.79-1.86-1.83-1.86h0Z" fill="#000" stroke-width="0"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
File diff suppressed because one or more lines are too long
8
src/app/webapp-common/assets/plotly-2.31.1.min.js
vendored
Normal file
8
src/app/webapp-common/assets/plotly-2.31.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -24,7 +24,6 @@
|
||||
[xAxisType]="xaxis"
|
||||
[isCompare]="true"
|
||||
[noMargins]="true"
|
||||
[legendConfiguration]="{noTextWrap: true}"
|
||||
[hideMaximize]="hideMaximize"
|
||||
(maximizeClicked)="maximize()">
|
||||
</sm-single-graph>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -94,7 +94,7 @@ export const BASE_ENV: Environment = {
|
||||
whiteLabelLoginTitle: null,
|
||||
whiteLabelLoginSubtitle: null,
|
||||
whiteLabelSlogan: null,
|
||||
plotlyURL: 'app/webapp-common/assets/plotly-2.25.2.min.js',
|
||||
plotlyURL: 'app/webapp-common/assets/plotly-2.31.1.min.js',
|
||||
docsLink: '/docs',
|
||||
useFilesProxy: true,
|
||||
branding: {logo: 'assets/logo-white.svg?v=7', logoSmall: 'assets/c-logo.svg?=2'},
|
||||
|
||||
@@ -107,6 +107,7 @@ $sm-neon-theme: mat.define-dark-theme((
|
||||
@include mat.divider-color($dark-theme);
|
||||
@include mat.checkbox-theme($dark-theme);
|
||||
@include mat.list-theme($dark-theme);
|
||||
@include mat.stepper-theme($dark-theme);
|
||||
|
||||
--mdc-typography-body1-letter-spacing: 0;
|
||||
--mdc-typography-button-letter-spacing: 0;
|
||||
@@ -147,6 +148,7 @@ $sm-neon-theme: mat.define-dark-theme((
|
||||
@include mat.divider-color($light-theme);
|
||||
@include mat.checkbox-color($sm-theme);
|
||||
@include mat.list-color($light-theme);
|
||||
@include mat.stepper-color($sm-theme);
|
||||
|
||||
--mdc-typography-body1-letter-spacing: 0;
|
||||
--mdc-typography-button-letter-spacing: 0;
|
||||
@@ -195,6 +197,52 @@ $sm-neon-theme: mat.define-dark-theme((
|
||||
font-size: 12px;
|
||||
font-family: $font-family-monospace;
|
||||
}
|
||||
|
||||
// MAT STEPPER
|
||||
// ------------------------
|
||||
|
||||
.mat-step-header.mat-accent {
|
||||
--mat-stepper-header-selected-state-label-text-color: #{$blue-300};
|
||||
--mat-stepper-header-focus-state-layer-color: transparent;
|
||||
}
|
||||
|
||||
.mat-horizontal-stepper-header-container {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.mat-ripple {display: none;}
|
||||
|
||||
.mat-step-header:hover:not([aria-disabled]),
|
||||
.mat-step-header:hover[aria-disabled=false] {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.mat-step-icon {
|
||||
--mat-stepper-header-icon-background-color: #{$white};
|
||||
--mat-stepper-header-icon-foreground-color: #{$blue-400};
|
||||
border: 2px solid $blue-300;
|
||||
transition: border 0.3s;
|
||||
}
|
||||
|
||||
.mat-step-icon-selected,
|
||||
.mat-step-icon-state-edit {
|
||||
border-color: $purple;
|
||||
}
|
||||
|
||||
.mat-step-label {
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.mat-step-text-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mat-step-label.mat-step-label-active.mat-step-label-selected {
|
||||
--mat-stepper-header-selected-state-label-text-color: #{$purple};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -321,10 +369,6 @@ mat-expansion-panel {
|
||||
}
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header {
|
||||
font-family: $font-family-base, sans-serif;
|
||||
}
|
||||
|
||||
.al-empty-collapse .mat-expansion-panel-header-title .al-header.sub-header {
|
||||
color: #ced1db;
|
||||
}
|
||||
@@ -607,6 +651,7 @@ html {
|
||||
|
||||
&.custom-columns {
|
||||
width: 370px;
|
||||
|
||||
}
|
||||
|
||||
sm-checkbox-three-state-list {
|
||||
@@ -623,16 +668,24 @@ html {
|
||||
}
|
||||
|
||||
.sm-menu-header {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: $blue-25;
|
||||
color: $blue-400;
|
||||
border-bottom: 1px solid $blue-200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $blue-200;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid $blue-500;
|
||||
font-size: 14px;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.light-theme {
|
||||
.mat-mdc-menu-content {
|
||||
.sm-menu-header {
|
||||
background: $blue-25;
|
||||
color: $blue-400;
|
||||
border-bottom: 1px solid $blue-200;
|
||||
}
|
||||
|
||||
.mat-mdc-menu-item {
|
||||
&.cdk-keyboard-focused, &.cdk-program-focused {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
@@ -659,7 +712,7 @@ html {
|
||||
min-height: 40px;
|
||||
--mat-menu-item-label-text-line-height: 20px;
|
||||
--mat-menu-item-label-text-size: 14px;
|
||||
padding: 0 32px 0 12px;
|
||||
padding: 0 12px;
|
||||
border-radius: 4px;
|
||||
|
||||
.mat-icon {
|
||||
@@ -705,10 +758,17 @@ input[type=number] {
|
||||
z-index: 11 !important;
|
||||
}
|
||||
|
||||
|
||||
// SPILTTER
|
||||
// ------------------------------
|
||||
|
||||
as-split {
|
||||
&.as-dragging {
|
||||
.as-split-area {
|
||||
transition: unset;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.as-split-gutter-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,29 +778,114 @@ as-split {
|
||||
|
||||
&.as-horizontal {
|
||||
& > .as-split-gutter {
|
||||
height: unset !important;
|
||||
position: relative;
|
||||
background-color: $dark-border !important;
|
||||
|
||||
&::before {
|
||||
// splitter vertical line
|
||||
content: "";
|
||||
display: block;
|
||||
width: 2px;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -1px;
|
||||
background-color: transparent;
|
||||
}
|
||||
&:hover::before {
|
||||
background-color: $purple;
|
||||
transition: background-color 0.2s ease 0.2s;
|
||||
}
|
||||
|
||||
&::after {
|
||||
// splitter area handle
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: 101;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.as-split-gutter-icon {
|
||||
background-color: $blue-900;
|
||||
border-left: $blue-600 solid 1px;
|
||||
border-right: $blue-600 solid 1px;
|
||||
// arrow circle icon
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 101;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
background-image: none !important;
|
||||
border: none !important;
|
||||
border-radius: 20px;
|
||||
&::after {
|
||||
// arrow
|
||||
content: "";
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
border: solid lighten($purple, 30%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.closed.as-horizontal {
|
||||
> .as-split-gutter {
|
||||
.as-split-gutter-icon {
|
||||
display: block;
|
||||
left: -10px;
|
||||
background: $purple !important;
|
||||
&::after {
|
||||
// arrow direction right
|
||||
left: 6px;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
> .as-split-area:nth-child(1) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
&.opened.as-horizontal {
|
||||
> .as-split-gutter {
|
||||
&:hover::before {
|
||||
left: auto;
|
||||
right: -1px;
|
||||
}
|
||||
.as-split-gutter-icon {
|
||||
display: block;
|
||||
right: -10px;
|
||||
background: $purple !important;
|
||||
&::after {
|
||||
// arrow direction left
|
||||
left: 8px;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
> .as-split-area:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.dark-theme .light-theme .as-horizontal > .as-split-gutter {
|
||||
background-color: transparent !important;
|
||||
as-split.as-horizontal.light-theme > .as-split-gutter {
|
||||
background-color: #dee1e9 !important;
|
||||
}
|
||||
|
||||
.as-split-gutter-icon {
|
||||
background-color: transparent !important;
|
||||
border-left: solid 1px #DEE1E9;
|
||||
border-right: none;
|
||||
background-image: none !important;
|
||||
|
||||
&:hover {
|
||||
border-left: $purple solid 2px;
|
||||
}
|
||||
// splitter shown on light background
|
||||
as-split.as-horizontal:not(.closed, .opened) as-split-area.light-theme + .as-split-gutter {
|
||||
&::before {
|
||||
left: 1px;
|
||||
width: 3px;
|
||||
}
|
||||
&::after {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -791,6 +936,10 @@ $type-colors: (
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-overlay-pane.mat-mdc-dialog-panel {
|
||||
--mat-dialog-container-max-width: 100vw;
|
||||
}
|
||||
|
||||
.image-viewer-dialog {
|
||||
.mat-mdc-dialog-container {
|
||||
padding: 0;
|
||||
@@ -982,3 +1131,13 @@ button.btn.button-outline-dark {
|
||||
--mdc-dialog-container-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-check:checked + .dark-theme .btn,
|
||||
:not(.btn-check) + .dark-theme .btn:active
|
||||
{
|
||||
--bs-btn-active-border-color: transparent;
|
||||
}
|
||||
|
||||
//.pallete-cursor {
|
||||
// cursor: url(../assets/icons/pallete-cursor.svg) 6 0, pointer;
|
||||
//}
|
||||
|
||||
@@ -77,11 +77,29 @@ export const getSignedUrl = createAction(
|
||||
error?: boolean;
|
||||
}}>()
|
||||
);
|
||||
export const signUrls = createAction(
|
||||
AUTH_PREFIX + '[sign urls]',
|
||||
props<{sign: {
|
||||
url: string;
|
||||
config?: {
|
||||
skipLocalFile?: boolean;
|
||||
skipFileServer?: boolean;
|
||||
disableCache?: number;
|
||||
dprsUrl?: string | boolean;
|
||||
error?: boolean;
|
||||
}
|
||||
}[]}>()
|
||||
);
|
||||
export const setSignedUrl = createAction(
|
||||
AUTH_PREFIX + '[set signed url]',
|
||||
props<{url: string; signed: string; expires: number}>()
|
||||
);
|
||||
|
||||
export const setSignedUrls = createAction(
|
||||
AUTH_PREFIX + '[set signed urls]',
|
||||
props<{signed: {url: string; signed: string; expires: number}[]}>()
|
||||
);
|
||||
|
||||
export const removeSignedUrl = createAction(
|
||||
AUTH_PREFIX + '[remove signed url]',
|
||||
props<{url: string}>()
|
||||
|
||||
@@ -200,6 +200,10 @@ export const setHideExamples = createAction(
|
||||
PROJECTS_PREFIX + ' [set hide examples]',
|
||||
props<{ hide: boolean }>()
|
||||
);
|
||||
export const setBlockUserScript = createAction(
|
||||
PROJECTS_PREFIX + ' [set block users scripts]',
|
||||
props<{ block: boolean }>()
|
||||
);
|
||||
|
||||
export const setDefaultNestedModeForFeature = createAction(
|
||||
PROJECTS_PREFIX + ' [set defaultNestedModeForFeature]',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {NAVIGATION_PREFIX} from '~/app.constants';
|
||||
import {createAction, props} from '@ngrx/store';
|
||||
import {createAction, createActionGroup, props} from '@ngrx/store';
|
||||
import {Params} from '@angular/router';
|
||||
import {FilterMetadata} from 'primeng/api/filtermetadata';
|
||||
import {SortMeta} from 'primeng/api';
|
||||
@@ -7,7 +7,6 @@ import {CrumbTypeEnum, IBreadcrumbsLink} from '@common/layout/breadcrumbs/breadc
|
||||
import {HeaderNavbarTabConfig} from '@common/layout/header-navbar-tabs/header-navbar-tabs-config.types';
|
||||
|
||||
export const BREADCRUMBS_PREFIX = 'BREADCRUMBS_';
|
||||
export const CONTEXT_MENU_PREFIX = 'CONTEXT_';
|
||||
|
||||
|
||||
export const navigationEnd = createAction(NAVIGATION_PREFIX + 'NAVIGATION_END');
|
||||
@@ -39,7 +38,7 @@ export const setURLParams = createAction(
|
||||
|
||||
export const setBreadcrumbs = createAction(
|
||||
BREADCRUMBS_PREFIX + 'SET_BREADCRUMBS',
|
||||
props<{ breadcrumbs: IBreadcrumbsLink[][]}>()
|
||||
props<{ breadcrumbs: IBreadcrumbsLink[][], workspaceNeutral?: boolean}>()
|
||||
);
|
||||
|
||||
export const setTypeBreadcrumbs = createAction(
|
||||
@@ -47,13 +46,15 @@ export const setTypeBreadcrumbs = createAction(
|
||||
props<{ breadcrumb: IBreadcrumbsLink; type?: CrumbTypeEnum }>()
|
||||
);
|
||||
|
||||
export const setContextMenu = createAction(
|
||||
CONTEXT_MENU_PREFIX + 'SET_CONTEXT_MENU',
|
||||
props<{ contextMenu: HeaderNavbarTabConfig[]}>()
|
||||
);
|
||||
|
||||
export const setContextMenuActiveFeature = createAction (
|
||||
CONTEXT_MENU_PREFIX + 'SET_CONTEXT_MENU_ACTIVE_FEATURE',
|
||||
props<{ activeFeature: string}>()
|
||||
export const setWorkspaceNeutral = createAction(
|
||||
BREADCRUMBS_PREFIX + 'SET_TYPE_BREADCRUMBS',
|
||||
props<{ neutral: boolean }>()
|
||||
);
|
||||
|
||||
export const headerActions = createActionGroup({
|
||||
source: 'header tabs',
|
||||
events: {
|
||||
setTabs: props<{ contextMenu: HeaderNavbarTabConfig[]}>(),
|
||||
setActiveTab: props<{ activeFeature: string}>()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export const setSelectedWorkspaceTab = createAction(
|
||||
|
||||
export const setFilterByUser = createAction(
|
||||
USERS_PREFIX +'SET_FILTERED_BY_USER',
|
||||
props<{showOnlyUserWork: boolean}>()
|
||||
props<{showOnlyUserWork: boolean, feature: string}>()
|
||||
);
|
||||
|
||||
export const setUserWorkspacesFromUser = createAction(USERS_PREFIX + ' set user workspaces from current user');
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Actions, concatLatestFrom, createEffect, ofType} from '@ngrx/effects';
|
||||
import {Actions, 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 {setCredentialLabel, setSignedUrls} 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';
|
||||
import {AuthGetCredentialsResponse} from '~/business-logic/model/auth/authGetCredentialsResponse';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {Action, Store} from '@ngrx/store';
|
||||
import {selectCurrentUser} from '../reducers/users-reducer';
|
||||
import {GetCurrentUserResponseUserObject} from '~/business-logic/model/users/getCurrentUserResponseUserObject';
|
||||
import {AdminService} from '~/shared/services/admin.service';
|
||||
import {selectDontShowAgainForBucketEndpoint, selectS3BucketCredentialsBucketCredentials, selectSignedUrl} from '@common/core/reducers/common-auth-reducer';
|
||||
import {EMPTY, of} from 'rxjs';
|
||||
import {
|
||||
selectDontShowAgainForBucketEndpoint,
|
||||
selectS3BucketCredentialsBucketCredentials,
|
||||
selectSignedUrl,
|
||||
selectSignedUrls
|
||||
} from '@common/core/reducers/common-auth-reducer';
|
||||
import {EMPTY, forkJoin, of} from 'rxjs';
|
||||
import {S3AccessDialogData, S3AccessResolverComponent} from '@common/layout/s3-access-resolver/s3-access-resolver.component';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {isGoogleCloudUrl, SignResponse} from '@common/settings/admin/base-admin-utils';
|
||||
import {isFileserverUrl} from '~/shared/utils/url';
|
||||
import {selectRouterQueryParams} from '@common/core/reducers/router-reducer';
|
||||
import {concatLatestFrom} from '@ngrx/operators';
|
||||
|
||||
@Injectable()
|
||||
export class CommonAuthEffects {
|
||||
private signAfterPopup: (ReturnType<typeof authActions.getSignedUrl>)[] = [];
|
||||
private signAfterPopup: Action[] = [];
|
||||
private openPopup: { [bucketName: string]: boolean } = {};
|
||||
|
||||
constructor(
|
||||
@@ -148,6 +154,55 @@ export class CommonAuthEffects {
|
||||
)
|
||||
));
|
||||
|
||||
signUrls = createEffect(() => {
|
||||
return this.actions.pipe(
|
||||
ofType(authActions.signUrls),
|
||||
filter(action => action.sign.length > 0),
|
||||
concatLatestFrom(() => this.store.select(selectSignedUrls)),
|
||||
mergeMap(([action, prevSigns]) =>
|
||||
forkJoin(action.sign.map(req =>
|
||||
of(action).pipe(
|
||||
switchMap(() => this.adminService.signUrlIfNeeded(req.url, req.config, prevSigns[req.url])),
|
||||
map(res => ({res, orgUrl: req.url}))
|
||||
)
|
||||
)).pipe(
|
||||
switchMap((responses) => {
|
||||
const groups = responses
|
||||
.filter(res => !!res.res)
|
||||
.reduce((acc, res) => {
|
||||
if (Object.hasOwn(acc, res.res.type)) {
|
||||
acc[res.res.type].push(res);
|
||||
} else {
|
||||
acc[res.res.type] = [res];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.keys(groups).map(type => {
|
||||
switch (type) {
|
||||
case 'popup': {
|
||||
this.signAfterPopup.push(action);
|
||||
const res = groups[type][0].res;
|
||||
return [authActions.showS3PopUp({
|
||||
credentials: res.bucket,
|
||||
provider: res.provider,
|
||||
credentialsError: null
|
||||
})];
|
||||
}
|
||||
case 'sign':
|
||||
return setSignedUrls({signed: groups['sign'].map((res: {res: SignResponse, orgUrl: string}) =>
|
||||
({url: res.orgUrl, signed: res.res.signed, expires: res.res.expires}))});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(a => !!a)
|
||||
.flat();
|
||||
})
|
||||
)
|
||||
),
|
||||
)
|
||||
});
|
||||
|
||||
s3popup = createEffect(() => this.actions.pipe(
|
||||
ofType(authActions.showS3PopUp),
|
||||
concatLatestFrom(() => this.store.select(selectDontShowAgainForBucketEndpoint)),
|
||||
@@ -163,16 +218,17 @@ export class CommonAuthEffects {
|
||||
return this.matDialog.open(S3AccessResolverComponent, {data: action as S3AccessDialogData, maxWidth: 700}).afterClosed().pipe(
|
||||
concatLatestFrom(() => this.store.select(selectS3BucketCredentialsBucketCredentials)),
|
||||
switchMap(([data, bucketCredentials]) => {
|
||||
window.setTimeout(() => this.signAfterPopup = []);
|
||||
const actions = [...this.signAfterPopup];
|
||||
this.signAfterPopup = [];
|
||||
if (data) {
|
||||
if (!data.success) {
|
||||
const emptyCredentials = bucketCredentials.find((cred => cred?.Bucket === data.bucket)) === undefined;
|
||||
const dontAskAgainForBucketName = emptyCredentials ? '' : data.bucket + data.endpoint;
|
||||
return [authActions.cancelS3Credentials({dontAskAgainForBucketName})];
|
||||
}
|
||||
return [authActions.saveS3Credentials({newCredential: data}), ...this.signAfterPopup];
|
||||
return [authActions.saveS3Credentials({newCredential: data}), ...actions];
|
||||
}
|
||||
return [...this.signAfterPopup];
|
||||
return actions;
|
||||
}),
|
||||
finalize(() => action?.credentials?.Bucket && delete this.openPopup[action.credentials.Bucket])
|
||||
);
|
||||
|
||||
@@ -56,13 +56,13 @@ import {ProjectsGetAllResponseSingle} from '~/business-logic/model/projects/proj
|
||||
import {rootProjectsPageSize} from '@common/constants';
|
||||
import {HTTP} from '~/app.constants';
|
||||
import {cleanTag} from '@common/shared/utils/helpers.util';
|
||||
import {selectProjectType} from '~/core/reducers/view.reducer';
|
||||
import {selectExperimentsTableFilters} from '@common/experiments/reducers';
|
||||
import {Params} from '@angular/router';
|
||||
import {selectCompareAddTableFilters} from '@common/experiments-compare/reducers';
|
||||
import {selectTableFilters} from '@common/models/reducers';
|
||||
import {selectSelectModelTableFilters} from '@common/select-model/select-model.reducer';
|
||||
import {TagColorMenuComponent} from '@common/shared/ui-components/tags/tag-color-menu/tag-color-menu.component';
|
||||
import {selectProjectType} from '@common/core/reducers/view.reducer';
|
||||
|
||||
export const ALL_PROJECTS_OBJECT = {id: '*', name: 'All Experiments'};
|
||||
|
||||
|
||||
@@ -6,20 +6,16 @@ import {catchError} from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
import {selectCurrentUser} from '../reducers/users-reducer';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {GetCurrentUserResponseUserObject} from '~/business-logic/model/users/getCurrentUserResponseUserObject';
|
||||
import {resetCurrentUser} from '~/core/actions/users.action';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
|
||||
@Injectable()
|
||||
export class WebappInterceptor implements HttpInterceptor {
|
||||
protected user: GetCurrentUserResponseUserObject;
|
||||
protected router: Router;
|
||||
protected store: Store;
|
||||
protected router = inject(Router);
|
||||
protected store = inject(Store);
|
||||
protected dialog = inject(MatDialog);
|
||||
protected user = this.store.selectSignal(selectCurrentUser);
|
||||
|
||||
constructor() {
|
||||
this.router = inject(Router);
|
||||
this.store = inject(Store);
|
||||
this.store.select(selectCurrentUser).subscribe(user => this.user = user);
|
||||
}
|
||||
|
||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
request = request.clone({
|
||||
@@ -43,11 +39,12 @@ export class WebappInterceptor implements HttpInterceptor {
|
||||
if (err.status === 401 && (
|
||||
['/dashboard'].includes(redirectUrl) ||
|
||||
!environment.autoLogin ||
|
||||
(environment.autoLogin && this.user)
|
||||
(environment.autoLogin && this.user())
|
||||
)) {
|
||||
if (redirectUrl.indexOf('/signup') === -1 && redirectUrl.indexOf('/login') === -1) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
this.store.dispatch(resetCurrentUser());
|
||||
this.dialog.closeAll();
|
||||
this.router.navigate(['login'], {queryParams: {redirect: redirectUrl}, replaceUrl: true});
|
||||
}
|
||||
return throwError(() => err);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {createSelector, on, ReducerTypes, select, Store} from '@ngrx/store';
|
||||
import {ActionCreator, createSelector, on, ReducerTypes, select, Store} from '@ngrx/store';
|
||||
import {filter, map, takeWhile, timeout} from 'rxjs/operators';
|
||||
import {isEqual} from 'lodash-es';
|
||||
import {
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
cancelS3Credentials,
|
||||
removeCredential, removeSignedUrl, resetCredential, resetCredentials,
|
||||
resetDontShowAgainForBucketEndpoint,
|
||||
saveS3Credentials, setCredentialLabel, setS3Credentials, setSignedUrl,
|
||||
saveS3Credentials, setCredentialLabel, setS3Credentials, setSignedUrl, setSignedUrls,
|
||||
showLocalFilePopUp,
|
||||
updateAllCredentials,
|
||||
updateS3Credential
|
||||
@@ -150,9 +150,16 @@ export const commonAuthReducer = [
|
||||
credentials: {[action.credentials[0]?.company || action.workspace]: action.credentials, ...action.extra}, revokeSucceed: false
|
||||
})),
|
||||
on(setSignedUrl, (state, action) => ({...state, signedUrls: {...state.signedUrls, [action.url]: {signed: action.signed, expires: action.expires}}})),
|
||||
on(setSignedUrls, (state, action) => ({...state, signedUrls: {
|
||||
...state.signedUrls,
|
||||
...action.signed.reduce((acc, res) => {
|
||||
acc[res.url] = {signed: res.signed, expires: res.expires};
|
||||
return acc;
|
||||
}, {})
|
||||
}})),
|
||||
on(removeSignedUrl, (state, action) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {[action.url]: remove, ...rest} = state.signedUrls;
|
||||
return {...state, signedUrls: rest};
|
||||
}),
|
||||
] as ReducerTypes<AuthState, any>[];
|
||||
] as ReducerTypes<AuthState, ActionCreator[]>[];
|
||||
|
||||
@@ -8,11 +8,10 @@ import {User} from '~/business-logic/model/users/user';
|
||||
import {ProjectsGetAllResponseSingle} from '~/business-logic/model/projects/projectsGetAllResponseSingle';
|
||||
import {selectRouterParams} from '@common/core/reducers/router-reducer';
|
||||
import {IBreadcrumbsLink, IBreadcrumbsOptions} from '@common/layout/breadcrumbs/breadcrumbs.component';
|
||||
import {selectProjectType} from '~/core/reducers/view.reducer';
|
||||
import {uniqBy} from 'lodash-es';
|
||||
import {ISmCol} from '@common/shared/ui-components/data/table/table.consts';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {isReadOnly} from '@common/shared/utils/is-read-only';
|
||||
import {selectProjectType} from '@common/core/reducers/view.reducer';
|
||||
|
||||
|
||||
export interface ScatterPlotPoint {
|
||||
@@ -60,6 +59,7 @@ export interface RootProjects {
|
||||
extraUsers: User[];
|
||||
showHidden: boolean;
|
||||
hideExamples: boolean;
|
||||
blockUserScript: boolean;
|
||||
mainPageTagsFilter: { [Feature: string]: { tags: string[]; filterMatchMode: string } };
|
||||
mainPageTagsFilterMatchMode: string;
|
||||
defaultNestedModeForFeature: { [feature: string]: boolean };
|
||||
@@ -90,6 +90,7 @@ const initRootProjects: RootProjects = {
|
||||
extraUsers: [],
|
||||
showHidden: false,
|
||||
hideExamples: false,
|
||||
blockUserScript: false,
|
||||
defaultNestedModeForFeature: {},
|
||||
selectedSubFeature: null,
|
||||
breadcrumbOptions: null,
|
||||
@@ -242,6 +243,7 @@ export const projectsReducer = createReducer(
|
||||
on(projectsActions.setProjectExtraUsers, (state, action): RootProjects => ({...state, extraUsers: action.users})),
|
||||
on(projectsActions.setShowHidden, (state, action): RootProjects => ({...state, showHidden: action.show})),
|
||||
on(projectsActions.setHideExamples, (state, action): RootProjects => ({...state, hideExamples: action.hide})),
|
||||
on(projectsActions.setBlockUserScript, (state, action): RootProjects => ({...state, blockUserScript: action.block})),
|
||||
on(projectsActions.setDefaultNestedModeForFeature, (state, action): RootProjects => ({
|
||||
...state,
|
||||
defaultNestedModeForFeature: {...state.defaultNestedModeForFeature, [action.feature]: action.isNested}
|
||||
@@ -261,5 +263,6 @@ export const selectShowHidden = createSelector(projects, selectSelectedProject,
|
||||
(state, selectedProject) => (state?.showHidden || selectedProject?.system_tags?.includes('hidden')));
|
||||
|
||||
export const selectHideExamples = createSelector(projects, state => state?.hideExamples);
|
||||
export const selectBlockUserScript = createSelector(projects, state => state?.blockUserScript);
|
||||
export const selectDefaultNestedModeForFeature = createSelector(projects, state => state?.defaultNestedModeForFeature);
|
||||
export const selectProjectsOptionsScrollId = createSelector(projects, state => state?.projectsOptionsScrollId);
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import {ActionCreator, createSelector, on, ReducerTypes} from '@ngrx/store';
|
||||
import {
|
||||
logout,
|
||||
setFilterByUser,
|
||||
setApiVersion,
|
||||
fetchCurrentUser,
|
||||
setCurrentUserName
|
||||
} from '../actions/users.actions';
|
||||
import {fetchCurrentUser, logout, setApiVersion, setCurrentUserName, setFilterByUser} from '../actions/users.actions';
|
||||
import {GetCurrentUserResponseUserObject} from '~/business-logic/model/users/getCurrentUserResponseUserObject';
|
||||
import {
|
||||
GetCurrentUserResponseUserObjectCompany
|
||||
@@ -17,6 +11,8 @@ 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;
|
||||
import {selectProjectType} from '@common/core/reducers/view.reducer';
|
||||
|
||||
|
||||
export interface UsersState {
|
||||
currentUser: GetCurrentUserResponseUserObject;
|
||||
@@ -24,7 +20,7 @@ export interface UsersState {
|
||||
userWorkspaces: OrganizationGetUserCompaniesResponseCompanies[];
|
||||
selectedWorkspaceTab: GetCurrentUserResponseUserObjectCompany;
|
||||
workspaces: GetCurrentUserResponseUserObjectCompany[];
|
||||
showOnlyUserWork: boolean;
|
||||
showOnlyUserWork: { [key: string]: boolean };
|
||||
serverVersions: { server: string; api: string };
|
||||
gettingStarted: GettingStarted;
|
||||
settings: UsersGetCurrentUserResponseSettings;
|
||||
@@ -36,7 +32,7 @@ export const initUsers: UsersState = {
|
||||
userWorkspaces: [],
|
||||
selectedWorkspaceTab: null,
|
||||
workspaces: [],
|
||||
showOnlyUserWork: false,
|
||||
showOnlyUserWork: {},
|
||||
serverVersions: null,
|
||||
gettingStarted: null,
|
||||
settings: null,
|
||||
@@ -52,7 +48,6 @@ export const selectActiveWorkspaceTier = createSelector(selectActiveWorkspace, w
|
||||
export const selectUserWorkspaces = createSelector(users, state => state.userWorkspaces);
|
||||
export const selectSelectedWorkspaceTab = createSelector(users, state => state.selectedWorkspaceTab);
|
||||
export const selectWorkspaces = createSelector(users, state => state.workspaces);
|
||||
export const selectShowOnlyUserWork = createSelector(users, state => state.showOnlyUserWork);
|
||||
export const selectServerVersions = createSelector(users, state => state.serverVersions);
|
||||
export const selectGettingStarted = createSelector(users, state => state.gettingStarted);
|
||||
export const selectWorkspaceOwner = createSelector(selectActiveWorkspace, selectUserWorkspaces, (active, workspaces) => {
|
||||
@@ -62,6 +57,7 @@ export const selectWorkspaceOwner = createSelector(selectActiveWorkspace, select
|
||||
}
|
||||
return null;
|
||||
});
|
||||
export const selectShowOnlyUserWork = createSelector(users, selectProjectType, (state, projectType) => projectType? state.showOnlyUserWork[projectType]: false);
|
||||
|
||||
export const usersReducerFunctions = [
|
||||
on(fetchCurrentUser, state => ({...state})),
|
||||
@@ -75,7 +71,7 @@ export const usersReducerFunctions = [
|
||||
currentUser: null
|
||||
})),
|
||||
on(setFilterByUser, (state, action) => {
|
||||
return ({...state, showOnlyUserWork: action.showOnlyUserWork});
|
||||
return ({...state, showOnlyUserWork: {...state.showOnlyUserWork, [action.feature]: action.showOnlyUserWork}});
|
||||
}),
|
||||
on(setApiVersion, (state, action) => ({...state, serverVersions: action.serverVersions}))
|
||||
] as ReducerTypes<UsersState, ActionCreator[]>[];
|
||||
|
||||
@@ -4,13 +4,14 @@ import {apiRequest, requestFailed} from '@common/core/actions/http.actions';
|
||||
import {Ace} from 'ace-builds';
|
||||
import {IBreadcrumbsLink} from '@common/layout/breadcrumbs/breadcrumbs.component';
|
||||
import {
|
||||
headerActions,
|
||||
setBreadcrumbs,
|
||||
setContextMenu,
|
||||
setContextMenuActiveFeature,
|
||||
setTypeBreadcrumbs
|
||||
} from '@common/core/actions/router.actions';
|
||||
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
|
||||
import {HeaderNavbarTabConfig} from '@common/layout/header-navbar-tabs/header-navbar-tabs-config.types';
|
||||
import {selectRouterConfig} from '@common/core/reducers/router-reducer';
|
||||
import {activeFeatureToProjectType, routeConfToProjectType} from '~/features/projects/projects-page.utils';
|
||||
|
||||
export interface ViewState {
|
||||
loading: { [endpoint: string]: boolean };
|
||||
@@ -37,6 +38,7 @@ export interface ViewState {
|
||||
contextMenu: HeaderNavbarTabConfig[];
|
||||
tableCardsCollapsed: {[entity: string]: boolean};
|
||||
contextMenuActiveFeature: string;
|
||||
workspaceNeutral: boolean;
|
||||
}
|
||||
|
||||
export const initViewState: ViewState = {
|
||||
@@ -66,7 +68,8 @@ export const initViewState: ViewState = {
|
||||
breadcrumbs: [[{}]],
|
||||
tableCardsCollapsed: {},
|
||||
contextMenu: null,
|
||||
contextMenuActiveFeature: null
|
||||
contextMenuActiveFeature: null,
|
||||
workspaceNeutral: false,
|
||||
};
|
||||
|
||||
export const views = state => state.views as ViewState;
|
||||
@@ -98,6 +101,9 @@ export const selectBreadcrumbs = createSelector(views, state => state && state.b
|
||||
export const selectTableCardsCollapsed = (entityType: EntityTypeEnum) => createSelector(views, state => state.tableCardsCollapsed[entityType]);
|
||||
export const selectContextMenu = createSelector(views, state => state && state.contextMenu);
|
||||
export const selectActiveFeature = createSelector(views, state => state && state.contextMenuActiveFeature);
|
||||
export const selectWorkspaceNeutral= createSelector(views, state => state?.workspaceNeutral);
|
||||
export const selectProjectType = createSelector(selectRouterConfig, selectActiveFeature,
|
||||
(config, activeFeature) => (config && routeConfToProjectType(config)) ?? activeFeatureToProjectType(activeFeature));
|
||||
|
||||
|
||||
export const viewReducers = [
|
||||
@@ -154,13 +160,13 @@ export const viewReducers = [
|
||||
...state,
|
||||
neverShowPopupAgain: action.reset ? state.neverShowPopupAgain.filter(popups => popups !== action.popupId) : Array.from(new Set([...state.neverShowPopupAgain, action.popupId]))
|
||||
})),
|
||||
on(setBreadcrumbs, (state, action) => ({
|
||||
...state, breadcrumbs: action.breadcrumbs
|
||||
on(setBreadcrumbs, (state, action): ViewState => ({
|
||||
...state, breadcrumbs: action.breadcrumbs, ...(action.workspaceNeutral !== undefined && {workspaceNeutral: action.workspaceNeutral})
|
||||
})),
|
||||
on(setContextMenu, (state, action) => ({
|
||||
on(headerActions.setTabs, (state, action) => ({
|
||||
...state, contextMenu: action.contextMenu
|
||||
})),
|
||||
on(setContextMenuActiveFeature, (state, action) => ({
|
||||
on(headerActions.setActiveTab, (state, action) => ({
|
||||
...state, contextMenuActiveFeature: action.activeFeature
|
||||
})),
|
||||
on(setTypeBreadcrumbs, (state, action) => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {createFeatureSelector, createReducer, createSelector, on, ReducerTypes} from '@ngrx/store';
|
||||
import {ActionCreator, createFeatureSelector, createReducer, createSelector, on, ReducerTypes} from '@ngrx/store';
|
||||
import {Project} from '~/business-logic/model/projects/project';
|
||||
import {Task} from '~/business-logic/model/tasks/task';
|
||||
import {User} from '~/business-logic/model/users/user';
|
||||
@@ -23,7 +23,7 @@ export const dashboardInitState: DashboardState = {
|
||||
export const commonDashboardReducers = [
|
||||
on(setRecentProjects, (state, action) => ({...state, recentProjects: action.projects})),
|
||||
on(setRecentExperiments, (state, action) => ({...state, recentTasks: action.experiments})),
|
||||
] as ReducerTypes<DashboardState, any>[];
|
||||
] as ReducerTypes<DashboardState, ActionCreator[]>[];
|
||||
|
||||
export const commonDashboardReducer = createReducer(
|
||||
dashboardInitState,
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
Output,
|
||||
EventEmitter,
|
||||
AfterViewInit,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
inject
|
||||
inject, viewChild, effect
|
||||
} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||
import {fromEvent, Observable, Subscription} from 'rxjs';
|
||||
import {fromEvent} from 'rxjs';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {Project} from '~/business-logic/model/projects/project';
|
||||
import {selectRecentProjects, selectRecentProjectsCount} from '../../common-dashboard.reducer';
|
||||
@@ -22,40 +18,47 @@ 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 {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector : 'sm-dashboard-projects',
|
||||
templateUrl: './dashboard-projects.component.html',
|
||||
styleUrls : ['./dashboard-projects.component.scss']
|
||||
})
|
||||
export class DashboardProjectsComponent implements AfterViewInit, OnDestroy {
|
||||
export class DashboardProjectsComponent {
|
||||
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;
|
||||
|
||||
@Output() width = new EventEmitter<number>();
|
||||
private header = viewChild<ElementRef<HTMLDivElement>>('header');
|
||||
|
||||
constructor() {
|
||||
this.store.dispatch(resetSelectedProject());
|
||||
this.store.select(selectCurrentUser)
|
||||
.pipe(filter(user => !!user), take(1))
|
||||
.subscribe(() => this.store.dispatch(getRecentProjects()));
|
||||
|
||||
effect(() => {
|
||||
if (this.header()) {
|
||||
this.width.emit(this.header().nativeElement.getBoundingClientRect().width);
|
||||
}
|
||||
});
|
||||
|
||||
fromEvent(window, 'resize')
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
throttleTime(50)
|
||||
)
|
||||
.subscribe(() => this.width.emit(this.header().nativeElement.getBoundingClientRect().width));
|
||||
}
|
||||
|
||||
@ViewChild('header') header: ElementRef<HTMLDivElement>;
|
||||
|
||||
ngAfterViewInit() {
|
||||
window.setTimeout(() => this.width.emit(this.header.nativeElement.getBoundingClientRect().width));
|
||||
this.sub = fromEvent(window, 'resize')
|
||||
.pipe(throttleTime(50))
|
||||
.subscribe(() => this.width.emit(this.header.nativeElement.getBoundingClientRect().width));
|
||||
}
|
||||
public projectCardClicked(project: Project) {
|
||||
(project.own_tasks===0 && project.sub_projects.length>0) ? this.router.navigateByUrl(`projects/${project.id}/projects`): this.router.navigateByUrl(`projects/${project.id}`);
|
||||
this.store.dispatch(setSelectedProjectId({projectId: project.id, example: isExample(project)}));
|
||||
@@ -74,9 +77,4 @@ export class DashboardProjectsComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.sub?.unsubscribe();
|
||||
this.header = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ import {isExample} from '../shared/utils/shared-utils';
|
||||
import {activeLinksList, ActiveSearchLink, activeSearchLink} from '~/features/dashboard-search/dashboard-search.consts';
|
||||
import {ChangeDetectorRef, Component, inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import { selectShowOnlyUserWork } from '@common/core/reducers/users-reducer';
|
||||
import {IReport} from '@common/reports/reports.consts';
|
||||
import {isEqual} from 'lodash-es';
|
||||
import { Task } from '~/business-logic/model/tasks/task';
|
||||
import {selectShowOnlyUserWork} from '@common/core/reducers/users-reducer';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-dashboard-search-base',
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<div class="experiment-body"
|
||||
[class.footer-visible]="((selectedExperiments$ | async) && (selectedExperiments$ | async)?.length > 1) || (showAllSelectedIsActive$ |async)">
|
||||
<as-split #split
|
||||
[gutterSize]=1
|
||||
[useTransition]="true"
|
||||
[gutterDblClickDuration]="400"
|
||||
(gutterClick)="clickOnSplit()"
|
||||
@@ -34,10 +35,14 @@
|
||||
(dragEnd)="splitSizeChange($event)"
|
||||
(dragStart)="disableInfoPanel()"
|
||||
(transitionEnd)="experimentsTable.table?.resize(); experimentsTable.afterTableInit()"
|
||||
[class.opened]="minimizedView && (selectSplitSize$ | async) <= 1"
|
||||
[class.closed]="minimizedView && (selectSplitSize$ | async) >= 99"
|
||||
>
|
||||
<as-split-area
|
||||
[size]="100 - (splitInitialSize)"
|
||||
[order]="1"
|
||||
[minSize]="1"
|
||||
[maxSize]="99"
|
||||
>
|
||||
<sm-experiments-table
|
||||
#experimentsTable
|
||||
|
||||
@@ -48,7 +48,7 @@ export class DebugImagesEffects {
|
||||
|
||||
activeLoader = createEffect(() => this.actions$.pipe(
|
||||
ofType(debugActions.fetchExperiments, debugActions.refreshMetric, debugActions.refreshDebugImagesMetrics),
|
||||
filter(action => !(action as any).autoRefresh),
|
||||
filter(action => !(action as {autoRefresh?: boolean}).autoRefresh),
|
||||
map(action => activeLoader(action.type))
|
||||
));
|
||||
|
||||
@@ -73,6 +73,8 @@ export class DebugImagesEffects {
|
||||
const actionsToShoot = [deactivateLoader(action.type)] as Action[];
|
||||
if (res.metrics[0].iterations && res.metrics[0].iterations.length > 0) {
|
||||
actionsToShoot.push(debugActions.setDebugImages({res, task: action.payload.task}));
|
||||
// actionsToShoot.push(debugActions.setDebugImages({res: {...res,
|
||||
// metrics: [{...res.metrics[0], iterations: [{...res.metrics[0].iterations[0], events: res.metrics[0].iterations[0].events.slice(0, 50)}]}]}, task: action.payload.task}));
|
||||
switch (action.type) {
|
||||
case debugActions.getNextBatch.type:
|
||||
actionsToShoot.push(debugActions.setTimeIsNow({task: action.payload.task, timeIsNow: false}));
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
<mat-expansion-panel *ngFor="let iteration of iterations; let first = first; trackBy:trackKey"
|
||||
class="images-section" [class.dark-theme]="isDarkTheme" togglePosition="before" [expanded]="first">
|
||||
<mat-expansion-panel-header class="debug-header" [collapsedHeight]="null" *ngIf="!isDatasetVersionPreview" data-id="debugHeader">
|
||||
<mat-panel-title> {{iteration.iter}}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<div class="d-flex justify-content flex-wrap sample-row">
|
||||
<sm-debug-image-snippet
|
||||
*ngFor="let frame of iteration.events; trackBy:trackFrame"
|
||||
[frame]="frame"
|
||||
[theme]="isDarkTheme? themeEnum.Dark: themeEnum.Light"
|
||||
(imageError)="imageUrlError({frame, experimentId})"
|
||||
(imageClicked)="imageClicked.emit({frame})"
|
||||
(createEmbedCode)="createEmbedCode.emit({metrics: [frame.metric], variants: [frame.variant], domRect:$event});">
|
||||
</sm-debug-image-snippet>
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
@for (iteration of iterations; track iteration.iter) {
|
||||
<mat-expansion-panel
|
||||
class="images-section"
|
||||
[class.dark-theme]="isDarkTheme"
|
||||
togglePosition="before"
|
||||
[expanded]="$first"
|
||||
(afterExpand)="resize()"
|
||||
>
|
||||
@if (!isDatasetVersionPreview) {
|
||||
<mat-expansion-panel-header class="debug-header" [collapsedHeight]="null" data-id="debugHeader">
|
||||
<mat-panel-title> {{iteration.iter}}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
}
|
||||
<ng-template matExpansionPanelContent>
|
||||
<sm-virtual-grid
|
||||
[items]="iteration.events"
|
||||
[cardTemplate]="snippetTemplate"
|
||||
[cardHeight]="180"
|
||||
[cardWidth]="180"
|
||||
[padding]="0"
|
||||
[trackFn]="trackFrame"
|
||||
></sm-virtual-grid>
|
||||
<ng-template #snippetTemplate let-frame>
|
||||
<sm-debug-image-snippet
|
||||
[frame]="frame"
|
||||
[theme]="isDarkTheme? themeEnum.Dark: themeEnum.Light"
|
||||
(imageError)="imageUrlError({frame, experimentId})"
|
||||
(imageClicked)="imageClicked.emit({frame})"
|
||||
(createEmbedCode)="createEmbedCode.emit({metrics: [frame.metric], variants: [frame.variant], domRect:$event});">
|
||||
</sm-debug-image-snippet>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
|
||||
::ng-deep .mat-expansion-panel-content {
|
||||
.mat-expansion-panel-body {
|
||||
height: 400px;
|
||||
padding: 12px 0 12px 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {Component, Input, Output} from '@angular/core';
|
||||
import {Component, Input, Output, viewChildren} from '@angular/core';
|
||||
import {EventEmitter} from '@angular/core';
|
||||
import {ThemeEnum} from '@common/constants';
|
||||
import {DebugSampleEvent, Iteration} from '@common/debug-images/debug-images-types';
|
||||
import {VirtualGridComponent} from '@common/shared/components/virtual-grid/virtual-grid.component';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-debug-images-view',
|
||||
@@ -11,8 +12,7 @@ import {DebugSampleEvent, Iteration} from '@common/debug-images/debug-images-typ
|
||||
export class DebugImagesViewComponent {
|
||||
public themeEnum = ThemeEnum;
|
||||
|
||||
public trackKey = (index: number, item: any) => item.iter;
|
||||
public trackFrame = (index: number, item: any) => `${item?.key} ${item?.timestamp}`;
|
||||
public trackFrame = item => `${item?.key} ${item?.timestamp}`;
|
||||
|
||||
@Input() experimentId;
|
||||
@Input() isMergeIterations;
|
||||
@@ -25,7 +25,14 @@ export class DebugImagesViewComponent {
|
||||
@Output() createEmbedCode = new EventEmitter<{metrics?: string[]; variants?: string[]; domRect: DOMRect}>();
|
||||
@Output() urlError = new EventEmitter<{ frame: DebugSampleEvent; experimentId: string }>();
|
||||
|
||||
private gridList = viewChildren(VirtualGridComponent);
|
||||
|
||||
|
||||
public imageUrlError(data: { frame: DebugSampleEvent; experimentId: string }) {
|
||||
this.urlError.emit(data);
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.gridList().forEach(grid => grid.resize(2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +1,93 @@
|
||||
<div class="p-3 images-container">
|
||||
<div class="single-debug-images-container"
|
||||
*ngFor="let experimentId of experimentIds | slice : 0 : LIMITED_VIEW_LIMIT; trackBy: trackExperiment;
|
||||
let first = first; let last = last"
|
||||
[class.separator]="experimentIds?.length > 1">
|
||||
<header *ngIf="experimentIds?.length > 1">
|
||||
<sm-experiment-compare-general-data
|
||||
*ngIf="(experiments | itemById: experimentId).name"
|
||||
[experiment]="experiments | itemById: experimentId"
|
||||
[tags]="(experiments | itemById: experimentId)?.tags"
|
||||
(copyIdClicked)="copyIdToClipboard()"
|
||||
></sm-experiment-compare-general-data>
|
||||
</header>
|
||||
<div class="navigator-container" [class.active]="bindNavigationMode" [class.first]="first"
|
||||
[class.last]="last" [style.display]="isDarkTheme ? 'none' : 'flex'">
|
||||
<div data-id="syncBrowsing" class="connector-icon-container pointer" smTooltip="Sync browsing" [matTooltipShowDelay]="500"
|
||||
[class.active]="bindNavigationMode" [class.hidden]="last" (click)="toggleConnectNavigation()">
|
||||
<i class="al-icon" [class.al-ico-connect]="bindNavigationMode" [class.al-ico-disconnect]="!bindNavigationMode"></i>
|
||||
@for (experimentId of experimentIds | slice : 0 : LIMITED_VIEW_LIMIT; track experimentId) {
|
||||
<div class="single-debug-images-container"
|
||||
[class.separator]="experimentIds?.length > 1">
|
||||
@if (experimentIds?.length > 1) {
|
||||
<header>
|
||||
@if ((experiments | itemById: experimentId).name) {
|
||||
<sm-experiment-compare-general-data
|
||||
[experiment]="experiments | itemById: experimentId"
|
||||
[tags]="(experiments | itemById: experimentId)?.tags"
|
||||
(copyIdClicked)="copyIdToClipboard()"
|
||||
></sm-experiment-compare-general-data>
|
||||
}
|
||||
</header>
|
||||
}
|
||||
<div class="navigator-container" [class.active]="bindNavigationMode" [class.first]="$first"
|
||||
[class.last]="$last" [style.display]="isDarkTheme ? 'none' : 'flex'">
|
||||
<div data-id="syncBrowsing" class="connector-icon-container pointer" smTooltip="Sync browsing" [matTooltipShowDelay]="500"
|
||||
[class.active]="bindNavigationMode" [class.hidden]="$last" (click)="toggleConnectNavigation()">
|
||||
<i class="al-icon" [class.al-ico-connect]="bindNavigationMode" [class.al-ico-disconnect]="!bindNavigationMode"></i>
|
||||
</div>
|
||||
@if (!thereAreNoMetrics(experimentId) && !disableStatusRefreshFilter) {
|
||||
<div class="metric-bar" [class.minimized]="minimized"
|
||||
data-id="metricBar">
|
||||
<label data-id="metricText">Metric:</label>
|
||||
<mat-form-field appearance="outline" class="no-bottom" [ngClass]="{'dark thin': isDarkTheme}" data-id="metricField">
|
||||
<mat-select
|
||||
#metricSelect
|
||||
(selectionChange)="selectMetric($event, experimentId)"
|
||||
[panelClass]="isDarkTheme ? 'dark black dark-theme': 'light-theme'"
|
||||
[value]="selectedMetrics[experimentId]"
|
||||
>
|
||||
@if (selectedMetrics[experimentId]) {
|
||||
<mat-option [value]="allImages">{{allImages}}</mat-option>
|
||||
}
|
||||
@for (metric of optionalMetrics[experimentId]; track metric) {
|
||||
<mat-option [value]="metric">{{metric}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<label data-id="IterationText">Iterations:</label>
|
||||
<div [class.disabled]="(beginningOfTime$| async)?.[experimentId]"
|
||||
(click)="nextBatch({task: experimentId, metric: metricSelect.value})"
|
||||
class="al-icon al-ico-next-batch al-color blue-300"
|
||||
smTooltip="Older images" data-id="OlderImages"></div>
|
||||
<b class="text-right">{{debugImages?.[experimentId]?.data?.slice(-1)[0].iter}}</b>
|
||||
<div class="al-icon al-ico-between al-color light-blue-grey"></div>
|
||||
<b>{{debugImages?.[experimentId]?.data?.[0].iter}}</b>
|
||||
<div [class.disabled]="(timeIsNow$| async)?.[experimentId]"
|
||||
(click)="previousBatch({task: experimentId, metric: metricSelect.value})"
|
||||
class="al-icon al-ico-prev-batch al-color blue-300"
|
||||
smTooltip="Newer images" data-id="NewerImages"></div>
|
||||
<div [class.disabled]="(timeIsNow$| async)?.[experimentId] && !allowAutorefresh"
|
||||
(click)="backToNow({task: experimentId, metric: metricSelect.value})"
|
||||
class="al-icon al-ico-back-to-top al-color blue-300"
|
||||
smTooltip="Newest samples" data-id="NewestSamples"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="metric-bar" [class.minimized]="minimized"
|
||||
*ngIf="!thereAreNoMetrics(experimentId) && !disableStatusRefreshFilter" data-id="metricBar">
|
||||
<label data-id="metricText">Metric:</label>
|
||||
<mat-form-field appearance="outline" class="no-bottom" [ngClass]="{'dark thin': isDarkTheme}" data-id="metricField">
|
||||
<mat-select
|
||||
#metricSelect
|
||||
(selectionChange)="selectMetric($event, experimentId)"
|
||||
[panelClass]="isDarkTheme ? 'dark black dark-theme': 'light-theme'"
|
||||
[value]="selectedMetrics[experimentId]"
|
||||
>
|
||||
<mat-option *ngIf="selectedMetrics[experimentId]" [value]="allImages">{{allImages}}</mat-option>
|
||||
<mat-option *ngFor="let metric of optionalMetrics[experimentId]" [value]="metric">{{metric}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<label data-id="IterationText">Iterations:</label>
|
||||
|
||||
<div [class.disabled]="(beginningOfTime$| async)?.[experimentId]"
|
||||
(click)="nextBatch({task: experimentId, metric: metricSelect.value})"
|
||||
class="al-icon al-ico-next-batch al-color blue-300"
|
||||
smTooltip="Older images" data-id="OlderImages"></div>
|
||||
|
||||
<b class="text-right">{{debugImages?.[experimentId]?.data?.slice(-1)[0].iter}}</b>
|
||||
<div class="al-icon al-ico-between al-color light-blue-grey"></div>
|
||||
<b>{{debugImages?.[experimentId]?.data?.[0].iter}}</b>
|
||||
|
||||
<div [class.disabled]="(timeIsNow$| async)?.[experimentId]"
|
||||
(click)="previousBatch({task: experimentId, metric: metricSelect.value})"
|
||||
class="al-icon al-ico-prev-batch al-color blue-300"
|
||||
smTooltip="Newer images" data-id="NewerImages"></div>
|
||||
|
||||
<div [class.disabled]="(timeIsNow$| async)?.[experimentId] && !allowAutorefresh"
|
||||
(click)="backToNow({task: experimentId, metric: metricSelect.value})"
|
||||
class="al-icon al-ico-back-to-top al-color blue-300"
|
||||
smTooltip="Newest samples" data-id="NewestSamples"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="no-images no-output" [class.dark]="isDarkTheme" *ngIf="shouldShowNoImagesForExperiment(experimentId)">
|
||||
<svg class="mb-3" xmlns="http://www.w3.org/2000/svg" width="200" height="100" viewBox="0 0 300 150">
|
||||
<path opacity="0.1"
|
||||
@if (shouldShowNoImagesForExperiment(experimentId)) {
|
||||
<div class="no-images no-output" [class.dark]="isDarkTheme">
|
||||
<svg class="mb-3" xmlns="http://www.w3.org/2000/svg" width="200" height="100" viewBox="0 0 300 150">
|
||||
<path opacity="0.1"
|
||||
d="M72.67,79.36a5.39,5.39,0,0,1-1.45,4.32,1.17,1.17,0,0,1-.6.34,1.13,1.13,0,0,1-1.31-.88,1.11,1.11,0,0,1,.28-1,3.19,3.19,0,0,0,.89-2.36c-.22-1.09-1.66-1.73-1.68-1.74A1.12,1.12,0,0,1,69.65,76C69.76,76,72.22,77.07,72.67,79.36ZM46.19,78.1a1.38,1.38,0,0,0-1.06,1.61L47.3,90.38a1.38,1.38,0,0,0,1.61,1.06l4-.82L50.19,77.29Zm30.87.39c-.91-4.54-5.23-7.07-5.41-7.18a1.13,1.13,0,1,0-1.16,1.94s3.63,2.13,4.34,5.68-1.8,7-1.82,7a1.12,1.12,0,0,0,.25,1.56,1.11,1.11,0,0,0,.86.2,1.12,1.12,0,0,0,.68-.43C75,87.09,78,83,77.06,78.49Zm-3.57-12a1.12,1.12,0,0,0-1,2,12.48,12.48,0,0,1,2.91,2.24,13.87,13.87,0,0,1,3.88,7.28,14.19,14.19,0,0,1-.78,8.27,13,13,0,0,1-1.83,3.22,1.11,1.11,0,0,0,1.06,1.82h0a1,1,0,0,0,.63-.36,16.47,16.47,0,0,0,3.13-13.38A16.18,16.18,0,0,0,73.49,66.53Zm-9.28,1a1.35,1.35,0,0,0-1.6-1.06h0a1.45,1.45,0,0,0-.71.4L52.2,76.93l2.7,13.24,12.82,5.52a1.36,1.36,0,0,0,1.79-.7,1.33,1.33,0,0,0,.09-.81ZM261.4,76.66l-3.77,21.62-8.28-8.4-.47,2.7a4.13,4.13,0,0,1-4.76,3.35l-25.67-4.48a4.14,4.14,0,0,1-3.35-4.76l2.36-13.51c0-.12,0-.24.08-.35a6.17,6.17,0,1,1,10-3.71,5.62,5.62,0,0,1-.5,1.55l4.49.79a6.37,6.37,0,0,1,.06-1.63,6.15,6.15,0,1,1,11.63,3.65l4.63.81a4,4,0,0,1,3.4,4.53c0,.08,0,.15-.05.23l-.48,2.7ZM225.28,68.1a3.7,3.7,0,0,0-3.52-3.87h-.19a3.79,3.79,0,1,0,3.71,3.87Zm16.29,3A3.7,3.7,0,0,0,238,67.24h-.19a3.79,3.79,0,1,0,3.72,3.86Zm53.2-20.19L281,129.18a17.25,17.25,0,0,1-20,14l-78.27-13.81a17,17,0,0,1-11.13-7.08,17.86,17.86,0,0,1-1-1.69H130.44a17.86,17.86,0,0,1-1,1.69,17,17,0,0,1-11.13,7.08L40.09,143.16a17.25,17.25,0,0,1-20-14L6.26,50.86a17.26,17.26,0,0,1,14-20h0L94.63,17.77A17.24,17.24,0,0,1,110.76,6.58h79.47a17.24,17.24,0,0,1,16.13,11.19l74.38,13.11a17.25,17.25,0,0,1,14,20ZM120.22,119.58h-9.47a17.21,17.21,0,0,1-17.19-17.19V25.1L21.81,37.75a9.55,9.55,0,0,0-7.74,11.06l13.76,78.06a9.57,9.57,0,0,0,11.06,7.71L117,120.81A9.68,9.68,0,0,0,120.22,119.58Zm69.82-7a9.54,9.54,0,0,0,9.52-9.52V24.13a9.53,9.53,0,0,0-9.5-9.55h-79a9.51,9.51,0,0,0-9.51,9.51v79a9.53,9.53,0,0,0,9.51,9.52Zm95.52-70.94a9.49,9.49,0,0,0-6.17-3.93L207.56,25.1v77.33a17.23,17.23,0,0,1-17.21,17.15h-9.49a9.66,9.66,0,0,0,3.28,1.23l78.15,13.77a9.58,9.58,0,0,0,11.07-7.75l13.78-78.06a9.44,9.44,0,0,0-1.58-7.09Zm-109,.43c1.09.64,1,2.34,1,2.34l-.14,37.87a3.33,3.33,0,0,1-3.36,3.3h0l-48.13-.16a3.37,3.37,0,0,1-3.37-3.35l.2-37.39a3.3,3.3,0,0,1,2.11-3l.18-.08h49.68s1.76-.15,1.83.53ZM160.49,53.34a3.8,3.8,0,0,0,7.59,0,3.7,3.7,0,0,0-3.67-3.76h-.12A3.79,3.79,0,0,0,160.49,53.34ZM171.6,74.93l-8.55-9.46-.19-.19a2.49,2.49,0,0,0-3.52.16v0l-4.2,4.63-9.19-10.63a3.05,3.05,0,0,0-1.59-1,2.75,2.75,0,0,0-2.66.95h0l-13.13,15v4.13l43,.14Z"/>
|
||||
</svg>
|
||||
<h4>NO DEBUG SAMPLES</h4>
|
||||
</svg>
|
||||
<h4>NO DEBUG SAMPLES</h4>
|
||||
</div>
|
||||
}
|
||||
@if (debugImages?.[experimentId]?.data) {
|
||||
<sm-debug-images-view
|
||||
[iterations]="debugImages[experimentId].data"
|
||||
[experimentId]="experimentId"
|
||||
[title]="experimentNames && experimentNames[experimentId]"
|
||||
[isMergeIterations]="mergeIterations"
|
||||
[isDarkTheme]="isDarkTheme"
|
||||
[isDatasetVersionPreview]="disableStatusRefreshFilter"
|
||||
(imageClicked)="imageClicked($event, experimentId)"
|
||||
(urlError)="urlError($event)"
|
||||
(createEmbedCode)="createEmbedCode($event, experimentId)"
|
||||
></sm-debug-images-view>
|
||||
}
|
||||
</div>
|
||||
<sm-debug-images-view
|
||||
*ngIf="debugImages?.[experimentId]?.data"
|
||||
[iterations]="debugImages[experimentId].data"
|
||||
[experimentId]="experimentId"
|
||||
[title]="experimentNames && experimentNames[experimentId]"
|
||||
[isMergeIterations]="mergeIterations"
|
||||
[isDarkTheme]="isDarkTheme"
|
||||
[isDatasetVersionPreview]="disableStatusRefreshFilter"
|
||||
(imageClicked)="imageClicked($event, experimentId)"
|
||||
(urlError)="urlError($event)"
|
||||
(createEmbedCode)="createEmbedCode($event, experimentId)"
|
||||
></sm-debug-images-view>
|
||||
</div>
|
||||
<div *ngIf="experimentIds?.length>LIMITED_VIEW_LIMIT"
|
||||
class="limit-message-container">
|
||||
<div class="limit-message">
|
||||
<i class="al-icon al-ico-info-circle mb-2"></i>Only the first 10 experiments are available for this view...
|
||||
}
|
||||
@if (experimentIds?.length>LIMITED_VIEW_LIMIT) {
|
||||
<div
|
||||
class="limit-message-container">
|
||||
<div class="limit-message">
|
||||
<i class="al-icon al-ico-info-circle mb-2"></i>Only the first 10 experiments are available for this view...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Component, DestroyRef,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges
|
||||
SimpleChanges, viewChildren
|
||||
} from '@angular/core';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {ActivatedRoute, Params} from '@angular/router';
|
||||
@@ -29,7 +29,7 @@ import {Task} from '~/business-logic/model/tasks/task';
|
||||
import {TaskStatusEnum} from '~/business-logic/model/tasks/taskStatusEnum';
|
||||
import {selectSelectedExperiment} from '~/features/experiments/reducers';
|
||||
import {ReportCodeEmbedService} from '~/shared/services/report-code-embed.service';
|
||||
import {getSignedUrl} from '../core/actions/common-auth.actions';
|
||||
import {getSignedUrl, signUrls} from '../core/actions/common-auth.actions';
|
||||
import {addMessage} from '../core/actions/layout.actions';
|
||||
import {selectS3BucketCredentials} from '../core/reducers/common-auth-reducer';
|
||||
import {selectRouterParams} from '../core/reducers/router-reducer';
|
||||
@@ -46,6 +46,11 @@ import {
|
||||
selectTaskNames,
|
||||
selectTimeIsNow
|
||||
} from './debug-images-reducer';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {DebugImagesViewComponent} from '@common/debug-images/debug-images-view/debug-images-view.component';
|
||||
import {selectSplitSize} from '@common/experiments/reducers';
|
||||
import {DebugImagesResponseIterations} from '~/business-logic/model/events/debugImagesResponseIterations';
|
||||
import {MetricsImageEvent} from '~/business-logic/model/events/metricsImageEvent';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-debug-images',
|
||||
@@ -60,6 +65,8 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() selected: Task;
|
||||
@Output() copyIdClicked = new EventEmitter();
|
||||
|
||||
private sampleViews = viewChildren(DebugImagesViewComponent);
|
||||
|
||||
private debugImagesSubscription: Subscription;
|
||||
private taskNamesSubscription: Subscription;
|
||||
private selectedExperimentSubscription: Subscription;
|
||||
@@ -99,6 +106,7 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
|
||||
private elRef: ElementRef,
|
||||
private refresh: RefreshService,
|
||||
private reportEmbed: ReportCodeEmbedService,
|
||||
private destroyRef: DestroyRef
|
||||
) {
|
||||
this.tasks$ = this.store.select(selectTaskNames);
|
||||
this.optionalMetrics$ = this.store.select(selectOptionalMetrics);
|
||||
@@ -115,26 +123,32 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
|
||||
.pipe(
|
||||
map(([, debugImages, metricForTask]) => !debugImages ? {} : Object.entries(debugImages).reduce(((acc, val: [string, EventsDebugImagesResponse]) => {
|
||||
const id = val[0];
|
||||
const iterations = val[1].metrics.find(m => m.task === id).iterations;
|
||||
const iterations: DebugImagesResponseIterations[] = val[1].metrics.find(m => m.task === id).iterations;
|
||||
if (iterations?.length === 0) {
|
||||
return {[id]: {}};
|
||||
}
|
||||
const metrics = val[1].metrics.map((metric) => metric?.['metric'] || metric.iterations[0].events[0].metric);
|
||||
acc[id] = {
|
||||
data: iterations.map(iteration => ({
|
||||
iter: iteration.iter,
|
||||
events: iteration.events.map(event => {
|
||||
this.store.dispatch(getSignedUrl({url: event.url, config: {disableCache: event.timestamp}}));
|
||||
events: iteration.events.map((event: MetricsImageEvent) => {
|
||||
return {
|
||||
...event,
|
||||
url: event.url,
|
||||
variantAndMetric: (this.selectedMetric === ALL_IMAGES || metricForTask[id] === ALL_IMAGES ) ? `${event.metric}/${event.variant}` : ''
|
||||
variantAndMetric: (this.selectedMetrics[id] === ALL_IMAGES || metricForTask[id] === ALL_IMAGES ) ? `${event.metric}/${event.variant}` : ''
|
||||
};
|
||||
})
|
||||
}))
|
||||
})),
|
||||
metrics,
|
||||
metric: metrics[0],
|
||||
scrollId: val[1].scroll_id,
|
||||
};
|
||||
acc[id].metrics = val[1].metrics.map((metric: any) => metric.metric || metric.iterations[0].events[0].metric);
|
||||
acc[id].metric = acc[id].metrics[0];
|
||||
acc[id].scrollId = val[1].scroll_id;
|
||||
this.store.dispatch(signUrls({sign: iterations.map(iteration =>
|
||||
iteration.events.map((event: MetricsImageEvent) =>
|
||||
({url: event.url, config: {disableCache: event.timestamp}})
|
||||
)
|
||||
).flat()
|
||||
}));
|
||||
return acc;
|
||||
}), {}))
|
||||
)
|
||||
@@ -213,6 +227,14 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
|
||||
this.store.dispatch(getDebugImagesMetrics({tasks: this.experimentIds}));
|
||||
this.changeDetection.markForCheck();
|
||||
});
|
||||
|
||||
if (this.minimized) {
|
||||
this.store.select(selectSplitSize)
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe(() => this.sampleViews().forEach(view => view.resize()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -303,10 +325,6 @@ export class DebugImagesComponent implements OnInit, OnDestroy, OnChanges {
|
||||
return tasks.some(task => [TaskStatusEnum.InProgress, TaskStatusEnum.Queued].includes(task.status));
|
||||
}
|
||||
|
||||
trackExperiment(index: number, experimentID: string) {
|
||||
return experimentID;
|
||||
}
|
||||
|
||||
selectMetric(change: {value: string}, taskId) {
|
||||
this.selectedMetric = change.value;
|
||||
if (this.bindNavigationMode) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {MatInputModule} from '@angular/material/input';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
import {MatExpansionModule} from '@angular/material/expansion';
|
||||
import {TooltipDirective} from '@common/shared/ui-components/indicators/tooltip/tooltip.directive';
|
||||
import {VirtualGridComponent} from '@common/shared/components/virtual-grid/virtual-grid.component';
|
||||
|
||||
const declarations = [DebugImagesComponent, DebugImagesViewComponent];
|
||||
|
||||
@@ -39,7 +40,8 @@ const declarations = [DebugImagesComponent, DebugImagesViewComponent];
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatExpansionModule,
|
||||
TooltipDirective
|
||||
TooltipDirective,
|
||||
VirtualGridComponent
|
||||
]
|
||||
})
|
||||
export class DebugImagesModule {
|
||||
|
||||
@@ -88,7 +88,7 @@ export class ExperimentCompareHyperParamsGraphComponent implements OnInit, OnDes
|
||||
private settingsLoaded: boolean;
|
||||
|
||||
public selectedItemsListMapper(data) {
|
||||
return data;
|
||||
return decodeURIComponent(data);
|
||||
}
|
||||
|
||||
@ViewChild('searchMetric') searchMetricRef: ElementRef;
|
||||
@@ -149,7 +149,7 @@ export class ExperimentCompareHyperParamsGraphComponent implements OnInit, OnDes
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const selectedHyperParams = this.selectedHyperParams?.filter(selectedParam => has(this.hyperParams, selectedParam));
|
||||
const selectedHyperParams = this.selectedHyperParams?.filter(selectedParam => has(this.hyperParams, selectedParam.split('.').slice(0,1).join('.')));
|
||||
selectedHyperParams && this.updateServer(this.selectedMetric, selectedHyperParams);
|
||||
this.cdr.detectChanges();
|
||||
}));
|
||||
@@ -197,7 +197,7 @@ export class ExperimentCompareHyperParamsGraphComponent implements OnInit, OnDes
|
||||
}
|
||||
|
||||
if (queryParams.params) {
|
||||
this.selectedHyperParams = Array.isArray(queryParams.params) ? queryParams.params : [queryParams.params];
|
||||
this.selectedHyperParams = Array.isArray(queryParams.params) ? queryParams.params.map(this.selectedItemsListMapper) : [queryParams.params].map(this.selectedItemsListMapper);
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
}));
|
||||
@@ -266,7 +266,12 @@ export class ExperimentCompareHyperParamsGraphComponent implements OnInit, OnDes
|
||||
}
|
||||
|
||||
|
||||
updateServer(selectedMetric?: SelectedMetricVariant, selectedParams?: string[], skipNavigation?: boolean, valueType?: SelectedMetricVariant['valueType'], selectedMetrics?: SelectedMetricVariant[], force?: boolean) {
|
||||
updateServer(selectedMetric?: SelectedMetricVariant,
|
||||
selectedParams?: string[],
|
||||
skipNavigation?: boolean,
|
||||
valueType?: SelectedMetricVariant['valueType'],
|
||||
selectedMetrics?: SelectedMetricVariant[],
|
||||
force?: boolean) {
|
||||
(this.routeWasLoaded || force) && !skipNavigation && this.router.navigate([], {
|
||||
queryParams: {
|
||||
metricPath: selectedMetric ? `${selectedMetric?.metric_hash}.${selectedMetric?.variant_hash}` : undefined,
|
||||
@@ -318,7 +323,7 @@ export class ExperimentCompareHyperParamsGraphComponent implements OnInit, OnDes
|
||||
}
|
||||
|
||||
selectedParamsForHoverChanged({param}) {
|
||||
const newSelectedParamsList = this.selectedParamsHoverInfo.includes(param) ? this.selectedParamsHoverInfo.filter(i => i !== param) : [...this.selectedParamsHoverInfo, param];
|
||||
const newSelectedParamsList = this.selectedParamsHoverInfo.includes(param) ? this.selectedParamsHoverInfo.filter(i => i !== param) : [...this.selectedParamsHoverInfo, param].map(this.selectedItemsListMapper);
|
||||
this.store.dispatch(setParamsHoverInfo({paramsHoverInfo: newSelectedParamsList}));
|
||||
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<span class="path1"></span><span class="path2"></span>
|
||||
</i>
|
||||
</button>
|
||||
<mat-form-field appearance="outline" hideRequiredMarker class="light-theme mat-light no-bottom">
|
||||
<mat-form-field appearance="outline" class="light-theme mat-light no-bottom">
|
||||
<input #filterRef
|
||||
name="filter"
|
||||
[formControl]="variantFilter"
|
||||
|
||||
@@ -289,16 +289,23 @@ export class ExperimentCompareMetricValuesComponent implements OnInit, OnDestroy
|
||||
}
|
||||
|
||||
exportToCSV() {
|
||||
const headers = this.experiments.map(ex => ex.name);
|
||||
const options = mkConfig({
|
||||
filename: `Scalars compare table`,
|
||||
showColumnHeaders: true,
|
||||
columnHeaders: ['Metric', 'Variant'].concat(this.experiments.map(ex => ex.name))
|
||||
columnHeaders: ['Metric', 'Variant'].concat(headers)
|
||||
});
|
||||
const csv = generateCsv(options)(this.dataTableFiltered.map(row => ({
|
||||
Metric: row.metric,
|
||||
Variant: row.variant,
|
||||
...Object.values(row.values).map(value => value?.[this.valuesMode.key] ?? '')
|
||||
})));
|
||||
const csv = generateCsv(options)(this.dataTableFiltered.map(row => {
|
||||
const values = Object.values(row.values).map(value => value?.[this.valuesMode.key] ?? '');
|
||||
return {
|
||||
Metric: row.metric,
|
||||
Variant: row.variant,
|
||||
...headers.reduce( (acc, header, i) => {
|
||||
acc[header] = values[i]
|
||||
return acc;
|
||||
}, {})
|
||||
}
|
||||
}));
|
||||
download(options)(csv);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult';
|
||||
import {uniqBy} from 'lodash-es';
|
||||
import {
|
||||
SelectionEvent
|
||||
SelectionEvent, SelectMetricForCustomColComponent
|
||||
} 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';
|
||||
@@ -32,8 +32,9 @@ import {MetricValueTypeStrings} from '@common/shared/utils/tableParamEncode';
|
||||
SelectMetadataKeysCustomColsComponent,
|
||||
MetricVariantToNamePipe,
|
||||
TooltipDirective,
|
||||
ShowTooltipIfEllipsisDirective
|
||||
],
|
||||
ShowTooltipIfEllipsisDirective,
|
||||
SelectMetricForCustomColComponent
|
||||
],
|
||||
templateUrl: './metric-variant-selector.component.html',
|
||||
styleUrl: './metric-variant-selector.component.scss'
|
||||
})
|
||||
@@ -55,5 +56,5 @@ export class MetricVariantSelectorComponent {
|
||||
protected readonly trackByIndex = trackByIndex;
|
||||
|
||||
protected readonly MetricValueTypeStrings = MetricValueTypeStrings;
|
||||
searchText: string;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {selectSelectedProject} from '@common/core/reducers/projects.reducer';
|
||||
import {Project} from '~/business-logic/model/projects/project';
|
||||
import {TitleCasePipe} from '@angular/common';
|
||||
import {resetSelectModelState} from '@common/select-model/select-model.actions';
|
||||
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';
|
||||
@@ -28,8 +27,9 @@ import {
|
||||
EXPERIMENTS_COMPARE_ROUTES,
|
||||
MODELS_COMPARE_ROUTES
|
||||
} from '@common/experiments-compare/experiments-compare.constants';
|
||||
import {setContextMenu} from '@common/core/actions/router.actions';
|
||||
import {headerActions} from '@common/core/actions/router.actions';
|
||||
import {isEqual} from 'lodash-es';
|
||||
import {selectProjectType} from '@common/core/reducers/view.reducer';
|
||||
|
||||
const toCompareEntityType = {
|
||||
[EntityTypeEnum.controller]: EntityTypeEnum.experiment,
|
||||
@@ -87,7 +87,7 @@ export class ExperimentsCompareComponent implements OnInit, OnDestroy {
|
||||
this.subs.unsubscribe();
|
||||
this.store.dispatch(resetSelectCompareHeader({fullReset: true}));
|
||||
this.store.dispatch(setGlobalLegendData({data: null}));
|
||||
this.store.dispatch(setContextMenu({contextMenu: null}));
|
||||
this.store.dispatch(headerActions.setTabs({contextMenu: null}));
|
||||
this.store.dispatch(resetSelectModelState({fullReset: true}));
|
||||
}
|
||||
|
||||
@@ -239,6 +239,6 @@ export class ExperimentsCompareComponent implements OnInit, OnDestroy {
|
||||
isActive: ((route.featureLink ?? route.header) === entitiesType)
|
||||
};
|
||||
});
|
||||
this.store.dispatch(setContextMenu({contextMenu}));
|
||||
this.store.dispatch(headerActions.setTabs({contextMenu}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ExperimentSettingsComponent} from '../../shared/components/experiment-settings/experiment-settings';
|
||||
import {
|
||||
SelectMetricForCustomColComponent
|
||||
} from '@common/experiments/dumb/select-metric-for-custom-col/select-metric-for-custom-col.component';
|
||||
import {MatRadioModule} from '@angular/material/radio';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||
@@ -18,7 +15,6 @@ import {IsEmptyPipe} from '@common/shared/pipes/is-empty.pipe';
|
||||
|
||||
const declarations = [
|
||||
ExperimentSettingsComponent,
|
||||
SelectMetricForCustomColComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -15,7 +15,7 @@ export const publishClicked = createAction(
|
||||
|
||||
export const stopClicked = createAction(
|
||||
EXPERIMENTS_INFO_PREFIX + 'stop experiments',
|
||||
props<{ selectedEntities: ISelectedExperiment[] }>()
|
||||
props<{ selectedEntities: ISelectedExperiment[], includePipelineSteps?: boolean}>()
|
||||
);
|
||||
export const startPipeline = createAction(
|
||||
EXPERIMENTS_INFO_PREFIX + 'start pipeline',
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
} from '~/business-logic/model/organization/organizationPrepareDownloadForGetAllRequest';
|
||||
import {ISelectedExperiment} from '~/features/experiments/shared/experiment-info.model';
|
||||
import {EventTypeEnum} from '~/business-logic/model/events/eventTypeEnum';
|
||||
import {
|
||||
createExperimentDialogResult
|
||||
} from '@common/experiments/containers/create-experiment-dialog/create-experiment-dialog.component';
|
||||
|
||||
// COMMANDS:
|
||||
export const getExperiments = createAction(EXPERIMENTS_PREFIX + ' [get experiments]');
|
||||
@@ -303,3 +306,15 @@ export const prepareTableForDownload = createAction(
|
||||
EXPERIMENTS_PREFIX + ' [prepareTableForDownload]',
|
||||
props<{ entityType: OrganizationPrepareDownloadForGetAllRequest.EntityTypeEnum }>()
|
||||
);
|
||||
export const createExperiment = createAction(
|
||||
EXPERIMENTS_PREFIX + ' [create experiment]',
|
||||
props<{data: createExperimentDialogResult}>()
|
||||
);
|
||||
export const createExperimentSuccess = createAction(
|
||||
EXPERIMENTS_PREFIX + ' [create experiment success]',
|
||||
props<{data: createExperimentDialogResult, project: string}>()
|
||||
);
|
||||
export const openExperiment = createAction(
|
||||
EXPERIMENTS_PREFIX + ' [open experiment]',
|
||||
props<{id: string; project: string}>()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
<sm-dialog-template
|
||||
class="wrapper"
|
||||
header="Create Experiment"
|
||||
iconClass="al-ico-training"
|
||||
[closeOnX]="true"
|
||||
>
|
||||
<mat-stepper [linear]="false" color="accent">
|
||||
<mat-step [stepControl]="codeFormGroup" label="Code">
|
||||
<form [formGroup]="codeFormGroup">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Experiment Name</mat-label>
|
||||
<input matInput placeholder="my task" formControlName="name">
|
||||
@if (codeFormGroup.controls.name.invalid) {
|
||||
<mat-error>
|
||||
@if (codeFormGroup.controls.name.errors?.minlength) {
|
||||
Name should be more than 2 characters long
|
||||
}
|
||||
@else if (codeFormGroup.controls.name.errors?.required) {
|
||||
Name is required
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<h5>Git</h5>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Repository URL</mat-label>
|
||||
<input matInput placeholder="git@github.com:allegroai/clearml.git" formControlName="repo">
|
||||
</mat-form-field>
|
||||
<div class="d-flex git">
|
||||
<mat-form-field appearance="outline" class="me-3">
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select formControlName="type" panelClass="light-theme" (selectionChange)="typeChange($event.value, gitTypes)">
|
||||
@for (gitType of gitTypes; track gitType) {
|
||||
<mat-option [value]="gitType">{{gitType[0].toUpperCase()}}{{gitType.slice(1)}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@switch (codeFormGroup.controls?.type.value) {
|
||||
@case (gitTypes[0]) {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Branch</mat-label>
|
||||
<input matInput placeholder="main" formControlName="branch">
|
||||
</mat-form-field>
|
||||
}
|
||||
@case (gitTypes[1]) {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Commit</mat-label>
|
||||
<input matInput placeholder="d4f9424589f320ec503db873799f451582174d90" formControlName="commit">
|
||||
</mat-form-field>
|
||||
}
|
||||
@case (gitTypes[2]) {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Tag</mat-label>
|
||||
<input matInput placeholder="version-1" formControlName="tag">
|
||||
</mat-form-field>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
<h5>Entry Point</h5>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Working Directory</mat-label>
|
||||
<input matInput placeholder="src" formControlName="directory">
|
||||
</mat-form-field>
|
||||
<div class="d-flex git">
|
||||
<mat-form-field appearance="outline" class="me-3">
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select formControlName="scriptType" panelClass="light-theme" (selectionChange)="typeChange($event.value, scriptTypes)">
|
||||
@for (scriptType of scriptTypes; track scriptType) {
|
||||
<mat-option [value]="scriptType">{{scriptType[0].toUpperCase()}}{{scriptType.slice(1)}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@switch (codeFormGroup.controls?.scriptType.value) {
|
||||
@case (scriptTypes[0]) {
|
||||
<mat-form-field appearance="outline" class="">
|
||||
<mat-label>Script</mat-label>
|
||||
<input matInput placeholder="entry_point.py" formControlName="script">
|
||||
</mat-form-field>
|
||||
}
|
||||
@case (scriptTypes[1]) {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Module</mat-label>
|
||||
<input matInput placeholder="cmd" formControlName="module">
|
||||
</mat-form-field>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<!-- <div class="checkbox">-->
|
||||
<!-- <mat-checkbox formControlName="existing">Existing Code Base (docker only)</mat-checkbox>-->
|
||||
<!-- </div>-->
|
||||
<div class="checkbox">
|
||||
<mat-checkbox formControlName="taskInit" (change)="checkDocker()">Add <code>Task.init</code> call</mat-checkbox>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-outline-neon" matStepperNext>NEXT</button>
|
||||
<ng-container *ngTemplateOutlet="saveButton"></ng-container>
|
||||
<button class="btn btn-neon" [disabled]="codeFormGroup.invalid || dockerFormGroup.invalid" (click)="runStep.select()">RUN</button>
|
||||
</div>
|
||||
<mat-step [stepControl]="argsFormGroup" [formGroup]="argsFormGroup" label="Arguments">
|
||||
<form formArrayName="args">
|
||||
<h5>Configuration Hyperparameters Args</h5>
|
||||
<section>
|
||||
@for (pair of args.controls; track $index) {
|
||||
<div [formGroupName]="$index" class="args-inputs">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Key</mat-label>
|
||||
<input matInput (keydown.enter)="$event.preventDefault()" formControlName="key" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Value</mat-label>
|
||||
<input (keydown.enter)="$event.preventDefault()" matInput formControlName="value">
|
||||
</mat-form-field>
|
||||
<button class="btn btn-icon" (click)="$event.preventDefault(); removeArg($index)"><i class="al-icon al-ico-trash"></i></button>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
<button class="btn btn-icon d-flex justify-content-between align-items-center mt-4" (click)="$event.preventDefault(); addArg()">
|
||||
<i class="al-icon al-ico-plus"></i>
|
||||
<span style="transform: translateY(1px);">Add argument</span>
|
||||
</button>
|
||||
</form>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-outline-neon" matStepperPrevious>BACK</button>
|
||||
<button class="btn btn-outline-neon" matStepperNext>NEXT</button>
|
||||
<ng-container *ngTemplateOutlet="saveButton"></ng-container>
|
||||
</div>
|
||||
</mat-step>
|
||||
</mat-step>
|
||||
<mat-step [stepControl]="envFormGroup" label="Environment">
|
||||
<form [formGroup]="envFormGroup" class="d-flex flex-column pe-4">
|
||||
<div class="checkbox">
|
||||
<mat-checkbox formControlName="poetry" (change)="togglePoetry($event.checked); checkDocker()">Use Poetry (docker only)</mat-checkbox>
|
||||
</div>
|
||||
<div class="d-flex gap">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Python Binary</mat-label>
|
||||
<input matInput placeholder="python3" formControlName="binary">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Preinstalled venv (docker only)</mat-label>
|
||||
<input matInput placeholder="clearml" formControlName="venv" (change)="checkDocker()">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-radio-group aria-label="Select Requirements Type" formControlName="requirements" class="mt-4" (change)="checkDocker()">
|
||||
<mat-radio-button value="skip">Skip (docker only)</mat-radio-button>
|
||||
<mat-radio-button value="text">Use <code>requirements.txt</code></mat-radio-button>
|
||||
<mat-radio-button value="manual">Specify packages</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
@if (envFormGroup.controls.requirements.value === 'manual') {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Requirements</mat-label>
|
||||
<textarea matInput formControlName="pip" class="terminal" rows="8" placeholder="bokeh>=1.4.0
|
||||
clearml
|
||||
matplotlib >= 3.1.1 ; python_version >= '3.6'
|
||||
matplotlib >= 2.2.4 ; python_version < '3.6'
|
||||
numpy != 1.24.0 # https://github.com/numpy/numpy/issues/22826
|
||||
pandas
|
||||
pillow>=4.0
|
||||
plotly
|
||||
seaborn
|
||||
six"></textarea>
|
||||
</mat-form-field>
|
||||
}
|
||||
</form>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-outline-neon" matStepperPrevious>BACK</button>
|
||||
<button class="btn btn-outline-neon" matStepperNext>NEXT</button>
|
||||
<ng-container *ngTemplateOutlet="saveButton"></ng-container>
|
||||
</div>
|
||||
</mat-step>
|
||||
<mat-step [stepControl]="dockerFormGroup" label="Docker">
|
||||
<form [formGroup]="dockerFormGroup" class="d-flex flex-column pe-4">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Image</mat-label>
|
||||
<input matInput formControlName="image">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Arguments</mat-label>
|
||||
<input matInput placeholder="-e env1=true" formControlName="args">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Startup Script</mat-label>
|
||||
<textarea matInput formControlName="script" class="terminal" rows="8"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-outline-neon" matStepperPrevious>BACK</button>
|
||||
<button class="btn btn-outline-neon" matStepperNext>NEXT</button>
|
||||
<ng-container *ngTemplateOutlet="saveButton"></ng-container>
|
||||
</div>
|
||||
</mat-step>
|
||||
<mat-step #runStep label="Run">
|
||||
<form [formGroup]="queueFormGroup" class="d-flex flex-column">
|
||||
<h5>Enqueue experiment to</h5>
|
||||
<section>
|
||||
<sm-paginated-entity-selector
|
||||
#selector
|
||||
formControlName="queue"
|
||||
label="Queue"
|
||||
[data]="queues() | filter: queueVal(): 'name'"
|
||||
[isRequired]="true"
|
||||
></sm-paginated-entity-selector>
|
||||
</section>
|
||||
<h5>Output</h5>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Destination</mat-label>
|
||||
<input matInput formControlName="output" placeHolder="s3://my_bucket/my_folder">
|
||||
</mat-form-field>
|
||||
</section>
|
||||
</form>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-outline-neon" matStepperPrevious>BACK</button>
|
||||
<ng-container *ngTemplateOutlet="saveButton"></ng-container>
|
||||
<button
|
||||
class="btn btn-neon"
|
||||
[disabled]="codeFormGroup.invalid || dockerFormGroup.invalid || queueFormGroup.invalid"
|
||||
(click)="close('run')"
|
||||
>RUN</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
<ng-template matStepperIcon="edit">
|
||||
<i class="al-icon al-ico-success sm"></i>
|
||||
</ng-template>
|
||||
</mat-stepper>
|
||||
</sm-dialog-template>
|
||||
|
||||
<ng-template #saveButton>
|
||||
<button class="btn btn-neon" [disabled]="codeFormGroup.invalid || dockerFormGroup.invalid" (click)="close('save')">SAVE AS DRAFT</button>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,87 @@
|
||||
@import "variables";
|
||||
|
||||
:host {
|
||||
::ng-deep .generic-container {
|
||||
padding: 32px 24px 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
height: calc(94vh - 345px);
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: $blue-500;
|
||||
margin: 24px 0 6px;
|
||||
}
|
||||
|
||||
section {
|
||||
border-top: 1px solid $blue-300;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: $font-family-monospace;
|
||||
background-color: $blue-50;
|
||||
border: 1px solid $blue-100;
|
||||
border-radius: 4px;
|
||||
margin: 0 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-mdc-form-field, sm-paginated-entity-selector {
|
||||
display: block;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.mat-mdc-radio-button ~ .mat-mdc-radio-button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.git .mat-mdc-form-field:first-child {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin: 24px 0 12px -6px;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
resize: none;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: 13px !important;
|
||||
line-height: 1.6;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.te {
|
||||
color: $blue-100;
|
||||
background-color: $blue-900;
|
||||
}
|
||||
|
||||
.args-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 40px;
|
||||
align-items: end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.d-flex.gap {
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: 24px 0 12px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreateExperimentDialogComponent } from './create-experiment-dialog.component';
|
||||
|
||||
describe('CreateExperimentDialogComponent', () => {
|
||||
let component: CreateExperimentDialogComponent;
|
||||
let fixture: ComponentFixture<CreateExperimentDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CreateExperimentDialogComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CreateExperimentDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,223 @@
|
||||
import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
|
||||
import {
|
||||
MatStep,
|
||||
MatStepLabel,
|
||||
MatStepper,
|
||||
MatStepperIcon,
|
||||
MatStepperNext,
|
||||
MatStepperPrevious
|
||||
} from '@angular/material/stepper';
|
||||
import {
|
||||
MAT_FORM_FIELD_DEFAULT_OPTIONS, MatError,
|
||||
MatFormField,
|
||||
MatFormFieldDefaultOptions,
|
||||
MatLabel
|
||||
} from '@angular/material/form-field';
|
||||
import {FormArray, FormBuilder, FormControl, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||
import {DialogTemplateComponent} from '@common/shared/ui-components/overlay/dialog-template/dialog-template.component';
|
||||
import {MatInput} from '@angular/material/input';
|
||||
import {LabeledFormFieldDirective} from '@common/shared/directive/labeled-form-field.directive';
|
||||
import {MatOption, MatSelect} from '@angular/material/select';
|
||||
import {MatCheckbox} from '@angular/material/checkbox';
|
||||
import {MatRadioButton, MatRadioGroup} from '@angular/material/radio';
|
||||
import {IOption} from '@common/constants';
|
||||
import {
|
||||
PaginatedEntitySelectorComponent
|
||||
} from '@common/shared/components/paginated-entity-selector/paginated-entity-selector.component';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {selectQueuesList} from '@common/experiments/shared/components/select-queue/select-queue.reducer';
|
||||
import {NgTemplateOutlet} from '@angular/common';
|
||||
import {MatDialogRef} from '@angular/material/dialog';
|
||||
import {getQueuesForEnqueue} from '@common/experiments/shared/components/select-queue/select-queue.actions';
|
||||
import {FilterPipe} from '@common/shared/pipes/filter.pipe';
|
||||
import {toSignal} from '@angular/core/rxjs-interop';
|
||||
import {Queue} from '~/business-logic/model/queues/queue';
|
||||
import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core';
|
||||
|
||||
|
||||
export interface createExperimentDialogResult {
|
||||
id?: string;
|
||||
action: 'save' | 'run';
|
||||
name: string;
|
||||
repo: string;
|
||||
type: 'branch' | 'commit' | 'tag';
|
||||
branch: string;
|
||||
commit: string;
|
||||
tag: string;
|
||||
directory: string;
|
||||
script: string;
|
||||
taskInit: boolean;
|
||||
args: {key: string; value: string}[];
|
||||
poetry: boolean;
|
||||
binary: string;
|
||||
venv: string;
|
||||
requirements: 'skip' | 'text' | 'manual';
|
||||
pip: string;
|
||||
docker : {
|
||||
image?: string;
|
||||
args?: string;
|
||||
script?: string;
|
||||
}
|
||||
queue?: Queue;
|
||||
output?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'sm-create-experiment-dialog',
|
||||
templateUrl: './create-experiment-dialog.component.html',
|
||||
styleUrl: './create-experiment-dialog.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatStepper,
|
||||
MatStep,
|
||||
MatFormField,
|
||||
ReactiveFormsModule,
|
||||
DialogTemplateComponent,
|
||||
MatStepperPrevious,
|
||||
MatStepLabel,
|
||||
MatStepperNext,
|
||||
MatInput,
|
||||
MatLabel,
|
||||
LabeledFormFieldDirective,
|
||||
MatSelect,
|
||||
MatOption,
|
||||
MatCheckbox,
|
||||
MatRadioGroup,
|
||||
MatRadioButton,
|
||||
PaginatedEntitySelectorComponent,
|
||||
MatStepperIcon,
|
||||
NgTemplateOutlet,
|
||||
FilterPipe,
|
||||
MatError,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||
useValue: {subscriptSizing: 'dynamic', appearance: 'outline', floatLabel: 'always'} as MatFormFieldDefaultOptions
|
||||
},
|
||||
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useValue: {disabled: true} as RippleGlobalOptions}
|
||||
]
|
||||
})
|
||||
export class CreateExperimentDialogComponent {
|
||||
private readonly formBuilder = inject(FormBuilder);
|
||||
private readonly store = inject(Store);
|
||||
private readonly dialog = inject(MatDialogRef);
|
||||
|
||||
protected queues = this.store.selectSignal(selectQueuesList);
|
||||
|
||||
protected gitTypes = ['branch', 'commit', 'tag'];
|
||||
protected scriptTypes = ['script', 'module'];
|
||||
protected requirementOptions: IOption[] = [
|
||||
{label: 'Skip', value: 'skip'},
|
||||
{label: 'requirements.txt', value: 'text'},
|
||||
{label: 'Manual', value: 'manual'}
|
||||
];
|
||||
|
||||
codeFormGroup = this.formBuilder.group({
|
||||
name: [null, [Validators.required, Validators.minLength(3)]],
|
||||
repo: [null, Validators.required],
|
||||
type: ['branch'],
|
||||
branch: ['master', Validators.required],
|
||||
commit: [null],
|
||||
tag: [null],
|
||||
directory: ['.', Validators.required],
|
||||
scriptType: ['script'],
|
||||
script: [null, Validators.required],
|
||||
module: [null],
|
||||
existing: [false],
|
||||
taskInit: [true],
|
||||
});
|
||||
argsFormGroup = this.formBuilder.group({
|
||||
args: this.formBuilder.array([])
|
||||
})
|
||||
envFormGroup = this.formBuilder.group({
|
||||
poetry: [false],
|
||||
binary: [null],
|
||||
venv: [null],
|
||||
requirements: ['text'],
|
||||
pip: ['']
|
||||
});
|
||||
dockerFormGroup = this.formBuilder.group({
|
||||
image: [null],
|
||||
args: [''],
|
||||
script: ['']
|
||||
});
|
||||
queueFormGroup = this.formBuilder.group({
|
||||
queue: [null, Validators.required],
|
||||
output: ['']
|
||||
})
|
||||
|
||||
protected queueVal = toSignal<string>(this.queueFormGroup.controls.queue.valueChanges);
|
||||
|
||||
constructor() {
|
||||
this.store.dispatch(getQueuesForEnqueue());
|
||||
}
|
||||
|
||||
get args() {
|
||||
return this.argsFormGroup.get('args') as FormArray;
|
||||
}
|
||||
|
||||
addArg() {
|
||||
this.args.push(this.formBuilder.group({
|
||||
key: ['', Validators.required],
|
||||
value: [''],
|
||||
}))
|
||||
}
|
||||
|
||||
removeArg(index: number) {
|
||||
this.args.removeAt(index);
|
||||
}
|
||||
|
||||
checkDocker() {
|
||||
if (this.envFormGroup.controls.poetry.value ||
|
||||
this.envFormGroup.controls.venv.value || this.envFormGroup.controls.requirements.value === 'skip') {
|
||||
this.dockerFormGroup.controls.image.setValidators(Validators.required);
|
||||
} else {
|
||||
this.dockerFormGroup.controls.image.clearValidators();
|
||||
}
|
||||
this.dockerFormGroup.controls.image.updateValueAndValidity();
|
||||
}
|
||||
|
||||
close(action: 'save' | 'run') {
|
||||
this.dialog.close({
|
||||
action,
|
||||
...this.codeFormGroup.value,
|
||||
...(this.codeFormGroup.controls.scriptType.value === 'module' && {script: `-m ${this.codeFormGroup.value.module}`}),
|
||||
...this.argsFormGroup.value,
|
||||
...this.envFormGroup.value,
|
||||
...(this.envFormGroup.controls.requirements.value === 'manual' && {pip: this.envFormGroup.controls.pip.value}),
|
||||
docker: this.dockerFormGroup.value,
|
||||
...this.queueFormGroup.value,
|
||||
...(this.queueFormGroup.controls.queue.value ? {queue: this.queues()?.find(queue => queue.name === this.queueFormGroup.controls.queue.value)} : null),
|
||||
} as createExperimentDialogResult)
|
||||
}
|
||||
|
||||
typeChange(value: string, types: string[]) {
|
||||
types.forEach(type => {
|
||||
if (type === value) {
|
||||
this.codeFormGroup.controls[type].setValidators(Validators.required);
|
||||
} else {
|
||||
this.codeFormGroup.controls[type].clearValidators();
|
||||
this.codeFormGroup.controls[type].updateValueAndValidity({onlySelf: true});
|
||||
}
|
||||
this.codeFormGroup.updateValueAndValidity();
|
||||
})
|
||||
}
|
||||
|
||||
togglePoetry(usePoetry: boolean) {
|
||||
if (usePoetry) {
|
||||
Object.keys(this.envFormGroup.controls)
|
||||
.filter(name => name !== 'poetry')
|
||||
.map(name => this.envFormGroup.controls[name] as FormControl)
|
||||
.forEach((control) => control.disable());
|
||||
} else {
|
||||
Object.keys(this.envFormGroup.controls)
|
||||
.filter(name => name !== 'poetry')
|
||||
.map(name => this.envFormGroup.controls[name] as FormControl)
|
||||
.forEach((control) => control.enable());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
sm-experiment-artifacts-navbar {
|
||||
border-right: 1px solid #dee1e9;
|
||||
//border-right: 1px solid #dee1e9;
|
||||
flex: 0 0 300px;
|
||||
&.minimized {
|
||||
flex: 0 0 250px;
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
editable: editable$ | async
|
||||
}" [ngTemplateOutlet]="selfie" #selfie>
|
||||
<sm-overlay [backdropActive]="backdropActive$|async"></sm-overlay>
|
||||
<as-split [unit]="'pixel'"
|
||||
<as-split
|
||||
class="light-theme"
|
||||
[unit]="'pixel'"
|
||||
[gutterSize]=1
|
||||
*ngIf="(modelInfo?.output?.length) || (modelInfo?.input?.length) || (modelInfo?.artifacts?.length) || editable; else noData"
|
||||
>
|
||||
<as-split-area [size]="minimized ? 250 : 360" [minSize]="50">
|
||||
<as-split-area [size]="minimized ? 250 : 360" [minSize]="150">
|
||||
<sm-experiment-artifacts-navbar
|
||||
[class.minimized]="minimized"
|
||||
[artifacts]="modelInfo?.artifacts"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<sm-overlay [backdropActive]="backdropActive$|async"></sm-overlay>
|
||||
<as-split [unit]="'pixel'">
|
||||
<as-split-area [size]="minimized ? 250 : 360" [minSize]="50">
|
||||
<as-split
|
||||
class="light-theme"
|
||||
[unit]="'pixel'"
|
||||
[gutterSize]=1
|
||||
>
|
||||
<as-split-area [size]="minimized ? 250 : 360" [minSize]="150">
|
||||
<sm-experiment-hyper-params-navbar
|
||||
[class.minimized]="minimized"
|
||||
[hyperParams]="(hyperParamsInfo$| async)"
|
||||
|
||||
@@ -24,7 +24,7 @@ import {isReadOnly} from '@common/shared/utils/is-read-only';
|
||||
import {MESSAGES_SEVERITY} from '@common/constants';
|
||||
import {setBreadcrumbsOptions} from '@common/core/actions/projects.actions';
|
||||
import {selectSelectedProject} from '@common/core/reducers/projects.reducer';
|
||||
import { setContextMenu } from '@common/core/actions/router.actions';
|
||||
import {headerActions} from '@common/core/actions/router.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-base-experiment-output',
|
||||
@@ -160,7 +160,7 @@ export abstract class BaseExperimentOutputComponent implements OnInit, OnDestroy
|
||||
parts.splice(5, 0, 'output');
|
||||
this.router.navigateByUrl(parts.join('/'));
|
||||
}
|
||||
this.store.dispatch(setContextMenu({contextMenu: null}));
|
||||
this.store.dispatch(headerActions.setTabs({contextMenu: null}));
|
||||
this.toMaximize = true;
|
||||
}
|
||||
onActivate(e, scrollContainer) {
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
<sm-menu
|
||||
[iconClass]="'al-icon al-ico-settings al-color pointer create-new-icon ' + (disabled ? 'pointer-events-none blue-500' : 'blue-300')"
|
||||
smMenuClass="light-theme custom-columns"
|
||||
#smMenu
|
||||
[iconClass]="menuHeader() ? 'al-icon al-ico-dropdown-arrow' : 'al-icon al-ico-settings al-color pointer create-new-icon ' + (disabled() ? 'pointer-events-none blue-500' : 'blue-300')"
|
||||
[header]="menuHeader()"
|
||||
[smMenuClass]="darkTheme() ? 'dark-theme dark custom-columns' : 'light-theme custom-columns'"
|
||||
data-id="CustomizeColumn"
|
||||
[buttonTooltip]=" skipValueType ? 'Customize charts view' : 'Customize table'"
|
||||
(mouseup)="!disabled && getMetricsToDisplay.emit()"
|
||||
(menuClosed)="setMode(customColumnModeEnum.Standard)"
|
||||
[style.pointer-events]="disabled ? 'none' : 'initial'"
|
||||
>
|
||||
<div *ngIf="!customColumnMode" (click)="$event.stopPropagation()">
|
||||
<sm-custom-columns-list
|
||||
[tableCols]="tableCols"
|
||||
[isLoading]="isLoading"
|
||||
[menuTitle]="skipValueType ? 'CUSTOMIZE CHARTS VIEW' : 'CUSTOMIZE COLUMNS' "
|
||||
(removeColFromList)="removeColFromList.emit($event)"
|
||||
(selectedTableColsChanged)="selectedTableColsChanged.emit($event)"
|
||||
>
|
||||
</sm-custom-columns-list>
|
||||
|
||||
<div [ngClass]="{loading: isLoading, loaded: !isLoading}">
|
||||
<div *ngIf="hyperParams" class="sm-menu-header">ADD CUSTOM COLUMN</div>
|
||||
<div class="custom-column-buttons">
|
||||
<div class="add-button metrics-button"
|
||||
[class.only-metrics]="!hyperParams"
|
||||
[class.disabled]="!metricVariants?.length"
|
||||
smClickStopPropagation
|
||||
(click)="$event.stopPropagation(); metricVariants?.length && setMode(customColumnModeEnum.Metrics)"
|
||||
><i class="al-icon al-ico-plus sm-md me-1"></i><span data-id="Metric Button" class="caption">ADD METRIC</span>
|
||||
</div>
|
||||
<div *ngIf="hyperParams" class="add-button metrics-button"
|
||||
smClickStopPropagation
|
||||
[class.disabled]="!hasHyperParams"
|
||||
(click)="$event.stopPropagation(); hasHyperParams && setMode(customColumnModeEnum.HyperParams)"
|
||||
><i class="al-icon al-ico-plus sm-md me-1"></i><span data-id="Hyperparameters Button" class="caption">HYPERPARAMETERS</span>
|
||||
[buttonTooltip]="menuTooltip() ?? topTitle()"
|
||||
(mouseup)="!disabled() && getMetricsToDisplay.emit()"
|
||||
(menuClosed)="customColumnMode.set(customColumnModeEnum.Standard)"
|
||||
[style.pointer-events]="disabled() ? 'none' : 'initial'"
|
||||
>
|
||||
@if (!customColumnMode()) {
|
||||
<div (click)="$event.stopPropagation()" [class.dark]="darkTheme()">
|
||||
<sm-custom-columns-list
|
||||
[tableCols]="tableCols()"
|
||||
[isLoading]="isLoading()"
|
||||
[selectable]="selectable()"
|
||||
[menuTitle]="topTitle() | uppercase"
|
||||
(removeColFromList)="removeColFromList.emit($event)"
|
||||
(selectedTableColsChanged)="selectedTableColsChanged.emit($event); !selectable() && smMenu.trigger.closeMenu()"
|
||||
>
|
||||
</sm-custom-columns-list>
|
||||
<div [class.loading]="isLoading()"
|
||||
[class.loaded]="!isLoading()">
|
||||
@if (sections().length > 1) {
|
||||
<div class="sm-menu-header">{{sectionsTitle()}}</div>
|
||||
}
|
||||
<div class="custom-column-buttons">
|
||||
<div class="add-button metrics-button"
|
||||
[class.only-one-section]="sections().length === 1"
|
||||
[class.disabled]="!sections()[0]?.options?.length"
|
||||
smClickStopPropagation
|
||||
(click)="$event.stopPropagation(); sections()[0]?.options?.length && customColumnMode.set(customColumnModeEnum.Metrics)"
|
||||
><i class="al-icon al-ico-plus sm-md me-1"></i><span data-id="Metric Button" class="caption">{{sections()[0]?.title ?? sections()[0]?.name | uppercase}}</span>
|
||||
</div>
|
||||
@if (sections()[1]) {
|
||||
<div class="add-button metrics-button"
|
||||
smClickStopPropagation
|
||||
[class.disabled]="!hasSecondSection()"
|
||||
(click)="$event.stopPropagation(); hasSecondSection() && customColumnMode.set(customColumnModeEnum.HyperParams)"
|
||||
><i class="al-icon al-ico-plus sm-md me-1"></i><span data-id="Hyperparameters Button" class="caption">{{sections()[1]?.title ?? sections()[1]?.name | uppercase}}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<sm-select-metric-for-custom-col *ngIf="customColumnMode === customColumnModeEnum.Metrics"
|
||||
[tableCols]="tableCols"
|
||||
[metricVariants]="metricVariants"
|
||||
[skipValueType]="skipValueType"
|
||||
(goBack)="setMode(customColumnModeEnum.Standard)"
|
||||
(selectedMetricToShow)="selectedMetricToShow.emit($event)">
|
||||
</sm-select-metric-for-custom-col>
|
||||
<sm-select-hyper-params-for-custom-col *ngIf="customColumnMode === customColumnModeEnum.HyperParams"
|
||||
class="hyper-params-custom-col"
|
||||
[tableCols]="tableCols"
|
||||
[hyperParams]="hyperParams"
|
||||
(goBack)="setMode(customColumnModeEnum.Standard)"
|
||||
(selectedHyperParamToShow)="selectedHyperParamToShow.emit($event)"
|
||||
(clearSelection)="clearSelection.emit()">
|
||||
</sm-select-hyper-params-for-custom-col>
|
||||
}
|
||||
@if (customColumnMode() === customColumnModeEnum.Metrics) {
|
||||
<ng-container *ngTemplateOutlet="sections()[0].template; context: {$implicit: sections()[0]}"></ng-container>
|
||||
}
|
||||
@if (customColumnMode() === customColumnModeEnum.HyperParams) {
|
||||
<ng-container *ngTemplateOutlet="sections()[1].template; context: {$implicit: sections()[1]}"></ng-container>
|
||||
}
|
||||
</sm-menu>
|
||||
|
||||
@@ -23,19 +23,28 @@
|
||||
width: 50%;
|
||||
white-space: nowrap;
|
||||
|
||||
&.only-metrics {
|
||||
&.only-one-section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin-top: 4px;
|
||||
}
|
||||
&:first-child:not(.only-metrics) {
|
||||
&:first-child:not(.only-one-section) {
|
||||
border-right: 1px solid $blue-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.custom-column-buttons .metrics-button {
|
||||
color: $blue-300;
|
||||
&:hover {
|
||||
color: $blue-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
@@ -61,12 +70,6 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
sm-select-metric-for-custom-col {
|
||||
display: block;
|
||||
height: 640px;
|
||||
max-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.custom-columns-disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,51 @@
|
||||
import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, computed, EventEmitter, input, model, Output, TemplateRef} from '@angular/core';
|
||||
import {CustomColumnMode} from '../../shared/common-experiments.const';
|
||||
import {ISmCol} from '@common/shared/ui-components/data/table/table.consts';
|
||||
import {
|
||||
SelectionEvent
|
||||
SelectMetricForCustomColComponent
|
||||
} from '@common/experiments/dumb/select-metric-for-custom-col/select-metric-for-custom-col.component';
|
||||
import {MenuComponent} from '@common/shared/ui-components/panel/menu/menu.component';
|
||||
import {CustomColumnsListComponent} from '@common/shared/components/custom-columns-list/custom-columns-list.component';
|
||||
import {ClickStopPropagationDirective} from '@common/shared/ui-components/directives/click-stop-propagation.directive';
|
||||
import {SelectHyperParamsForCustomColComponent} from '@common/experiments/dumb/select-hyper-params-for-custom-col/select-hyper-params-for-custom-col.component';
|
||||
import {NgTemplateOutlet, UpperCasePipe} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-experiment-custom-cols-menu',
|
||||
selector: 'sm-custom-cols-menu',
|
||||
templateUrl: './experiment-custom-cols-menu.component.html',
|
||||
styleUrls: ['./experiment-custom-cols-menu.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
MenuComponent,
|
||||
CustomColumnsListComponent,
|
||||
ClickStopPropagationDirective,
|
||||
SelectMetricForCustomColComponent,
|
||||
SelectHyperParamsForCustomColComponent,
|
||||
NgTemplateOutlet,
|
||||
UpperCasePipe
|
||||
],
|
||||
standalone: true
|
||||
})
|
||||
export class ExperimentCustomColsMenuComponent {
|
||||
public hasHyperParams: boolean;
|
||||
private _hyperParams: any[];
|
||||
|
||||
@Input() metricVariants;
|
||||
@Input() skipValueType;
|
||||
@Input() tableCols;
|
||||
@Input() disabled: boolean;
|
||||
|
||||
@Input() set hyperParams(hyperParams) {
|
||||
this._hyperParams = hyperParams;
|
||||
this.hasHyperParams = Object.values(hyperParams ?? {}).some(section => Object.keys(section).length > 0);
|
||||
}
|
||||
get hyperParams() {
|
||||
return this._hyperParams;
|
||||
}
|
||||
|
||||
@Input() isLoading: boolean;
|
||||
@Input() menuTitle: string;
|
||||
sections = input<{title?: string; name: string; options: any[]; skipValue?: boolean; template: TemplateRef<any>}[]>();
|
||||
menuHeader = input<string>();
|
||||
topTitle = input<string>();
|
||||
menuTooltip = input<string>();
|
||||
sectionsTitle = input<string>();
|
||||
selectable = input<boolean>(true);
|
||||
tableCols = input<ISmCol[]>()
|
||||
disabled = input<boolean>();
|
||||
isLoading = input<boolean>();
|
||||
darkTheme = input<boolean>();
|
||||
customColumnMode = model(CustomColumnMode.Standard as CustomColumnMode);
|
||||
hasSecondSection = computed(() =>
|
||||
Object.values(this.sections()?.[1]?.options ?? {}).some(section => Object.keys(section).length > 0));
|
||||
|
||||
@Output() getMetricsToDisplay = new EventEmitter();
|
||||
@Output() removeColFromList = new EventEmitter<ISmCol['id']>();
|
||||
@Output() selectedTableColsChanged = new EventEmitter<ISmCol>();
|
||||
@Output() selectedMetricToShow = new EventEmitter<SelectionEvent>();
|
||||
@Output() selectedHyperParamToShow = new EventEmitter<{param: string; addCol: boolean}>();
|
||||
@Output() clearSelection = new EventEmitter();
|
||||
|
||||
customColumnMode = CustomColumnMode.Standard as CustomColumnMode;
|
||||
public customColumnModeEnum = CustomColumnMode;
|
||||
|
||||
setMode(mode: CustomColumnMode) {
|
||||
this.customColumnMode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,100 @@
|
||||
<div class="d-flex justify-content-between header-container align-items-center"
|
||||
[ngClass]="{'archive-mode': isArchived}">
|
||||
[class.archive-mode]="isArchived()">
|
||||
<div class="d-flex-center">
|
||||
<ng-container *ngTemplateOutlet="addButtonTemplate; context: {smallScreen: (isSmallScreen$ | async).matches}">
|
||||
<ng-container *ngTemplateOutlet="addButtonTemplate(); context: {smallScreen: (isSmallScreen$ | async).matches}">
|
||||
</ng-container>
|
||||
<sm-toggle-archive
|
||||
[class.hide-item]="sharedView"
|
||||
[showArchived]="isArchived"
|
||||
[class.hide-item]="sharedView()"
|
||||
[showArchived]="isArchived()"
|
||||
[minimize]="(isSmallScreen$ | async).matches"
|
||||
(toggleArchived)="isArchivedChanged.emit($event)"
|
||||
></sm-toggle-archive>
|
||||
<sm-button-toggle
|
||||
[disabled]="!tableMode || noData"
|
||||
[disabled]="!tableMode() || noData"
|
||||
class="ms-3"
|
||||
[options]="toggleButtons"
|
||||
[value]="!noData && tableMode"
|
||||
[rippleEffect]="rippleEffect"
|
||||
[value]="!noData && tableMode()"
|
||||
[rippleEffect]="rippleEffect()"
|
||||
(valueChanged)="tableModeChanged.emit($event)"></sm-button-toggle>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-end align-items-center right-buttons">
|
||||
<sm-clear-filters-button
|
||||
*ngIf="!minimizedView"
|
||||
[tableFilters]="tableFilters"
|
||||
(clearTableFilters)="clearTableFilters.emit(tableFilters)"
|
||||
></sm-clear-filters-button>
|
||||
<sm-menu *ngIf="tableMode !== 'compare'"
|
||||
class="download-btn" buttonClass="al-icon al-ico-download pointer lm" panelClasses="light-theme"
|
||||
[showCart]="false" smTooltip="Download table as CSV" [disabled]="noData" data-id="downloadCSV">
|
||||
<sm-menu-item (itemClicked)="downloadTableAsCSV.emit()" itemLabel="Download on screen items"></sm-menu-item>
|
||||
<sm-menu-item (itemClicked)="downloadFullTableAsCSV.emit()"
|
||||
[itemLabel]="'Download first '+ (maxDownloadItems$ | async) +' items'"></sm-menu-item>
|
||||
</sm-menu>
|
||||
@if (!minimizedView()) {
|
||||
<sm-clear-filters-button
|
||||
[tableFilters]="tableFilters()"
|
||||
(clearTableFilters)="clearTableFilters.emit(tableFilters())"
|
||||
></sm-clear-filters-button>
|
||||
}
|
||||
@if (tableMode() !== 'compare') {
|
||||
<sm-menu
|
||||
class="download-btn" buttonClass="al-icon al-ico-download pointer lm" panelClasses="light-theme"
|
||||
[showCart]="false" smTooltip="Download table as CSV" [disabled]="noData" data-id="downloadCSV">
|
||||
<sm-menu-item (itemClicked)="downloadTableAsCSV.emit()" itemLabel="Download on screen items"></sm-menu-item>
|
||||
<sm-menu-item (itemClicked)="downloadFullTableAsCSV.emit()"
|
||||
[itemLabel]="'Download first '+ (maxDownloadItems$ | async) +' items'"></sm-menu-item>
|
||||
</sm-menu>
|
||||
}
|
||||
|
||||
<mat-form-field *ngIf="tableMode === 'compare'" appearance="outline" class="dark-outline compare-view-select no-bottom">
|
||||
<mat-select
|
||||
name="compareView"
|
||||
panelClass="dark-outline"
|
||||
[ngModel]="compareView"
|
||||
(selectionChange)="compareViewChanged.emit($event.value)"
|
||||
>
|
||||
<mat-option value="scalars">Scalars</mat-option>
|
||||
<mat-option value="plots">Plots</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (tableMode() === 'compare') {
|
||||
<mat-form-field appearance="outline" class="dark-outline compare-view-select no-bottom">
|
||||
<mat-select
|
||||
name="compareView"
|
||||
panelClass="dark-outline"
|
||||
[ngModel]="compareView()"
|
||||
(selectionChange)="compareViewChanged.emit($event.value)"
|
||||
>
|
||||
<mat-option value="scalars">Scalars</mat-option>
|
||||
<mat-option value="plots">Plots</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<i class="al-icon al-ico-tune sm-md"
|
||||
*ngIf="tableMode === 'compare' && compareView === 'scalars'"
|
||||
[class.active]="showCompareScalarSettings"
|
||||
(click)="toggleShowCompareSettings.emit()"></i>
|
||||
@if (tableMode() === 'compare' && compareView() === 'scalars') {
|
||||
<i class="al-icon al-ico-tune sm-md"
|
||||
[class.active]="showCompareScalarSettings()"
|
||||
(click)="toggleShowCompareSettings.emit()"></i>
|
||||
}
|
||||
|
||||
<sm-experiment-custom-cols-menu
|
||||
*ngIf="!minimizedView || tableMode === 'compare'"
|
||||
[metricVariants]="metricVariants"
|
||||
[hyperParams]="hyperParams"
|
||||
[tableCols]="tableCols"
|
||||
[isLoading]="isMetricsLoading"
|
||||
[skipValueType]="tableMode === 'compare'"
|
||||
[disabled]="tableMode === 'compare' && metricVariants?.length === 0"
|
||||
(selectedMetricToShow)="selectedMetricToShow.emit($event)"
|
||||
(selectedHyperParamToShow)="selectedHyperParamToShow.emit($event)"
|
||||
(selectedTableColsChanged)="selectedTableColsChanged.emit($event)"
|
||||
(getMetricsToDisplay)="getMetricsToDisplay.emit($event)"
|
||||
(removeColFromList)="removeColFromList.emit($event)"
|
||||
(clearSelection)="clearSelection.emit()"
|
||||
></sm-experiment-custom-cols-menu>
|
||||
@if (!minimizedView() || tableMode() === 'compare') {
|
||||
<sm-custom-cols-menu
|
||||
[sections]="tableMode() === 'compare' ? [{title: 'add metric', name: 'metrics', options: metricVariants(), template: metricsTemplate}] : [{title: 'add metric',name: 'metrics', options: metricVariants(), template: metricsTemplate}, {name: 'hyperparameters', options: hyperParams(), template: hyperParamsTemplate}]"
|
||||
[topTitle]="tableMode() === 'compare' ? 'Customize charts view' : 'Customize columns'"
|
||||
[menuTooltip]="tableMode() === 'compare' ? 'Customize charts view' : 'Customize table'"
|
||||
[sectionsTitle]="'ADD CUSTOM COLUMN'"
|
||||
[tableCols]="tableColsWithHeader()"
|
||||
[isLoading]="isMetricsLoading()"
|
||||
[disabled]="tableMode() === 'compare' && metricVariants()?.length === 0"
|
||||
[(customColumnMode)]="customColumnMode"
|
||||
(selectedTableColsChanged)="selectedTableColsChanged.emit($event)"
|
||||
(getMetricsToDisplay)="getMetricsToDisplay.emit($event)"
|
||||
(removeColFromList)="removeColFromList.emit($event)"
|
||||
></sm-custom-cols-menu>
|
||||
}
|
||||
<sm-refresh-button
|
||||
[allowAutoRefresh]="true"
|
||||
(setAutoRefresh)="setAutoRefresh.emit($event)"
|
||||
></sm-refresh-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #metricsTemplate let-sectionData>
|
||||
<sm-select-metric-for-custom-col
|
||||
[tableCols]="tableColsWithHeader()"
|
||||
[metricVariants]="sectionData.options"
|
||||
[skipValueType]="tableMode() === 'compare'"
|
||||
(goBack)="customColumnMode = customColumnModeEnum.Standard"
|
||||
(selectedMetricToShow)="selectedMetricToShow.emit($event)">
|
||||
</sm-select-metric-for-custom-col>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #hyperParamsTemplate let-sectionData>
|
||||
<sm-select-hyper-params-for-custom-col
|
||||
class="hyper-params-custom-col"
|
||||
[tableCols]="tableCols()"
|
||||
[hyperParams]="sectionData.options"
|
||||
(goBack)="customColumnMode = customColumnModeEnum.Standard"
|
||||
(selectedHyperParamToShow)="selectedHyperParamToShow.emit($event)"
|
||||
(clearSelection)="clearSelection.emit()">
|
||||
</sm-select-hyper-params-for-custom-col>
|
||||
</ng-template>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
sm-experiment-custom-cols-menu {
|
||||
sm-custom-cols-menu {
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
.compare-view-select {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sm-select-metric-for-custom-col {
|
||||
display: block;
|
||||
height: 640px;
|
||||
max-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
|
||||
import {Component, computed, EventEmitter, input, OnInit, Output, TemplateRef} from '@angular/core';
|
||||
import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult';
|
||||
import {ISmCol} from '@common/shared/ui-components/data/table/table.consts';
|
||||
import {FilterMetadata} from 'primeng/api/filtermetadata';
|
||||
@@ -8,6 +8,7 @@ import {Option} from '@common/shared/ui-components/inputs/button-toggle/button-t
|
||||
import {trackByValue} from '@common/shared/utils/forms-track-by';
|
||||
import {resourceToIconMap} from '~/features/experiments/experiments.consts';
|
||||
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
|
||||
import {CustomColumnMode} from '@common/experiments/shared/common-experiments.const';
|
||||
|
||||
@Component({
|
||||
selector : 'sm-experiment-header',
|
||||
@@ -15,29 +16,27 @@ import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
|
||||
styleUrls : ['./experiment-header.component.scss']
|
||||
})
|
||||
export class ExperimentHeaderComponent extends BaseEntityHeaderComponent implements OnInit{
|
||||
private _tableCols: ISmCol[];
|
||||
|
||||
protected readonly customColumnModeEnum = CustomColumnMode;
|
||||
customColumnMode: CustomColumnMode;
|
||||
toggleButtons: Option[];
|
||||
@Input() isArchived: boolean;
|
||||
@Input() metricVariants: Array<MetricVariantResult>;
|
||||
@Input() hyperParams: any[];
|
||||
@Input() minimizedView: boolean;
|
||||
@Input() isMetricsLoading: boolean;
|
||||
@Input() tableFilters: { [s: string]: FilterMetadata };
|
||||
@Input() sharedView: boolean;
|
||||
@Input() showNavbarLinks: boolean;
|
||||
@Input() tableMode: 'table' | 'info' | 'compare';
|
||||
@Input() compareView: 'scalars' | 'plots';
|
||||
@Input() showCompareScalarSettings: boolean;
|
||||
@Input() rippleEffect: boolean;
|
||||
@Input() addButtonTemplate: TemplateRef<{smallScreen: boolean}>;
|
||||
|
||||
@Input() set tableCols(tableCols) {
|
||||
this._tableCols = tableCols?.filter(col => col.header !== '');
|
||||
}
|
||||
isArchived = input<boolean>();
|
||||
metricVariants = input<Array<MetricVariantResult>>();
|
||||
hyperParams = input<any[]>();
|
||||
minimizedView = input<boolean>();
|
||||
isMetricsLoading = input<boolean>();
|
||||
tableFilters = input<{ [s: string]: FilterMetadata }>();
|
||||
sharedView = input<boolean>();
|
||||
showNavbarLinks = input<boolean>();
|
||||
tableMode = input<'table' | 'info' | 'compare'>();
|
||||
compareView = input<'scalars' | 'plots'>();
|
||||
showCompareScalarSettings = input<boolean>();
|
||||
rippleEffect = input<boolean>();
|
||||
addButtonTemplate = input<TemplateRef<{smallScreen: boolean}>>();
|
||||
|
||||
get tableCols() {
|
||||
return this._tableCols;
|
||||
}
|
||||
tableCols = input<ISmCol[]>();
|
||||
tableColsWithHeader = computed(() => this.tableCols()?.filter(col => col.header !== ''));
|
||||
|
||||
@Output() isArchivedChanged = new EventEmitter<boolean>();
|
||||
@Output() selectedTableColsChanged = new EventEmitter<ISmCol>();
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
(tagSelected)="addTag($event)"
|
||||
></sm-experiment-menu-extended>
|
||||
<div *ngIf="minimized" (click)="closeInfoClicked.emit()" class="d-flex align-items-center line-item" data-id="Cross Button">
|
||||
<i class="al-icon al-ico-dialog-x pointer"></i>
|
||||
<i class="al-icon al-ico-dialog-x pointer" data-id="closeButton"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,6 @@ import Convert from 'ansi-to-html';
|
||||
import {Log} from '../../actions/common-experiment-output.actions';
|
||||
|
||||
import hasAnsi from 'has-ansi';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
interface LogRow {
|
||||
timestamp?: string;
|
||||
@@ -191,8 +190,7 @@ export class ExperimentLogInfoComponent implements OnDestroy, AfterViewInit {
|
||||
.filter(msg => !!msg)
|
||||
.forEach((msg: string, index) => {
|
||||
const msgHasAnsi = this.hasAnsi(msg);
|
||||
const converted = msg ? (msgHasAnsi ? DOMPurify.sanitize(this.convert.toHtml(msg)) :
|
||||
msg) : '';
|
||||
const converted = msg ? (msgHasAnsi ? this.convert.toHtml(msg) : msg) : '';
|
||||
if (!index) {
|
||||
this.lines.push({timestamp: logItem['timestamp'] || logItem['@timestamp'], entry: converted, hasAnsi: msgHasAnsi});
|
||||
} else {
|
||||
|
||||
@@ -264,6 +264,7 @@ export class ExperimentsTableComponent extends BaseTableView implements OnInit,
|
||||
[EXPERIMENTS_TABLE_COL_FIELDS.TAGS]: [],
|
||||
[EXPERIMENTS_TABLE_COL_FIELDS.PARENT]: null,
|
||||
[EXPERIMENTS_TABLE_COL_FIELDS.PROJECT]: null,
|
||||
[EXPERIMENTS_TABLE_COL_FIELDS.VERSION]: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -354,7 +355,7 @@ export class ExperimentsTableComponent extends BaseTableView implements OnInit,
|
||||
this.tagsMenuOpened.emit();
|
||||
}
|
||||
} else if (col.id.includes('hyperparams')) {
|
||||
this.store.dispatch(hyperParamSelectedInfoExperiments({col: {id: col.id}, loadMore: false, values: null}));
|
||||
this.store.dispatch(hyperParamSelectedInfoExperiments({col: {id: col.id}, loadMore: false, values: []}));
|
||||
this.store.dispatch(setHyperParamsFiltersPage({page: 0}));
|
||||
this.store.dispatch(hyperParamSelectedExperiments({col, searchValue: ''}));
|
||||
} else if (col.id === EXPERIMENTS_TABLE_COL_FIELDS.PROJECT) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid $blue-200;
|
||||
min-height: 48px;
|
||||
h3 {
|
||||
color: $blue-400;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {GroupedCheckedFilterListComponent} from '@common/shared/ui-components/data/grouped-checked-filter-list/grouped-checked-filter-list.component';
|
||||
import {ClickStopPropagationDirective} from '@common/shared/ui-components/directives/click-stop-propagation.directive';
|
||||
|
||||
@Component({
|
||||
selector : 'sm-select-hyper-params-for-custom-col',
|
||||
selector: 'sm-select-hyper-params-for-custom-col',
|
||||
templateUrl: './select-hyper-params-for-custom-col.component.html',
|
||||
styleUrls : ['./select-hyper-params-for-custom-col.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
styleUrls: ['./select-hyper-params-for-custom-col.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
GroupedCheckedFilterListComponent,
|
||||
ClickStopPropagationDirective
|
||||
],
|
||||
standalone: true
|
||||
})
|
||||
export class SelectHyperParamsForCustomColComponent {
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div smClickStopPropagation class="metrics-container">
|
||||
<div *ngIf="goBack.observed" class="head">
|
||||
<i (click)="goBack.emit()" data-id="Back button" class="al-icon sm-md al-ico-back pointer m-auto" smClickStopPropagation></i>
|
||||
<h3>SELECT METRIC TO DISPLAY</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@if (goBack.observed) {
|
||||
<div class="head">
|
||||
<i (click)="goBack.emit()" data-id="Back button" class="al-icon sm-md al-ico-back pointer m-auto" smClickStopPropagation></i>
|
||||
<h3>SELECT METRIC TO DISPLAY</h3>
|
||||
</div>
|
||||
}
|
||||
<sm-search
|
||||
class="underline-search"
|
||||
[value]="searchText"
|
||||
@@ -14,76 +14,88 @@
|
||||
(valueChanged)="searchQ($event)"
|
||||
></sm-search>
|
||||
<div class="metrics" [class.has-title]="goBack.observed">
|
||||
<div *ngIf="!filteredMetricTree && !(searchText?.length > 0)" class="p-4 pe-none">
|
||||
<mat-spinner class="m-auto" [diameter]="32" [strokeWidth]="4" color="accent"></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="filteredMetricTree && filteredMetricTree.length === 0" class="d-flex h-100">
|
||||
<div class="empty-state">No data to show</div>
|
||||
</div>
|
||||
@if (!filteredMetricTree && !(searchText?.length > 0)) {
|
||||
<div class="p-4 pe-none">
|
||||
<mat-spinner class="m-auto" [diameter]="32" [strokeWidth]="4" color="accent"></mat-spinner>
|
||||
</div>
|
||||
}
|
||||
@if (filteredMetricTree && filteredMetricTree.length === 0) {
|
||||
<div class="d-flex h-100">
|
||||
<div class="empty-state">No data to show</div>
|
||||
</div>
|
||||
}
|
||||
@if (enableClearSelection && !(metricsCols| isEmpty)) {
|
||||
<div class="actions pointer" (click)="clearSelection.emit()">Clear Selection</div>
|
||||
}
|
||||
<mat-expansion-panel
|
||||
*ngFor="let metric of filteredMetricTree; trackBy: trackByMetric"
|
||||
[expanded]="searchText?.length > 0 || expandedMetrics[metric[1][0].metric_hash]"
|
||||
>
|
||||
<mat-expansion-panel-header
|
||||
class="py-2" expandedHeight="42px" collapsedHeight="42px"
|
||||
(click)="expandedMetrics[metric[1][0].metric_hash] = !expandedMetrics[metric[1][0].metric_hash]"
|
||||
>
|
||||
<mat-panel-title class="w-100">
|
||||
<span class="panel-title ellipsis" data-id="metricType" [smTooltip]="metric[0]" smShowTooltipIfEllipsis>{{ metric[0] }}</span>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<div class="metric-expansion-content" *ngFor="let variant of metricTree[metric[0]] | advancedFilter:searchText; trackBy: trackByVariant">
|
||||
<ng-container *ngIf="multiSelect; else single">
|
||||
<div class="variant-label">
|
||||
<sm-checkbox-control #metricVariant
|
||||
(formDataChanged)="!skipValueType ? toggleAllMetricsToDisplay(variant, metricVariant.formData) : toggleMetricToDisplay(variant, metricVariant.formData, 'value')"
|
||||
[formData]="metricsCols[variant.metric_hash + variant.variant_hash] !== undefined"
|
||||
[label]="variant.variant"
|
||||
></sm-checkbox-control>
|
||||
@for (metric of filteredMetricTree; track metric[1][0].metric_hash) {
|
||||
<mat-expansion-panel
|
||||
[expanded]="searchText?.length > 0 || expandedMetrics[metric[1][0].metric_hash]"
|
||||
>
|
||||
<mat-expansion-panel-header
|
||||
class="py-2" expandedHeight="42px" collapsedHeight="42px"
|
||||
(click)="expandedMetrics[metric[1][0].metric_hash] = !expandedMetrics[metric[1][0].metric_hash]"
|
||||
>
|
||||
<mat-panel-title class="w-100">
|
||||
<span class="panel-title ellipsis" data-id="metricType" [smTooltip]="metric[0]" smShowTooltipIfEllipsis>{{ metric[0] }}</span>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
@for (variant of metricTree[metric[0]] | advancedFilter:searchText; track variant.variant_hash) {
|
||||
<div class="metric-expansion-content">
|
||||
@if (multiSelect) {
|
||||
<div class="variant-label">
|
||||
<sm-checkbox-control #metricVariant
|
||||
(formDataChanged)="!skipValueType ? toggleAllMetricsToDisplay(variant, metricVariant.formData) : toggleMetricToDisplay(variant, metricVariant.formData, 'value')"
|
||||
[formData]="metricsCols[variant.metric_hash + variant.variant_hash] !== undefined"
|
||||
[label]="variant.variant"
|
||||
></sm-checkbox-control>
|
||||
</div>
|
||||
@if (metricVariant.formData && !skipValueType) {
|
||||
<div class="value-type">
|
||||
<sm-checkbox-control #last label="LAST"
|
||||
[formData]="metricsCols[variant.metric_hash + variant.variant_hash]?.includes('value')"
|
||||
(formDataChanged)="toggleMetricToDisplay(variant, last.formData, 'value')"
|
||||
></sm-checkbox-control>
|
||||
<sm-checkbox-control #min label="MIN"
|
||||
[formData]="metricsCols[variant.metric_hash + variant.variant_hash]?.includes('min_value')"
|
||||
(formDataChanged)="toggleMetricToDisplay(variant, min.formData, 'min_value')"
|
||||
></sm-checkbox-control>
|
||||
<sm-checkbox-control #max label="MAX"
|
||||
[formData]="metricsCols[variant.metric_hash + variant.variant_hash]?.includes('max_value')"
|
||||
(formDataChanged)="toggleMetricToDisplay(variant, max.formData, 'max_value')"
|
||||
></sm-checkbox-control>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="variant-label">
|
||||
<mat-radio-button
|
||||
class="sm"
|
||||
#metricVariant
|
||||
(change)="toggleAllMetricsToDisplay(variant, false)"
|
||||
[checked]="metricsCols[variant.metric_hash + variant.variant_hash] !== undefined"
|
||||
>{{ variant.variant }}
|
||||
</mat-radio-button>
|
||||
</div>
|
||||
@if (metricVariant.checked) {
|
||||
<div class="value-type">
|
||||
<mat-radio-group
|
||||
[ngModel]="metricsCols[variant.metric_hash + variant.variant_hash][0]"
|
||||
(change)="toggleMetricToDisplay(variant, true, $event.value)"
|
||||
>
|
||||
<mat-radio-button class="sm" value="value">LAST</mat-radio-button>
|
||||
<mat-radio-button class="sm px-4" value="min_value">MIN</mat-radio-button>
|
||||
<mat-radio-button class="sm" value="max_value">MAX</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div *ngIf="metricVariant.formData && !skipValueType" class="value-type">
|
||||
<sm-checkbox-control #last label="LAST"
|
||||
[formData]="metricsCols[variant.metric_hash + variant.variant_hash]?.includes('value')"
|
||||
(formDataChanged)="toggleMetricToDisplay(variant, last.formData, 'value')"
|
||||
></sm-checkbox-control>
|
||||
<sm-checkbox-control #min label="MIN"
|
||||
[formData]="metricsCols[variant.metric_hash + variant.variant_hash]?.includes('min_value')"
|
||||
(formDataChanged)="toggleMetricToDisplay(variant, min.formData, 'min_value')"
|
||||
></sm-checkbox-control>
|
||||
<sm-checkbox-control #max label="MAX"
|
||||
[formData]="metricsCols[variant.metric_hash + variant.variant_hash]?.includes('max_value')"
|
||||
(formDataChanged)="toggleMetricToDisplay(variant, max.formData, 'max_value')"
|
||||
></sm-checkbox-control>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #single>
|
||||
<div class="variant-label">
|
||||
<mat-radio-button
|
||||
class="sm"
|
||||
#metricVariant
|
||||
(change)="toggleAllMetricsToDisplay(variant, false)"
|
||||
[checked]="metricsCols[variant.metric_hash + variant.variant_hash] !== undefined"
|
||||
>{{ variant.variant }}
|
||||
</mat-radio-button>
|
||||
</div>
|
||||
<div *ngIf="metricVariant.checked" class="value-type">
|
||||
<mat-radio-group
|
||||
[ngModel]="metricsCols[variant.metric_hash + variant.variant_hash][0]"
|
||||
(change)="toggleMetricToDisplay(variant, true, $event.value)"
|
||||
>
|
||||
<mat-radio-button class="sm" value="value">LAST</mat-radio-button>
|
||||
<mat-radio-button class="sm px-4" value="min_value">MIN</mat-radio-button>
|
||||
<mat-radio-button class="sm" value="max_value">MAX</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
<div class="more-results" *ngIf="moreResults > 0">And {{ moreResults }} more, use search to narrow selection</div>
|
||||
}
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
@if (moreResults > 0) {
|
||||
<div class="more-results">And {{ moreResults }} more, use search to narrow selection</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid $blue-200;
|
||||
min-height: 48px;
|
||||
h3 {
|
||||
color: $blue-400;
|
||||
font-size: 14px;
|
||||
@@ -101,10 +102,6 @@
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
padding-left: 12px;
|
||||
|
||||
&.has-title {
|
||||
// height: calc(100% - #{90px});
|
||||
}
|
||||
}
|
||||
|
||||
.metric-expansion-content {
|
||||
|
||||
@@ -2,6 +2,17 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Inp
|
||||
import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult';
|
||||
import {ISmCol} from '@common/shared/ui-components/data/table/table.consts';
|
||||
import {MetricValueType} from '@common/experiments-compare/experiments-compare.constants';
|
||||
import {SearchComponent} from '@common/shared/ui-components/inputs/search/search.component';
|
||||
import {ClickStopPropagationDirective} from '@common/shared/ui-components/directives/click-stop-propagation.directive';
|
||||
import {MatProgressSpinner} from '@angular/material/progress-spinner';
|
||||
import {MatExpansionPanel, MatExpansionPanelContent, MatExpansionPanelHeader, MatExpansionPanelTitle} from '@angular/material/expansion';
|
||||
import {IsEmptyPipe} from '@common/shared/pipes/is-empty.pipe';
|
||||
import {TooltipDirective} from '@common/shared/ui-components/indicators/tooltip/tooltip.directive';
|
||||
import {AdvancedFilterPipe} from '@common/shared/pipes/advanced-filter.pipe';
|
||||
import {CheckboxControlComponent} from '@common/shared/ui-components/forms/checkbox-control/checkbox-control.component';
|
||||
import {MatRadioButton, MatRadioGroup} from '@angular/material/radio';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indicators/tooltip/show-tooltip-if-ellipsis.directive';
|
||||
|
||||
export interface SelectionEvent {
|
||||
variant: MetricVariantResult;
|
||||
@@ -10,10 +21,28 @@ export interface SelectionEvent {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector : 'sm-select-metric-for-custom-col',
|
||||
selector: 'sm-select-metric-for-custom-col',
|
||||
templateUrl: './select-metric-for-custom-col.component.html',
|
||||
styleUrls : ['./select-metric-for-custom-col.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
styleUrls: ['./select-metric-for-custom-col.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
SearchComponent,
|
||||
ClickStopPropagationDirective,
|
||||
MatProgressSpinner,
|
||||
MatExpansionPanel,
|
||||
IsEmptyPipe,
|
||||
MatExpansionPanelHeader,
|
||||
MatExpansionPanelTitle,
|
||||
TooltipDirective,
|
||||
AdvancedFilterPipe,
|
||||
CheckboxControlComponent,
|
||||
MatExpansionPanelContent,
|
||||
ShowTooltipIfEllipsisDirective,
|
||||
MatRadioButton,
|
||||
MatRadioGroup,
|
||||
FormsModule
|
||||
],
|
||||
standalone: true
|
||||
})
|
||||
export class SelectMetricForCustomColComponent {
|
||||
public metricTree: {[metricName: string]: MetricVariantResult[]};
|
||||
@@ -23,8 +52,6 @@ export class SelectMetricForCustomColComponent {
|
||||
public searchText: string;
|
||||
public entriesLimit = 300;
|
||||
public moreResults: number;
|
||||
trackByMetric = (index, entry: [string, MetricVariantResult[]]) => entry[1][0].metric_hash;
|
||||
trackByVariant = (index, variant: MetricVariantResult) => variant.variant_hash;
|
||||
private debounceTimer: number;
|
||||
|
||||
@Input() set metricVariants(metricVar: Array<MetricVariantResult>) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Actions, concatLatestFrom, createEffect, ofType} from '@ngrx/effects';
|
||||
import {Actions, createEffect, ofType} from '@ngrx/effects';
|
||||
import {concatLatestFrom} from '@ngrx/operators';
|
||||
import {Action, Store} from '@ngrx/store';
|
||||
import {ApiTasksService} from '~/business-logic/api-services/tasks.service';
|
||||
import {Router} from '@angular/router';
|
||||
@@ -98,7 +99,7 @@ export class CommonExperimentsMenuEffects {
|
||||
enqueueExperiment$ = createEffect(() => this.actions$.pipe(
|
||||
ofType(menuActions.enqueueClicked),
|
||||
concatLatestFrom(() => this.store.select(selectSelectedExperiment)),
|
||||
switchMap(([action, selectedEntity]: [ReturnType<typeof menuActions.enqueueClicked>, IExperimentInfo]) => {
|
||||
switchMap(([action, selectedEntity]) => {
|
||||
const ids = action.selectedEntities.map(exp => exp.id);
|
||||
return this.apiTasks.tasksEnqueueMany({
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
@@ -262,7 +263,7 @@ export class CommonExperimentsMenuEffects {
|
||||
data: {tasks: action.experiments, shouldBeAbortedTasks}
|
||||
})).afterClosed()),
|
||||
mergeMap(confirmed => [
|
||||
confirmed ? stopClicked({selectedEntities: [...confirmed.shouldBeAbortedTasks, ...action.experiments]}) : emptyAction(),
|
||||
confirmed ? stopClicked({selectedEntities: action.experiments, includePipelineSteps: isPipeline }) : emptyAction(),
|
||||
deactivateLoader(action.type)
|
||||
]),
|
||||
catchError(error => [deactivateLoader(action.type), requestFailed(error), addMessage(MESSAGES_SEVERITY.ERROR, 'Failed to fetch tasks running children')])
|
||||
@@ -275,7 +276,7 @@ export class CommonExperimentsMenuEffects {
|
||||
concatLatestFrom(() => this.store.select(selectSelectedExperiment)),
|
||||
switchMap(([action, selectedEntity]) => {
|
||||
const ids = action.selectedEntities.map(exp => exp.id);
|
||||
return this.apiTasks.tasksStopMany({ids})
|
||||
return this.apiTasks.tasksStopMany({ids, include_pipeline_steps: action.includePipelineSteps})
|
||||
.pipe(
|
||||
mergeMap(res => this.updateExperimentsSuccess(action, MenuItems.abort, ids, selectedEntity, res)),
|
||||
catchError(error => this.updateExperimentFailed(action.type, error))
|
||||
@@ -410,8 +411,10 @@ export class CommonExperimentsMenuEffects {
|
||||
this.store.select(selectRouterParams),
|
||||
this.store.select(exSelectors.selectSelectedTableExperiment),
|
||||
this.store.select(selectTableMode),
|
||||
this.store.select(selectIsPipelines),
|
||||
]),
|
||||
switchMap(([action, routerParams, selectedExperiment, tableMode]) => this.apiTasks.tasksArchiveMany({ids: action.selectedEntities.map(exp => exp.id)})
|
||||
switchMap(([action, routerParams, selectedExperiment, tableMode, isPipelines]) =>
|
||||
this.apiTasks.tasksArchiveMany({ids: action.selectedEntities.map(exp => exp.id), include_pipeline_steps: isPipelines})
|
||||
.pipe(
|
||||
concatLatestFrom(() => this.store.select(selectRouterConfig)),
|
||||
mergeMap(([res, routerConfig]: [TasksArchiveManyResponse, RouterState['config']]) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {Actions, concatLatestFrom, createEffect, ofType} from '@ngrx/effects';
|
||||
import {Action, Store} from '@ngrx/store';
|
||||
import {cloneDeep, flatten, isEqual} from 'lodash-es';
|
||||
import {EMPTY, forkJoin, iif, interval, Observable, of} from 'rxjs';
|
||||
import {EMPTY, iif, interval, Observable, of} from 'rxjs';
|
||||
import {
|
||||
auditTime,
|
||||
catchError,
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
selectIsArchivedMode,
|
||||
selectIsDeepMode,
|
||||
selectRouterProjectId,
|
||||
selectSelectedProject,
|
||||
selectSelectedProjectId,
|
||||
selectShowHidden
|
||||
} from '../../core/reducers/projects.reducer';
|
||||
import {selectRouterConfig, selectRouterParams} from '../../core/reducers/router-reducer';
|
||||
@@ -109,6 +109,9 @@ import {MetricVariantResult} from '~/business-logic/model/projects/metricVariant
|
||||
import {ApiEventsService} from '~/business-logic/api-services/events.service';
|
||||
import {EventTypeEnum} from '~/business-logic/model/events/eventTypeEnum';
|
||||
import {EventsGetMultiTaskMetricsResponse} from '~/business-logic/model/events/eventsGetMultiTaskMetricsResponse';
|
||||
import {TasksCreateResponse} from '~/business-logic/model/tasks/tasksCreateResponse';
|
||||
import {ErrorService} from '@common/shared/services/error.service';
|
||||
import * as menuActions from '@common/experiments/actions/common-experiments-menu.actions';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@@ -117,7 +120,7 @@ export class CommonExperimentsViewEffects {
|
||||
constructor(
|
||||
private actions$: Actions, private store: Store, private apiTasks: ApiTasksService,
|
||||
private projectsApi: ApiProjectsService, private eventsApi: ApiEventsService, private router: Router,
|
||||
private route: ActivatedRoute, private orgApi: ApiOrganizationService
|
||||
private route: ActivatedRoute, private orgApi: ApiOrganizationService, private errService: ErrorService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -385,7 +388,7 @@ export class CommonExperimentsViewEffects {
|
||||
]),
|
||||
filter(([, , , , tableMode]) => tableMode !== 'table'),
|
||||
tap(([, routeConfig, tasks, projectId]) => this.navigateAfterExperimentSelectionChanged(tasks[0] as ITableExperiment, projectId, routeConfig, true)),
|
||||
mergeMap(([, , tasks, , tableMode]) => [exActions.setTableMode({mode: tableMode}), exActions.setSelectedExperiments({experiments: []})])
|
||||
mergeMap(([, , , , tableMode]) => [exActions.setTableMode({mode: tableMode}), exActions.setSelectedExperiments({experiments: []})])
|
||||
));
|
||||
|
||||
|
||||
@@ -579,12 +582,12 @@ export class CommonExperimentsViewEffects {
|
||||
debounce((action) => interval(action.searchValue ? 300 : 0)),
|
||||
concatLatestFrom(() => [
|
||||
this.store.select(selectIsDeepMode),
|
||||
this.store.select(selectSelectedProject),
|
||||
this.store.select(selectRouterProjectId),
|
||||
this.store.select(selectIsCompare),
|
||||
this.store.select(selectHyperParamsFiltersPage),
|
||||
]),
|
||||
switchMap(([action, isDeep, selectedProject, isCompare, page]) => {
|
||||
const projectId = action.col.projectId || selectedProject.id;
|
||||
switchMap(([action, isDeep, selectedProjectId, isCompare, page]) => {
|
||||
const projectId = action.col.projectId || selectedProjectId;
|
||||
const {section, name} = decodeHyperParam(action.col);
|
||||
return this.projectsApi.projectsGetHyperparamValues({
|
||||
include_subprojects: isDeep,
|
||||
@@ -943,4 +946,85 @@ export class CommonExperimentsViewEffects {
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
createExperiment = createEffect(() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(exActions.createExperiment),
|
||||
concatLatestFrom(() => this.store.select(selectSelectedProjectId)),
|
||||
switchMap(([action, projectId]) => this.apiTasks.tasksCreate({
|
||||
project: projectId,
|
||||
name: action.data.name,
|
||||
type: 'training',
|
||||
script: {
|
||||
repository: action.data.repo,
|
||||
...(action.data.type === 'branch' ?
|
||||
{branch: action.data.branch ?? 'master'} :
|
||||
action.data.type === 'tag' ?
|
||||
{tag: action.data.tag} :
|
||||
{version_num: action.data.commit}
|
||||
),
|
||||
working_dir: action.data.directory,
|
||||
entry_point: action.data.script,
|
||||
binary: action.data.binary,
|
||||
requirements: action.data.requirements === 'manual' ? {pip: action.data.pip} : null,
|
||||
},
|
||||
hyperparams: {
|
||||
Args: action.data.args
|
||||
.filter(arg => arg.key?.length > 0)
|
||||
.reduce((acc, arg) => {
|
||||
const name = arg.key.startsWith('--') ? arg.key.slice(2) : arg.key;
|
||||
acc[name] = {name, value: arg.value, section: 'Args'};
|
||||
return acc;
|
||||
}, {})
|
||||
},
|
||||
...(action.data.output && {output_dest: action.data.output}),
|
||||
...((action.data.docker.image || action.data.taskInit) && {
|
||||
container: {
|
||||
image: action.data.docker.image,
|
||||
arguments: `${action.data.docker.args}${action.data.taskInit ? ' -e CLEARML_AGENT_FORCE_TASK_INIT=1' : ''}${action.data.poetry ? ' -e CLEARML_AGENT_FORCE_POETRY' : ''}${action.data.venv ? ' -e CLEARML_AGENT_SKIP_PIP_VENV_INSTALL=' + action.data.venv : ''}${action.data.requirements === 'skip' ? '-e CLEARML_AGENT_SKIP_PYTHON_ENV_INSTALL=1' : ''}`.trimStart(),
|
||||
setup_shell_script: action.data.docker.script
|
||||
}
|
||||
}),
|
||||
}).pipe(
|
||||
map((res: TasksCreateResponse) => exActions.createExperimentSuccess({data: {...action.data, id: res.id}, project: projectId}))
|
||||
)),
|
||||
catchError(error => [addMessage(MESSAGES_SEVERITY.ERROR, `Failed to create experiment.\n${this.errService.getErrorMsg(error.error)}`)])
|
||||
);
|
||||
});
|
||||
|
||||
createExperimentSuccess = createEffect(() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(exActions.createExperimentSuccess),
|
||||
map(action => addMessage(MESSAGES_SEVERITY.SUCCESS, `Successfully created experiment ${action.data.name}`, [{name: 'open experiment', actions: [exActions.openExperiment({id: action.data.id, project: action.project})]}]))
|
||||
);
|
||||
});
|
||||
|
||||
updateExperimentsAfterCreate = createEffect(() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(exActions.createExperimentSuccess),
|
||||
map(() => exActions.refreshExperiments({autoRefresh: false, hideLoader: false}))
|
||||
);
|
||||
});
|
||||
|
||||
openExperiment = createEffect(() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(exActions.openExperiment),
|
||||
map(action => this.router.navigate(['projects', action.project, 'experiments', action.id]),)
|
||||
);
|
||||
}, {dispatch: false});
|
||||
|
||||
enqueueCreateExperiment = createEffect(() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(exActions.createExperimentSuccess),
|
||||
filter(action => !!action.data.queue),
|
||||
switchMap(action => this.apiTasks.tasksEnqueue({
|
||||
queue: action.data.queue.id,
|
||||
task: action.data.id,
|
||||
verify_watched_queue: true,
|
||||
}).pipe(
|
||||
map(res => res.queue_watched === false ? menuActions.openEmptyQueueMessage({queue: action.data.queue, entityName: action.data.name}) : {type: 'EMPTY'}),
|
||||
)),
|
||||
catchError(error => [addMessage(MESSAGES_SEVERITY.ERROR, `Failed to enqueue experiment.\n${this.errService.getErrorMsg(error.error)}`)])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export const INITIAL_EXPERIMENT_TABLE_COLS: ISmCol[] = [
|
||||
headerType: ColHeaderTypeEnum.sortFilter,
|
||||
filterable: true,
|
||||
searchableFilter: true,
|
||||
paginatedFilterPageSize: 100,
|
||||
sortable: false,
|
||||
header: 'TAGS',
|
||||
style: {width: '300px'},
|
||||
|
||||
@@ -31,20 +31,23 @@
|
||||
(compareViewChanged)="compareViewChanged($event)"
|
||||
></sm-experiment-header>
|
||||
<ng-template #addButton let-isSmallScreen="smallScreen">
|
||||
<button
|
||||
class="btn btn-cml-primary d-flex justify-content-between align-items-center me-3"
|
||||
[disabled]="isArchived$ | async"
|
||||
data-id="New Experiment"
|
||||
(click)="newExperiment()"
|
||||
[smTooltip]="isSmallScreen ? 'NEW EXPERIMENT' : ''"
|
||||
>
|
||||
<i class="al-icon al-ico-add sm"></i>
|
||||
<span *ngIf="!isSmallScreen" class="button-label">NEW EXPERIMENT</span>
|
||||
</button>
|
||||
@if (projectId$() !== '*') {
|
||||
<button
|
||||
class="btn btn-cml-primary d-flex justify-content-between align-items-center me-3"
|
||||
[disabled]="isArchived$ | async"
|
||||
data-id="New Experiment"
|
||||
(click)="newExperiment()"
|
||||
[smTooltip]="isSmallScreen ? 'NEW EXPERIMENT' : ''"
|
||||
>
|
||||
<i class="al-icon al-ico-add sm"></i>
|
||||
<span *ngIf="!isSmallScreen" class="button-label">NEW EXPERIMENT</span>
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
<div class="experiment-body"
|
||||
[class.footer-visible]="((selectedExperiments$ | async) && (selectedExperiments$ | async)?.length > 1) || (showAllSelectedIsActive$ |async)">
|
||||
<as-split #split
|
||||
[gutterSize]=1
|
||||
[useTransition]="true"
|
||||
[gutterDblClickDuration]="400"
|
||||
(gutterClick)="clickOnSplit()"
|
||||
@@ -52,10 +55,14 @@
|
||||
(dragEnd)="splitSizeChange($event)"
|
||||
(dragStart)="disableInfoPanel()"
|
||||
(transitionEnd)="experimentsTable.table?.resize(100); experimentsTable.afterTableInit()"
|
||||
[class.opened]="minimizedView && (selectSplitSize$ | async) <= 1"
|
||||
[class.closed]="minimizedView && (selectSplitSize$ | async) >= 99"
|
||||
>
|
||||
<as-split-area
|
||||
[size]="100 - (splitInitialSize)"
|
||||
[order]="1"
|
||||
[minSize]="1"
|
||||
[maxSize]="99"
|
||||
>
|
||||
<sm-experiments-table
|
||||
#experimentsTable
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
selectIsExperimentInEditMode,
|
||||
selectMetricVariantForView,
|
||||
selectMetricVariants,
|
||||
selectMetricVariantsPlots,
|
||||
selectNoMoreExperiments,
|
||||
selectSelectedExperiments,
|
||||
selectSelectedExperimentsDisableAvailable,
|
||||
@@ -40,14 +39,6 @@ import {SearchState, selectSearchQuery} from '../common-search/common-search.red
|
||||
import {ITableExperiment} from './shared/common-experiment-model.model';
|
||||
import {selectIsSharedAndNotOwner, selectMetricsLoading, selectSelectedExperiment} from '~/features/experiments/reducers';
|
||||
import * as experimentsActions from './actions/common-experiments-view.actions';
|
||||
import {
|
||||
addProjectsTag,
|
||||
getSelectedExperiments,
|
||||
setCompareView,
|
||||
setTableCols,
|
||||
tableFilterChanged,
|
||||
toggleCompareScalarSettings
|
||||
} from './actions/common-experiments-view.actions';
|
||||
import {MetricVariantResult} from '~/business-logic/model/projects/metricVariantResult';
|
||||
import {resetAceCaretsPositions, setAutoRefresh} from '../core/actions/layout.actions';
|
||||
import {setArchive as setProjectArchive, setBreadcrumbsOptions, setDeep} from '../core/actions/projects.actions';
|
||||
@@ -83,11 +74,12 @@ import {ExperimentMenuExtendedComponent} from '~/features/experiments/containers
|
||||
import {INITIAL_EXPERIMENT_TABLE_COLS} from './experiment.consts';
|
||||
import {selectIsPipelines} from '@common/experiments-compare/reducers';
|
||||
import {ExperimentMenuComponent} from '@common/experiments/shared/components/experiment-menu/experiment-menu.component';
|
||||
import {WelcomeMessageComponent} from '@common/layout/welcome-message/welcome-message.component';
|
||||
import {ConfigurationService} from '@common/shared/services/configuration.service';
|
||||
import {isReadOnly} from '@common/shared/utils/is-read-only';
|
||||
import {rootProjectsPageSize} from '@common/constants';
|
||||
import {SelectionEvent} from '@common/experiments/dumb/select-metric-for-custom-col/select-metric-for-custom-col.component';
|
||||
import {
|
||||
CreateExperimentDialogComponent
|
||||
} from '@common/experiments/containers/create-experiment-dialog/create-experiment-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-common-experiments',
|
||||
@@ -217,7 +209,7 @@ export class ExperimentsComponent extends BaseEntityPageComponent implements OnI
|
||||
|
||||
override ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.store.dispatch(setTableCols({cols: this.tableCols}));
|
||||
this.store.dispatch(experimentsActions.setTableCols({cols: this.tableCols}));
|
||||
let prevQueryParams: Params;
|
||||
this.sub.add(this.store.select(selectRouterParams).pipe(map(params => this.getParamId(params))).subscribe(() =>
|
||||
this.store.dispatch(resetAceCaretsPositions())));
|
||||
@@ -400,7 +392,7 @@ export class ExperimentsComponent extends BaseEntityPageComponent implements OnI
|
||||
tag,
|
||||
experiments: this.singleRowContext ? [contextExperiment] : this.selectedExperiments.filter(_selected => !isReadOnly(_selected))
|
||||
}));
|
||||
this.store.dispatch(addProjectsTag({tag}));
|
||||
this.store.dispatch(experimentsActions.addProjectsTag({tag}));
|
||||
}
|
||||
|
||||
setContextMenuStatus(menuStatus: boolean) {
|
||||
@@ -442,7 +434,7 @@ export class ExperimentsComponent extends BaseEntityPageComponent implements OnI
|
||||
this.entities = experiments;
|
||||
const experimentsIds = this.route.snapshot.firstChild?.params?.ids?.split(',').filter(id => !!id);
|
||||
if (mode === 'compare' && experimentsIds?.length > 0 && this.selectedExperiments.length === 0) {
|
||||
this.store.dispatch(getSelectedExperiments({ids: experimentsIds}));
|
||||
this.store.dispatch(experimentsActions.getSelectedExperiments({ids: experimentsIds}));
|
||||
}
|
||||
if (!experimentId && this.shouldOpenDetails && this.firstExperiment && mode === 'info') {
|
||||
this.shouldOpenDetails = false;
|
||||
@@ -652,7 +644,7 @@ export class ExperimentsComponent extends BaseEntityPageComponent implements OnI
|
||||
|
||||
clearTableFiltersHandler(tableFilters: { [s: string]: FilterMetadata }) {
|
||||
const filters = Object.keys(tableFilters).map(col => ({col, value: []}));
|
||||
this.store.dispatch(tableFilterChanged({filters, projectId: this.selectedProjectId}));
|
||||
this.store.dispatch(experimentsActions.tableFilterChanged({filters, projectId: this.selectedProjectId}));
|
||||
}
|
||||
|
||||
onContextMenuOpen({x, y, single, backdrop}: { x: number; y: number; single?: boolean; backdrop?: boolean }) {
|
||||
@@ -700,15 +692,11 @@ export class ExperimentsComponent extends BaseEntityPageComponent implements OnI
|
||||
}
|
||||
|
||||
newExperiment() {
|
||||
this.dialog.open(WelcomeMessageComponent, {
|
||||
width: '720px',
|
||||
height: '764px',
|
||||
data: {
|
||||
showTabs: true,
|
||||
step: 2,
|
||||
newExperimentYouTubeVideoId: ConfigurationService.globalEnvironment.newExperimentYouTubeVideoId
|
||||
}
|
||||
});
|
||||
this.dialog.open(CreateExperimentDialogComponent, {
|
||||
width: '800px',
|
||||
}).afterClosed()
|
||||
.pipe(filter(res => !!res))
|
||||
.subscribe(data => this.store.dispatch(experimentsActions.createExperiment({data})));
|
||||
}
|
||||
|
||||
downloadTableAsCSV() {
|
||||
@@ -754,11 +742,33 @@ export class ExperimentsComponent extends BaseEntityPageComponent implements OnI
|
||||
}
|
||||
|
||||
showCompareSettingsChanged() {
|
||||
this.store.dispatch(toggleCompareScalarSettings());
|
||||
this.store.dispatch(experimentsActions.toggleCompareScalarSettings());
|
||||
}
|
||||
|
||||
compareViewChanged(compareView: 'scalars' | 'plots') {
|
||||
this.store.dispatch(setCompareView({mode: compareView}));
|
||||
this.store.dispatch(experimentsActions.setCompareView({mode: compareView}));
|
||||
return this.router.navigate(['compare'], {relativeTo: this.route, queryParamsHandling: 'preserve'});
|
||||
}
|
||||
|
||||
override filterSearchChanged({colId, value}: { colId: string; value: { value: string; loadMore?: boolean }}) {
|
||||
super.filterSearchChanged({colId, value});
|
||||
if (colId === 'parent.name') {
|
||||
// No pagination in BE - setting same list will set noMoreOptions to true
|
||||
if (value.loadMore) {
|
||||
this.store.dispatch(experimentsActions.setParents({parents: [...this.parents]}));
|
||||
} else {
|
||||
this.store.dispatch(experimentsActions.resetTablesFilterParentsOptions());
|
||||
this.store.dispatch(experimentsActions.getParents({searchValue: value.value}));
|
||||
}
|
||||
} else if (colId.startsWith('hyperparams.')) {
|
||||
if (!value.loadMore) {
|
||||
this.store.dispatch(experimentsActions.hyperParamSelectedInfoExperiments({col: {id: colId}, loadMore: false, values: null}));
|
||||
this.store.dispatch(experimentsActions.setHyperParamsFiltersPage({page: 0}));
|
||||
}
|
||||
this.store.dispatch(experimentsActions.hyperParamSelectedExperiments({
|
||||
col: {id: colId, getter: `${colId}.value`},
|
||||
searchValue: value.value
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<div class="form-container">
|
||||
<mat-form-field class="w-100"
|
||||
appearance="outline"
|
||||
hideRequiredMarker="true"
|
||||
(mousedown)="!isFocused(projectInputRef) && projectInput.value && projectInput.reset(); projectInputRef.blur(); projectInputRef.focus()">
|
||||
<mat-label>Project</mat-label>
|
||||
<input matInput type="text"
|
||||
@@ -80,9 +79,8 @@
|
||||
name="forceParent"
|
||||
[checked]="forceParent$ | async"
|
||||
(change)="formData.forceParent = $event.checked"
|
||||
>Set <b *ngIf="reference" [smTooltip]="reference.length > 80 ? reference : undefined">{{reference.length > 80 ? (reference | slice:0:77) + '...' : reference }}</b> <span style="white-space: nowrap">as parent</span> </mat-checkbox>
|
||||
<mat-form-field class="w-100" appearance="outline"
|
||||
hideRequiredMarker="true">
|
||||
>Set<b *ngIf="reference" [smTooltip]="reference.length > 80 ? reference : undefined"> {{reference.length > 80 ? (reference | slice:0:77) + '...' : reference }} </b><span style="white-space: nowrap">as parent</span> </mat-checkbox>
|
||||
<mat-form-field class="w-100" appearance="outline">
|
||||
<mat-label>Description</mat-label>
|
||||
<textarea
|
||||
class="clone-description"
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
#tagMenu
|
||||
class="light-theme"
|
||||
[tags]="experiment?.tags"
|
||||
[tagsFilterByProject]="!allProjects && tagsFilterByProject"
|
||||
[tagsFilterByProject]="!allProjects() && tagsFilterByProject"
|
||||
[projectTags]="projectTags"
|
||||
[companyTags]="companyTags"
|
||||
(tagSelected)="tagSelected.emit($event)">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {filter, take} from 'rxjs/operators';
|
||||
import {ICONS} from '@common/constants';
|
||||
import {Queue} from '~/business-logic/model/queues/queue';
|
||||
@@ -25,7 +24,10 @@ import * as experimentsActions from '../../../actions/common-experiments-view.ac
|
||||
import {ShareDialogComponent} from '@common/shared/ui-components/overlay/share-dialog/share-dialog.component';
|
||||
import {ConfigurationService} from '@common/shared/services/configuration.service';
|
||||
import {selectNeverShowPopups} from '@common/core/reducers/view.reducer';
|
||||
import {CommonDeleteDialogComponent} from '@common/shared/entity-page/entity-delete/common-delete-dialog.component';
|
||||
import {
|
||||
CommonDeleteDialogComponent,
|
||||
DeleteData
|
||||
} from '@common/shared/entity-page/entity-delete/common-delete-dialog.component';
|
||||
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
|
||||
import {
|
||||
autoRefreshExperimentInfo,
|
||||
@@ -49,7 +51,8 @@ import {resetDebugImages} from '@common/debug-images/debug-images-actions';
|
||||
import {
|
||||
IOption
|
||||
} from '@common/shared/ui-components/inputs/select-autocomplete-with-chips/select-autocomplete-with-chips.component';
|
||||
import {setContextMenu} from '@common/core/actions/router.actions';
|
||||
import {headerActions} from '@common/core/actions/router.actions';
|
||||
import {ConfirmDialogConfig} from '@common/shared/ui-components/overlay/confirm-dialog/confirm-dialog.model';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -101,6 +104,7 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
get selectedExperiments(): IExperimentInfo[] {
|
||||
return this._selectedExperiments;
|
||||
}
|
||||
|
||||
@Output() tagSelected = new EventEmitter<string>();
|
||||
|
||||
protected blTaskService: BlTasksService;
|
||||
@@ -112,11 +116,11 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.blTaskService = inject(BlTasksService);
|
||||
this.dialog = inject(MatDialog);
|
||||
this.router = inject(Router);
|
||||
this.configService = inject(ConfigurationService);
|
||||
this.route = inject(ActivatedRoute);
|
||||
this.blTaskService = inject(BlTasksService);
|
||||
this.dialog = inject(MatDialog);
|
||||
this.router = inject(Router);
|
||||
this.configService = inject(ConfigurationService);
|
||||
this.route = inject(ActivatedRoute);
|
||||
}
|
||||
|
||||
|
||||
@@ -129,17 +133,31 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
const selectedExperiments = this.selectedExperiments ? selectionDisabledArchive(this.selectedExperiments).selectedFiltered : [this._experiment];
|
||||
|
||||
if (selectedExperiments[0].system_tags?.includes('archived')) {
|
||||
this.store.dispatch(commonMenuActions.restoreSelectedExperiments({selectedEntities: selectedExperiments, entityType}));
|
||||
this.store.dispatch(commonMenuActions.restoreSelectedExperiments({
|
||||
selectedEntities: selectedExperiments,
|
||||
entityType
|
||||
}));
|
||||
} else {
|
||||
this.store.select(selectNeverShowPopups)
|
||||
.pipe(take(1))
|
||||
.subscribe(neverShow => {
|
||||
const showShareWarningDialog = selectedExperiments.find(item => item?.system_tags.includes('shared')) &&
|
||||
!neverShow?.includes('archive-shared-task');
|
||||
const showRunningWarningDialog = selectedExperiments.some(experiment =>
|
||||
[TaskStatusEnum.Queued, TaskStatusEnum.InProgress].includes(experiment.status)
|
||||
);
|
||||
if (showShareWarningDialog) {
|
||||
this.showConfirmArchiveExperiments(this.store, this.dialog, selectedExperiments, entityType);
|
||||
this.showConfirmArchiveExperiments(selectedExperiments, entityType);
|
||||
}
|
||||
else if (showRunningWarningDialog) {
|
||||
this.showConfirmArchiveExperiments(selectedExperiments, entityType, 'ARCHIVE A RUNNING TASK',
|
||||
'Some of the experiments you are about to archive are running or queued.<br>Archiving running experiments will also <b>RESET</b> them.<br>Archive experiments?',
|
||||
false);
|
||||
} else {
|
||||
this.store.dispatch(commonMenuActions.archiveSelectedExperiments({selectedEntities: selectedExperiments, entityType}));
|
||||
this.store.dispatch(commonMenuActions.archiveSelectedExperiments({
|
||||
selectedEntities: selectedExperiments,
|
||||
entityType
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -147,22 +165,22 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
|
||||
toggleFullScreen(showFullScreen: boolean) {
|
||||
if (showFullScreen) {
|
||||
this.store.dispatch(setContextMenu({contextMenu: null}));
|
||||
this.router.navigateByUrl(`projects/${this.projectId}/experiments/${this._experiment.id}/output/execution`);
|
||||
this.store.dispatch(headerActions.setTabs({contextMenu: null}));
|
||||
this.router.navigateByUrl(`projects/${this.projectId()}/experiments/${this._experiment.id}/output/execution`);
|
||||
} else {
|
||||
const part = this.route.firstChild.routeConfig.path;
|
||||
if (['log', 'metrics/scalar', 'metrics/plots', 'debugImages'].includes(part)) {
|
||||
this.router.navigateByUrl(`projects/${this.projectId}/experiments/${this._experiment.id}/info-output/${part}`);
|
||||
this.router.navigateByUrl(`projects/${this.projectId()}/experiments/${this._experiment.id}/info-output/${part}`);
|
||||
} else {
|
||||
this.router.navigateByUrl(`projects/${this.projectId}/experiments/${this._experiment.id}/${part}`);
|
||||
this.router.navigateByUrl(`projects/${this.projectId()}/experiments/${this._experiment.id}/${part}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enqueuePopup() {
|
||||
const selectedExperiments = !(this.activateFromMenuButton || this.useCurrentEntity) ?
|
||||
selectionDisabledEnqueue(this.selectedExperiments).selectedFiltered :
|
||||
[this._experiment];
|
||||
selectionDisabledEnqueue(this.selectedExperiments).selectedFiltered :
|
||||
[this._experiment];
|
||||
|
||||
const selectQueueDialog: MatDialogRef<SelectQueueComponent, { confirmed: boolean; queue: Queue }> =
|
||||
this.dialog.open(SelectQueueComponent, {
|
||||
@@ -208,7 +226,11 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
}
|
||||
|
||||
private enqueueExperiment(queue, selectedExperiments) {
|
||||
this.store.dispatch(commonMenuActions.enqueueClicked({selectedEntities: selectedExperiments, queue, verifyWatchers: true}));
|
||||
this.store.dispatch(commonMenuActions.enqueueClicked({
|
||||
selectedEntities: selectedExperiments,
|
||||
queue,
|
||||
verifyWatchers: true
|
||||
}));
|
||||
}
|
||||
|
||||
private dequeueExperiment(selectedExperiments) {
|
||||
@@ -222,7 +244,7 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
public resetPopup() {
|
||||
const selectedExperiments = (!(this.activateFromMenuButton || this.useCurrentEntity) && this.selectedExperiments) ? selectionDisabledReset(this.selectedExperiments).selectedFiltered : [this._experiment];
|
||||
const devWarning: boolean = selectedExperiments.some(exp => isDevelopment(exp));
|
||||
const confirmDialogRef = this.dialog.open(CommonDeleteDialogComponent, {
|
||||
const confirmDialogRef = this.dialog.open<CommonDeleteDialogComponent, DeleteData, boolean>(CommonDeleteDialogComponent, {
|
||||
data: {
|
||||
entity: selectedExperiments?.length > 0 ? selectedExperiments?.length === 1 ? selectedExperiments[0] : selectedExperiments : this._experiment,
|
||||
numSelected: selectedExperiments?.length ?? this.numSelected,
|
||||
@@ -317,7 +339,7 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
const currentProjects = Array.from(new Set(selectedExperiments.map(exp => exp.project?.id).filter(p => p)));
|
||||
const dialog = this.dialog.open(ChangeProjectDialogComponent, {
|
||||
data: {
|
||||
currentProjects: currentProjects.length > 0 ? currentProjects : [this.projectId],
|
||||
currentProjects: currentProjects.length > 0 ? currentProjects : [this.projectId()],
|
||||
defaultProject: this._experiment?.project,
|
||||
reference: selectedExperiments.length > 1 ? selectedExperiments : selectedExperiments[0]?.name,
|
||||
type: 'experiment'
|
||||
@@ -353,7 +375,7 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
}).afterClosed().pipe(
|
||||
take(1),
|
||||
filter(res => !!res),
|
||||
).subscribe(res => {
|
||||
).subscribe(res => {
|
||||
this.cloneExperiment(res);
|
||||
});
|
||||
}
|
||||
@@ -372,7 +394,7 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
|
||||
deleteExperimentPopup(entityType?: EntityTypeEnum, includeChildren?: boolean) {
|
||||
const selectedExperiments = (!(this.activateFromMenuButton || this.useCurrentEntity) && this.selectedExperiments) ? selectionDisabledDelete(this.selectedExperiments).selectedFiltered : [this._experiment];
|
||||
const confirmDialogRef = this.dialog.open(CommonDeleteDialogComponent, {
|
||||
const confirmDialogRef = this.dialog.open<CommonDeleteDialogComponent, DeleteData, boolean>(CommonDeleteDialogComponent, {
|
||||
data: {
|
||||
entity: selectedExperiments?.length > 0 ? selectedExperiments?.length === 1 ? selectedExperiments[0] : selectedExperiments : this._experiment,
|
||||
numSelected: selectedExperiments?.length ?? this.numSelected,
|
||||
@@ -391,33 +413,37 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
this.store.dispatch(deactivateEdit());
|
||||
|
||||
if (this.activateFromMenuButton || this.selectedExperiments.map(e => e.id).includes(this.selectedExperiment?.id)) {
|
||||
const entityBaseRoute= { [EntityTypeEnum.experiment]: 'projects',[EntityTypeEnum.dataset]: 'datasets/simple', [EntityTypeEnum.controller]:'pipelines' };
|
||||
window.setTimeout(() => this.router.navigate([`${entityBaseRoute[entityType] || 'projects'}/${this.projectId}/experiments`], {queryParamsHandling: 'preserve'}));
|
||||
const entityBaseRoute = {
|
||||
[EntityTypeEnum.experiment]: 'projects',
|
||||
[EntityTypeEnum.dataset]: 'datasets/simple',
|
||||
[EntityTypeEnum.controller]: 'pipelines'
|
||||
};
|
||||
window.setTimeout(() => this.router.navigate([`${entityBaseRoute[entityType] || 'projects'}/${this.projectId()}/experiments`], {queryParamsHandling: 'preserve'}));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showConfirmArchiveExperiments(store: Store, dialog: MatDialog, selectedExperiments: ISelectedExperiment[], entityType: EntityTypeEnum): void {
|
||||
const confirmDialogRef = dialog.open(ConfirmDialogComponent, {
|
||||
showConfirmArchiveExperiments(selectedExperiments: ISelectedExperiment[], entityType: EntityTypeEnum, title?: string, body?: string, showNeverShowAgain = true): void {
|
||||
this.dialog.open<ConfirmDialogComponent, ConfirmDialogConfig, {isConfirmed: boolean, neverShowAgain: boolean}>(ConfirmDialogComponent, {
|
||||
data: {
|
||||
title: 'ARCHIVE A PUBLICLY SHARED TASK',
|
||||
body: `This task is accessible through a public access link.
|
||||
title: title ?? 'ARCHIVE A PUBLICLY SHARED TASK',
|
||||
body: body ?? `This task is accessible through a public access link.
|
||||
Archiving will disable public access`,
|
||||
yes: 'OK',
|
||||
no: 'Cancel',
|
||||
iconClass: 'al-icon al-ico-archive al-color',
|
||||
showNeverShowAgain: true
|
||||
showNeverShowAgain
|
||||
}
|
||||
});
|
||||
confirmDialogRef.afterClosed().subscribe((confirmed) => {
|
||||
if (confirmed) {
|
||||
store.dispatch(archiveSelectedExperiments({selectedEntities: selectedExperiments, entityType}));
|
||||
if (confirmed.neverShowAgain) {
|
||||
store.dispatch(neverShowPopupAgain({popupId: 'archive-shared-task'}));
|
||||
}).afterClosed()
|
||||
.subscribe((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.store.dispatch(archiveSelectedExperiments({selectedEntities: selectedExperiments, entityType}));
|
||||
if (confirmed.neverShowAgain) {
|
||||
this.store.dispatch(neverShowPopupAgain({popupId: title ?? 'archive-shared-task'}));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stopAllChildrenPopup() {
|
||||
@@ -426,10 +452,10 @@ export class ExperimentMenuComponent extends BaseContextMenuComponent implements
|
||||
}
|
||||
|
||||
toggleDetails() {
|
||||
this.store.dispatch(experimentsActions.setTableMode({mode:'info'}));
|
||||
this.store.dispatch(experimentsActions.setTableMode({mode: 'info'}));
|
||||
this.store.dispatch(experimentsActions.experimentSelectionChanged({
|
||||
experiment: this._experiment,
|
||||
project: this.projectId
|
||||
project: this.projectId()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<div class="overflow-container" #container>
|
||||
@if (isCommunity && activeWorkspace && !workspaceNeutral) {
|
||||
@if (isCommunity() && activeWorkspace && !workspaceNeutral()) {
|
||||
<span class="workspace">{{activeWorkspace.name}}</span>
|
||||
<i class="al-icon al-ico-slash"></i>
|
||||
}
|
||||
@for (breadcrumbGroup of breadcrumbs ; track breadcrumbGroup; let lastGroup = $last; let
|
||||
@for (breadcrumbGroup of breadcrumbs() ; track breadcrumbGroup; let lastGroup = $last; let
|
||||
firstGroup = $first) {
|
||||
@if (!(shouldCollapse && breadcrumbGroup?.length > 0 && breadcrumbGroup?.[0]?.collapsable)) {
|
||||
@for (breadcrumb of breadcrumbGroup ; track breadcrumb; let lastCrumb = $last; let firstCrumb = $first) {
|
||||
@@ -55,8 +55,9 @@
|
||||
[routerLink]="breadcrumb.url!=='projects/*/projects'? breadcrumb.url: 'projects/*'"
|
||||
>
|
||||
@if (breadcrumb.hidden) {
|
||||
<i class="al-icon al-ico-ghost sm me-1"></i>
|
||||
}{{breadcrumb.name}}
|
||||
<i matMenuItemIcon class="al-icon al-ico-ghost sm me-1"></i>
|
||||
}
|
||||
{{breadcrumb.name}}
|
||||
</span>
|
||||
}
|
||||
</mat-menu>
|
||||
@@ -68,7 +69,7 @@
|
||||
[matMenuTriggerFor]="shareModal"
|
||||
(menuOpened)="openShareModal()"
|
||||
>
|
||||
@if (showShareButton && !isCommunity) {
|
||||
@if (showShareButton && !isCommunity()) {
|
||||
<i class="fa fa-share-alt share pointer" smTooltip="Share"></i>
|
||||
}
|
||||
</div>
|
||||
@@ -95,7 +96,7 @@
|
||||
</div>
|
||||
</mat-menu>
|
||||
</div>
|
||||
@if (archive) {
|
||||
@if (archive()) {
|
||||
<div data-id="Archive Label" class="archive"><i class="al-icon xs al-ico-archive me-1"></i>Archive
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
ElementRef,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit, QueryList,
|
||||
ViewChild, ViewChildren
|
||||
viewChild,
|
||||
viewChildren
|
||||
} from '@angular/core';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {selectRouterConfig, selectRouterQueryParams} from '../../core/reducers/router-reducer';
|
||||
import {debounceTime} from 'rxjs/operators';
|
||||
import {ActivatedRoute, RouterLink} from '@angular/router';
|
||||
import {combineLatest, fromEvent, Observable, Subscription} from 'rxjs';
|
||||
import {fromEvent} from 'rxjs';
|
||||
import {addMessage} from '../../core/actions/layout.actions';
|
||||
import {ConfigurationService} from '../../shared/services/configuration.service';
|
||||
import {
|
||||
@@ -20,8 +21,7 @@ import {
|
||||
} from '~/business-logic/model/users/getCurrentUserResponseUserObjectCompany';
|
||||
import {selectShowHiddenUserSelection} from '../../core/reducers/projects.reducer';
|
||||
import {MESSAGES_SEVERITY} from '@common/constants';
|
||||
import {setBreadcrumbs} from '@common/core/actions/router.actions';
|
||||
import {selectBreadcrumbs} from '@common/core/reducers/view.reducer';
|
||||
import {selectBreadcrumbs, selectWorkspaceNeutral} from '@common/core/reducers/view.reducer';
|
||||
import {MatMenuModule} from '@angular/material/menu';
|
||||
import {TooltipDirective} from '@common/shared/ui-components/indicators/tooltip/tooltip.directive';
|
||||
|
||||
@@ -34,6 +34,8 @@ import {BreadcrumbsService} from '@common/shared/services/breadcrumbs.service';
|
||||
import {TagListComponent} from '@common/shared/ui-components/tags/tag-list/tag-list.component';
|
||||
import {cloneItemIntoDummy} from '@common/shared/utils/shared-utils';
|
||||
import {IdBadgeComponent} from '@common/shared/components/id-badge/id-badge.component';
|
||||
import {toSignal} from '@angular/core/rxjs-interop';
|
||||
import {selectArchive, selectProjectsFeature} from '@common/layout/layout.selectors';
|
||||
|
||||
export enum CrumbTypeEnum {
|
||||
Workspace = 'Workspace',
|
||||
@@ -87,124 +89,56 @@ export interface IBreadcrumbsOptions {
|
||||
],
|
||||
standalone: true
|
||||
})
|
||||
export class BreadcrumbsComponent implements OnInit, OnDestroy {
|
||||
public breadcrumbs: IBreadcrumbsLink[][] = [];
|
||||
export class BreadcrumbsComponent {
|
||||
private store = inject(Store);
|
||||
public route = inject(ActivatedRoute);
|
||||
private configService = inject(ConfigurationService);
|
||||
private cd = inject(ChangeDetectorRef);
|
||||
private breadcrumbsService = inject(BreadcrumbsService); // don't delete
|
||||
public currentUrl: string;
|
||||
public showShareButton: boolean = false;
|
||||
public isCommunity: boolean;
|
||||
public archive: boolean;
|
||||
public workspaceNeutral: boolean;
|
||||
// public isDeep: boolean;
|
||||
public subProjectsMenuIsOpen: boolean;
|
||||
public shouldCollapse: boolean;
|
||||
private sub = new Subscription();
|
||||
// private isSearching$ = this.store.select(selectIsSearching);
|
||||
|
||||
@Input() activeWorkspace: GetCurrentUserResponseUserObjectCompany;
|
||||
@ViewChild('container') private breadCrumbsContainer: ElementRef<HTMLDivElement>;
|
||||
@ViewChildren('crumb') private crumbElements: QueryList<ElementRef<HTMLDivElement>>;
|
||||
public breadcrumbs$: Observable<IBreadcrumbsLink[][]>;
|
||||
public showHidden: boolean;
|
||||
public projectFeature: boolean;
|
||||
protected environment = toSignal(this.configService.getEnvironment());
|
||||
private resize = toSignal(fromEvent(window, 'resize').pipe(debounceTime(100)));
|
||||
protected projectFeature = this.store.selectSignal(selectProjectsFeature);
|
||||
protected showHidden = this.store.selectSignal(selectShowHiddenUserSelection);
|
||||
protected workspaceNeutral = this.store.selectSignal(selectWorkspaceNeutral);
|
||||
protected archive = this.store.selectSignal<boolean>(selectArchive);
|
||||
private breadCrumbsContainer = viewChild<ElementRef<HTMLDivElement>>('container');
|
||||
private crumbElements= viewChildren<ElementRef<HTMLDivElement>>('crumb');
|
||||
protected breadcrumbLinks = this.store.selectSignal<IBreadcrumbsLink[][]>(selectBreadcrumbs);
|
||||
protected breadcrumbs = computed(() => this.breadcrumbLinks()
|
||||
.map(breadcrumbsGroup => breadcrumbsGroup?.filter( breadcrumb =>
|
||||
(!breadcrumb.hidden) || (this.showHidden() && this.projectFeature())
|
||||
))
|
||||
.filter(breadcrumbsGroup => breadcrumbsGroup?.length > 0) ?? []
|
||||
);
|
||||
protected isCommunity = computed(() => this.environment().communityServer);
|
||||
protected shouldCollapse: boolean;
|
||||
|
||||
|
||||
|
||||
constructor(
|
||||
private store: Store,
|
||||
public route: ActivatedRoute,
|
||||
private configService: ConfigurationService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private breadcrumbsService: BreadcrumbsService // don't delete
|
||||
) {
|
||||
this.breadcrumbs$ = this.store.select(selectBreadcrumbs);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.sub.add(fromEvent(window, 'resize')
|
||||
.pipe(debounceTime(100))
|
||||
.subscribe(() => {
|
||||
this.calcOverflowing();
|
||||
})
|
||||
);
|
||||
|
||||
this.sub.add(this.breadcrumbs$.subscribe(breadcrumbs => {
|
||||
this.breadcrumbs = breadcrumbs
|
||||
.map(breadcrumbsGroup => breadcrumbsGroup?.filter( breadcrumb =>
|
||||
(!breadcrumb.hidden) || (this.showHidden && this.projectFeature)
|
||||
))
|
||||
.filter(breadcrumbsGroup => breadcrumbsGroup?.length > 0);
|
||||
this.calcOverflowing();
|
||||
// this.cd.detectChanges();
|
||||
}));
|
||||
|
||||
this.sub.add(this.breadcrumbs$.pipe(debounceTime(100)).subscribe(() => {
|
||||
this.calcOverflowing();
|
||||
}));
|
||||
|
||||
// this.sub.add(this.isSearching$.pipe(debounceTime(100)).subscribe(() => {
|
||||
// if (!this.shouldCollapse) {
|
||||
// this.calcOverflowing();
|
||||
// }
|
||||
// })
|
||||
// );
|
||||
|
||||
this.sub.add(this.configService.globalEnvironmentObservable.subscribe(env => this.isCommunity = env.communityServer));
|
||||
|
||||
// // todo: check if needed
|
||||
// this.sub.add(this.store.select(selectIsDeepMode).subscribe(isDeep => {
|
||||
// this.isDeep = isDeep;
|
||||
// this.cd.detectChanges();
|
||||
// })
|
||||
// );
|
||||
this.sub.add(this.store.select(selectRouterConfig).subscribe(config => {
|
||||
let route = this.route.snapshot;
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
}
|
||||
!config?.includes(':projectId') && route?.data?.staticBreadcrumb && this.store.dispatch(setBreadcrumbs({
|
||||
breadcrumbs: route?.data?.staticBreadcrumb
|
||||
}));
|
||||
}));
|
||||
|
||||
|
||||
this.sub.add(combineLatest([
|
||||
this.store.select(selectRouterConfig),
|
||||
this.store.select(selectRouterQueryParams),
|
||||
this.store.select(selectShowHiddenUserSelection),
|
||||
]).pipe(
|
||||
debounceTime(200),
|
||||
).subscribe(([config, params, showHidden]) => {
|
||||
this.showHidden = showHidden;
|
||||
this.projectFeature = config?.[0] === 'projects';
|
||||
this.archive = params?.archive==='true';
|
||||
let route = this.route.snapshot;
|
||||
let hide = false;
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
if (route.data.workspaceNeutral !== undefined) {
|
||||
hide = route.data.workspaceNeutral;
|
||||
}
|
||||
}
|
||||
this.workspaceNeutral = hide;
|
||||
effect(() => {
|
||||
this.resize();
|
||||
if (this.breadCrumbsContainer()) {
|
||||
this.shouldCollapse = false;
|
||||
this.cd.detectChanges();
|
||||
})
|
||||
);
|
||||
}
|
||||
const lastCrumb = this.crumbElements().at(-1).nativeElement;
|
||||
const dummyContainer = document.createElement('span');
|
||||
dummyContainer.style.position = 'fixed';
|
||||
this.breadCrumbsContainer().nativeElement.appendChild(dummyContainer);
|
||||
cloneItemIntoDummy(lastCrumb, dummyContainer);
|
||||
const width = dummyContainer.offsetWidth;
|
||||
this.breadCrumbsContainer().nativeElement.removeChild(dummyContainer);
|
||||
this.shouldCollapse = lastCrumb.clientWidth < width;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
private calcOverflowing() {
|
||||
this.shouldCollapse = false;
|
||||
this.cd.detectChanges();
|
||||
const lastCrumb = this.crumbElements.last.nativeElement;
|
||||
const dummyContainer = document.createElement('span');
|
||||
dummyContainer.style.position = 'fixed';
|
||||
this.breadCrumbsContainer.nativeElement.appendChild(dummyContainer);
|
||||
cloneItemIntoDummy(lastCrumb, dummyContainer);
|
||||
const width = dummyContainer.offsetWidth;
|
||||
this.breadCrumbsContainer.nativeElement.removeChild(dummyContainer);
|
||||
this.shouldCollapse = lastCrumb.clientWidth < width;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
|
||||
openShareModal() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="navbar-header-container">
|
||||
<ng-container *ngFor="let route of contextNavbar$ | async; trackBy:trackByFn">
|
||||
@for (route of contextNavbar(); track route.header) {
|
||||
<sm-navbar-item
|
||||
*smCheckPermission="route.permissionCheck"
|
||||
direction="bottom"
|
||||
@@ -7,8 +7,8 @@
|
||||
[header]="route.header"
|
||||
[active]="route.isActive"
|
||||
[routerLink] = "route.link"
|
||||
[subHeader]="(archivedMode$ | async) && route.subHeader"
|
||||
[subHeader]="archivedMode() && route.subHeader"
|
||||
(click)="setFeature(route.featureName ?? route.header)"
|
||||
></sm-navbar-item>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {Observable} from 'rxjs';
|
||||
import {selectIsArchivedMode} from '@common/core/reducers/projects.reducer';
|
||||
import {selectContextMenu} from '@common/core/reducers/view.reducer';
|
||||
import {setContextMenuActiveFeature} from '@common/core/actions/router.actions';
|
||||
import {headerActions} from '@common/core/actions/router.actions';
|
||||
import {HeaderNavbarTabConfig} from '@common/layout/header-navbar-tabs/header-navbar-tabs-config.types';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'sm-header-navbar-tabs',
|
||||
templateUrl: './header-navbar-tabs.component.html',
|
||||
styleUrls: ['./header-navbar-tabs.component.scss']
|
||||
styleUrls: ['./header-navbar-tabs.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HeaderNavbarTabsComponent {
|
||||
private store = inject(Store);
|
||||
public routes: HeaderNavbarTabConfig[];
|
||||
|
||||
public contextNavbar$: Observable<HeaderNavbarTabConfig[]>;
|
||||
public archivedMode$: Observable<boolean>;
|
||||
|
||||
|
||||
constructor(private store: Store) {
|
||||
this.contextNavbar$ = this.store.select(selectContextMenu);
|
||||
this.archivedMode$ = this.store.select(selectIsArchivedMode);
|
||||
}
|
||||
|
||||
trackByFn(index, route: HeaderNavbarTabConfig) {
|
||||
return route.header;
|
||||
}
|
||||
protected contextNavbar = this.store.selectSignal(selectContextMenu);
|
||||
protected archivedMode = this.store.selectSignal(selectIsArchivedMode);
|
||||
|
||||
setFeature(featureName: string) {
|
||||
this.store.dispatch(setContextMenuActiveFeature({activeFeature: featureName}))
|
||||
this.store.dispatch(headerActions.setActiveTab({activeFeature: featureName}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,86 +2,101 @@
|
||||
<div class="d-flex">
|
||||
<sm-breadcrumbs
|
||||
class="spacer"
|
||||
[class.flex-grow-1]="!(userFocus)"
|
||||
[class.user-focus]="userFocus "
|
||||
[class.share-view]="isShareMode"
|
||||
[activeWorkspace]="activeWorkspace">
|
||||
[class.flex-grow-1]="!userFocus()"
|
||||
[class.user-focus]="userFocus()"
|
||||
[class.share-view]="isShareMode()"
|
||||
[activeWorkspace]="activeWorkspace()">
|
||||
</sm-breadcrumbs>
|
||||
<sm-show-only-user-work *ngIf="userFocus" class="ms-3"></sm-show-only-user-work>
|
||||
@if (userFocus()) {
|
||||
<sm-show-only-user-work class="ms-3"></sm-show-only-user-work>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div *ngIf="showLogo && !isLogin" class="logo-full middle"
|
||||
[class.make-room-for-slogan]="(environment$ | async).whiteLabelSlogan">
|
||||
<img *ngIf="!(environment$ | async).whiteLabelLogo; else: whiteLabel" alt="logo"
|
||||
[priority]="true"
|
||||
[ngSrc]="(environment$ | async).branding.logo"
|
||||
width="130" height="42">
|
||||
<ng-template #whiteLabel>
|
||||
<div class="slogan">{{(environment$ | async).whiteLabelSlogan}}</div>
|
||||
<div *ngIf="isLogin" class="logo-full">
|
||||
<img alt="logo" [priority]="true" ngSrc="assets/logo-white.svg" width="130" height="42">
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<sm-header-navbar-tabs
|
||||
*ngIf="!showLogo && !isLogin"
|
||||
></sm-header-navbar-tabs>
|
||||
@if (showLogo() && !isLogin()) {
|
||||
<div class="logo-full middle"
|
||||
[class.make-room-for-slogan]="environment().whiteLabelSlogan">
|
||||
@if (!environment().whiteLabelLogo) {
|
||||
<img alt="logo"
|
||||
[priority]="true"
|
||||
[ngSrc]="environment().branding.logo"
|
||||
width="130" height="42">
|
||||
} @else {
|
||||
<div class="slogan">{{environment().whiteLabelSlogan}}</div>
|
||||
@if (isLogin()) {
|
||||
<div class="logo-full">
|
||||
<img alt="logo" [priority]="true" ngSrc="assets/logo-white.svg" width="130" height="42">
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!showLogo() && !isLogin()) {
|
||||
<sm-header-navbar-tabs
|
||||
></sm-header-navbar-tabs>
|
||||
}
|
||||
|
||||
<div *ngIf="isLogin" class="spacer"></div>
|
||||
@if (isLogin()) {
|
||||
<div class="spacer"></div>
|
||||
}
|
||||
|
||||
<div class="right-buttons" data-id="rightSideHeaderpanel" *ngIf="!hideMenus">
|
||||
<sm-common-search #search [class.share-view]="isShareMode"></sm-common-search>
|
||||
|
||||
<span class="d-flex pointer resources-trigger" [matMenuTriggerFor]="resourcesMenu">
|
||||
<i class="al-icon al-ico-help-outlined" data-id="help Icon"></i>
|
||||
</span>
|
||||
<span class="pointer menu-trigger position-relative" data-id="Avatar" [matMenuTriggerFor]="profileMenu">
|
||||
<img alt="avatar" class="avatar" *ngIf="(user | async).avatar; else iconAvatar" [src]="(user | async).avatar">
|
||||
<ng-template #iconAvatar>
|
||||
<div class="user-icon">
|
||||
<i class="al-icon al-ico-account sm-md"></i>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div *ngIf="(userNotificationPath$ | async) || (invitesPending$ | async)?.length" class="user-notification"></div>
|
||||
</span>
|
||||
|
||||
<mat-menu #profileMenu="matMenu" class="user-menu">
|
||||
<button mat-menu-item [routerLink]="'settings/' + (userNotificationPath$ | async)" data-id="Settings Button">
|
||||
<i class="al-icon icon sm-md" [class]="(userNotificationPath$ | async) ? 'al-ico-settings-alert' : 'al-ico-settings'">
|
||||
<span class="path1"></span><span class="path2"></span>
|
||||
</i>Settings
|
||||
</button>
|
||||
<sm-header-user-menu-actions></sm-header-user-menu-actions>
|
||||
<button mat-menu-item (click)="logout()" data-id="Logout">
|
||||
<i class="al-ico-logout al-icon icon sm-md"></i>Logout
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #resourcesMenu="matMenu" class="user-menu light-theme" panelClass="light-theme">
|
||||
<button mat-menu-item (click)="openWelcome($event)" data-id="Python Package setup Option">
|
||||
<i class="al-icon sm-md al-ico-code-file"></i>ClearML Python Package setup
|
||||
</button>
|
||||
<a mat-menu-item href="https://www.youtube.com/c/ClearML/featured" target="_blank" data-id="Youtube Option">
|
||||
<i class="al-icon al-ico-youtube sm-md"></i>ClearML on Youtube
|
||||
</a>
|
||||
<a mat-menu-item [href]="(environment$ | async).docsLink" target="_blank" data-id="Online Documentation Option">
|
||||
<i class="al-icon sm-md al-ico-documentation"></i>Online Documentation
|
||||
</a>
|
||||
<button mat-menu-item (click)="openTip()" data-id="Pro Tips Option">
|
||||
<i class="al-icon sm-md al-ico-tips"></i>Pro Tips
|
||||
</button>
|
||||
<ng-container *smCheckPermission="'applications'">
|
||||
<button mat-menu-item
|
||||
*ngIf="(environment$ | async)?.appsYouTubeIntroVideoId && $any((environment$ | async)).appAwarenessMenu !== false"
|
||||
(click)="openAppsAwareness($event)" data-id="Apps Introduction Option"
|
||||
>
|
||||
<i class="al-icon sm-md al-ico-applications"></i>ClearML Apps Introduction
|
||||
@if (!hideMenus()) {
|
||||
<div class="right-buttons" data-id="rightSideHeaderpanel">
|
||||
<sm-common-search #search [class.share-view]="isShareMode()"></sm-common-search>
|
||||
<span class="d-flex pointer resources-trigger" [matMenuTriggerFor]="resourcesMenu">
|
||||
<i class="al-icon al-ico-help-outlined" data-id="help Icon"></i>
|
||||
</span>
|
||||
<span class="pointer menu-trigger position-relative" data-id="Avatar" [matMenuTriggerFor]="profileMenu">
|
||||
@if (user().avatar; as avatar) {
|
||||
<img alt="avatar" class="avatar" [src]="avatar">
|
||||
} @else {
|
||||
<div class="user-icon">
|
||||
<i class="al-icon al-ico-account sm-md"></i>
|
||||
</div>
|
||||
}
|
||||
@if (userNotificationPath() || invitesPending()?.length) {
|
||||
<div class="user-notification"></div>
|
||||
}
|
||||
</span>
|
||||
<mat-menu #profileMenu="matMenu" class="user-menu">
|
||||
<button mat-menu-item [routerLink]="'settings/' + userNotificationPath()" data-id="Settings Button">
|
||||
<i class="al-icon icon sm-md" [class]="userNotificationPath() ? 'al-ico-settings-alert' : 'al-ico-settings'">
|
||||
<span class="path1"></span><span class="path2"></span>
|
||||
</i>Settings
|
||||
</button>
|
||||
</ng-container>
|
||||
<a mat-menu-item href="mailto:support@clear.ml" data-id="Contact Us">
|
||||
<i class="al-icon sm-md al-ico-email"></i>Contact Us
|
||||
</a>
|
||||
</mat-menu>
|
||||
</div>
|
||||
<sm-header-user-menu-actions></sm-header-user-menu-actions>
|
||||
<button mat-menu-item (click)="logout()" data-id="Logout">
|
||||
<i class="al-ico-logout al-icon icon sm-md"></i>Logout
|
||||
</button>
|
||||
</mat-menu>
|
||||
<mat-menu #resourcesMenu="matMenu" class="user-menu light-theme" panelClass="light-theme">
|
||||
<button mat-menu-item (click)="openWelcome($event)" data-id="Python Package setup Option">
|
||||
<i class="al-icon sm-md al-ico-code-file"></i>ClearML Python Package setup
|
||||
</button>
|
||||
<a mat-menu-item href="https://www.youtube.com/c/ClearML/featured" target="_blank" data-id="Youtube Option">
|
||||
<i class="al-icon al-ico-youtube sm-md"></i>ClearML on Youtube
|
||||
</a>
|
||||
<a mat-menu-item [href]="environment().docsLink" target="_blank" data-id="Online Documentation Option">
|
||||
<i class="al-icon sm-md al-ico-documentation"></i>Online Documentation
|
||||
</a>
|
||||
@if (tipsService.hasTips()) {
|
||||
<button mat-menu-item (click)="openTip()" data-id="Pro Tips Option">
|
||||
<i class="al-icon sm-md al-ico-tips"></i>Pro Tips
|
||||
</button>
|
||||
}
|
||||
<ng-container *smCheckPermission="'applications'">
|
||||
@if (environment()?.appsYouTubeIntroVideoId && $any(environment()).appAwarenessMenu !== false) {
|
||||
<button mat-menu-item
|
||||
(click)="openAppsAwareness($event)" data-id="Apps Introduction Option"
|
||||
>
|
||||
<i class="al-icon sm-md al-ico-applications"></i>ClearML Apps Introduction
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
<a mat-menu-item href="mailto:support@clear.ml" data-id="Contact Us">
|
||||
<i class="al-icon sm-md al-ico-email"></i>Contact Us
|
||||
</a>
|
||||
</mat-menu>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
sm-show-only-user-work {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.resources-trigger {
|
||||
display: flex;
|
||||
color: $blue-300;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, computed, inject, input, signal} from '@angular/core';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {selectActiveWorkspace, selectCurrentUser} from '../../core/reducers/users-reducer';
|
||||
import {Observable, Subscription} from 'rxjs';
|
||||
import {logout} from '../../core/actions/users.actions';
|
||||
import {addMessage, openAppsAwarenessDialog} from '../../core/actions/layout.actions';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {GetCurrentUserResponseUserObject} from '~/business-logic/model/users/getCurrentUserResponseUserObject';
|
||||
import {ConfigurationService} from '../../shared/services/configuration.service';
|
||||
import {GetCurrentUserResponseUserObjectCompany} from '~/business-logic/model/users/getCurrentUserResponseUserObjectCompany';
|
||||
import {distinctUntilKeyChanged, filter} from 'rxjs/operators';
|
||||
@@ -17,7 +15,7 @@ import {LoginService} from '~/shared/services/login.service';
|
||||
import {selectUserSettingsNotificationPath} from '~/core/reducers/view.reducer';
|
||||
import {selectInvitesPending} from '~/core/reducers/users.reducer';
|
||||
import {MESSAGES_SEVERITY} from '@common/constants';
|
||||
import {UsersGetInvitesResponseInvites} from '~/business-logic/model/users/usersGetInvitesResponseInvites';
|
||||
import {takeUntilDestroyed, toSignal} from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-header',
|
||||
@@ -25,61 +23,49 @@ import {UsersGetInvitesResponseInvites} from '~/business-logic/model/users/users
|
||||
styleUrls: ['./header.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HeaderComponent implements OnInit, OnDestroy {
|
||||
@Input() isShareMode: boolean;
|
||||
@Input() isLogin: boolean;
|
||||
@Input() hideMenus: boolean;
|
||||
showLogo: boolean;
|
||||
profile: boolean;
|
||||
userFocus: boolean;
|
||||
public environment$ = this.configService.getEnvironment();
|
||||
public user: Observable<GetCurrentUserResponseUserObject>;
|
||||
public activeWorkspace: GetCurrentUserResponseUserObjectCompany;
|
||||
public url: Observable<string>;
|
||||
public invitesPending$: Observable<UsersGetInvitesResponseInvites[]>;
|
||||
private sub = new Subscription();
|
||||
public userNotificationPath$: Observable<string>;
|
||||
export class HeaderComponent {
|
||||
private store = inject(Store);
|
||||
private dialog = inject(MatDialog);
|
||||
public tipsService = inject(TipsService);
|
||||
private loginService = inject(LoginService);
|
||||
private router = inject(Router);
|
||||
private activeRoute = inject(ActivatedRoute);
|
||||
private configService = inject(ConfigurationService);
|
||||
|
||||
isShareMode = input<boolean>();
|
||||
isLogin = input<boolean>();
|
||||
hideMenus = input<boolean>();
|
||||
|
||||
protected environment = toSignal(this.configService.getEnvironment());
|
||||
protected url = this.store.selectSignal(selectRouterUrl);
|
||||
protected user = this.store.selectSignal(selectCurrentUser);
|
||||
protected userNotificationPath = this.store.selectSignal(selectUserSettingsNotificationPath);
|
||||
protected invitesPending = this.store.selectSignal(selectInvitesPending);
|
||||
protected userFocus = signal<boolean>(false);
|
||||
protected hideSideNav = signal<boolean>(false);
|
||||
protected dashboard = signal<boolean>(false);
|
||||
protected showLogo = computed<boolean>(() => this.hideSideNav() || this.dashboard());
|
||||
public activeWorkspace = toSignal<GetCurrentUserResponseUserObjectCompany>(this.store.select(selectActiveWorkspace)
|
||||
.pipe(
|
||||
filter(workspace => !!workspace),
|
||||
distinctUntilKeyChanged('id')
|
||||
)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private store: Store,
|
||||
private dialog: MatDialog,
|
||||
private tipsService: TipsService,
|
||||
private loginService: LoginService,
|
||||
private router: Router,
|
||||
private activeRoute: ActivatedRoute,
|
||||
private configService: ConfigurationService
|
||||
) {
|
||||
this.url = this.store.select(selectRouterUrl);
|
||||
|
||||
this.user = this.store.select(selectCurrentUser);
|
||||
this.userNotificationPath$ = this.store.select(selectUserSettingsNotificationPath);
|
||||
this.invitesPending$ = this.store.select(selectInvitesPending);
|
||||
this.sub.add(this.store.select(selectActiveWorkspace)
|
||||
.pipe(
|
||||
filter(workspace => !!workspace),
|
||||
distinctUntilKeyChanged('id')
|
||||
)
|
||||
.subscribe(workspace => {
|
||||
this.activeWorkspace = workspace;
|
||||
}));
|
||||
|
||||
this.sub.add(this.router.events
|
||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||
.subscribe(() => this.getRouteData()));
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getRouteData();
|
||||
this.router.events
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
filter((event) => event instanceof NavigationEnd)
|
||||
)
|
||||
.subscribe(() => this.getRouteData());
|
||||
}
|
||||
|
||||
getRouteData() {
|
||||
this.userFocus = !!this.activeRoute?.firstChild?.snapshot.data?.userFocus;
|
||||
this.showLogo = this.activeRoute?.firstChild?.snapshot.url?.[0]?.path === 'dashboard' || this.activeRoute?.firstChild?.snapshot.data.hideSideNav;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.sub.unsubscribe();
|
||||
this.userFocus.set(!!this.activeRoute?.firstChild?.snapshot.data?.userFocus);
|
||||
this.hideSideNav.set(this.activeRoute?.firstChild?.snapshot.data.hideSideNav);
|
||||
this.dashboard.set(this.activeRoute?.firstChild?.snapshot.url?.[0]?.path === 'dashboard');
|
||||
}
|
||||
|
||||
logout() {
|
||||
|
||||
@@ -3,8 +3,6 @@ import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {LoggedOutAlertComponent} from './logged-out-alert/logged-out-alert.component';
|
||||
import {StoreModule} from '@ngrx/store';
|
||||
import {LayoutReducer} from './layout.reducer';
|
||||
import {ServerNotificationDialogContainerComponent} from './server-notification-dialog-container/server-notification-dialog-container.component';
|
||||
import {CommonSearchModule} from '../common-search/common-search.module';
|
||||
import {HeaderComponent} from './header/header.component';
|
||||
@@ -35,7 +33,6 @@ import {TooltipDirective} from '@common/shared/ui-components/indicators/tooltip/
|
||||
ReactiveFormsModule,
|
||||
CommonSearchModule,
|
||||
RouterModule,
|
||||
StoreModule.forFeature('layout', LayoutReducer),
|
||||
YouTubePlayerModule,
|
||||
NgOptimizedImage,
|
||||
BreadcrumbsComponent,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user