release v1.16

This commit is contained in:
shallegro
2024-06-16 16:17:24 +03:00
parent d4f9424589
commit a30155004a
269 changed files with 7605 additions and 5853 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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)
];

View File

@@ -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";

View File

@@ -1,7 +0,0 @@
export function sourcesReducer(sources = {}, action) {
switch (action.type) {
default:
return sources;
}
}

View File

@@ -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,

View File

@@ -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 : [

View File

@@ -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
);

View File

@@ -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) => ({ });

View File

@@ -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 : []
})

View File

@@ -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>
}

View File

@@ -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}));

View File

@@ -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}));
}
}

View File

@@ -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,
]

View File

@@ -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}));
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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

View 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

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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'},

View File

@@ -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;
//}

View File

@@ -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}>()

View File

@@ -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]',

View File

@@ -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}>()
}
});

View File

@@ -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');

View File

@@ -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])
);

View File

@@ -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'};

View File

@@ -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);

View File

@@ -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[]>[];

View File

@@ -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);

View File

@@ -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[]>[];

View File

@@ -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) => ({

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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',

View File

@@ -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

View File

@@ -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}));

View File

@@ -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>
}

View File

@@ -53,6 +53,7 @@
::ng-deep .mat-expansion-panel-content {
.mat-expansion-panel-body {
height: 400px;
padding: 12px 0 12px 28px;
}
}

View File

@@ -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))
}
}

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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}));
}

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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}));
}
}

View File

@@ -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({

View File

@@ -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',

View File

@@ -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}>()
);

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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();
});
});

View File

@@ -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());
}
}
}

View File

@@ -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;

View File

@@ -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"

View File

@@ -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)"

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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>();

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>) {

View File

@@ -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']]) => {

View File

@@ -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)}`)])
);
});
}

View File

@@ -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'},

View File

@@ -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

View File

@@ -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
}));
}
}
}

View File

@@ -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"

View File

@@ -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)">

View File

@@ -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()
}));
}
}

View File

@@ -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>
}

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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}))
}
}

View File

@@ -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>

View File

@@ -41,6 +41,10 @@
width: 95%;
}
sm-show-only-user-work {
height: 22px;
}
.resources-trigger {
display: flex;
color: $blue-300;

View File

@@ -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() {

View File

@@ -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