Release v1.5 (#27)

This commit is contained in:
shyallegro
2022-06-07 17:48:29 +03:00
committed by GitHub
parent be221b67dd
commit 9f8f256186
276 changed files with 5943 additions and 3344 deletions

4475
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ClearML-webapp",
"version": "1.4.0",
"version": "1.5.0",
"license": "",
"scripts": {
"ng": "ng",
@@ -48,7 +48,7 @@
"bootstrap": "^4.6.1",
"britecharts": "^2.18.0",
"curved-arrows": "^0.1.0",
"d3-selection": "^1.4.2",
"d3-selection": "^3.0.0",
"diff": "^5.0.0",
"filesize": "^8.0.7",
"has-ansi": "^5.0.1",
@@ -87,7 +87,7 @@
"@fortawesome/fontawesome-free": "^6.0.0",
"@ngrx/schematics": "^13.0.2",
"@ngrx/store-devtools": "^13.0.2",
"@types/d3-selection": "^1.4.3",
"@types/d3-selection": "^3.0.2",
"@types/lodash": "^4.14.178",
"@types/node": "^16.11.19",
"@types/plotly.js": "^1.54.20",

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.
@@ -22,6 +22,7 @@ import { CustomHttpUrlEncodingCodec } from '../encoder';
import { Observable } from 'rxjs';
import { AuthCreateCredentialsRequest } from '../model/auth/authCreateCredentialsRequest';
import { AuthCreateCredentialsResponse } from '../model/auth/authCreateCredentialsResponse';
import { AuthCreateUserRequest } from '../model/auth/authCreateUserRequest';
import { AuthCreateUserResponse } from '../model/auth/authCreateUserResponse';
@@ -40,6 +41,8 @@ import { AuthValidateTokenResponse } from '../model/auth/authValidateTokenRespon
import { BASE_PATH, COLLECTION_FORMATS } from '../variables';
import { Configuration } from '../configuration';
import {AuthEditCredentialsRequest} from '~/business-logic/model/auth/authEditCredentialsRequest';
import {AuthEditCredentialsResponse} from '~/business-logic/model/auth/authEditCredentialsResponse';
@Injectable()
@@ -75,13 +78,13 @@ export class ApiAuthService {
/**
*
*
* Internal. Creates a new set of credentials for the authenticated user. New key/secret is returned. Note: Secret will never be returned in any other API call. If a secret is lost or compromised, the key should be revoked and a new set of credentials can be created.
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public authCreateCredentials(request: object, options?: any, observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
public authCreateCredentials(request: AuthCreateCredentialsRequest, options?: any, observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
if (request === null || request === undefined) {
throw new Error('Required parameter request was null or undefined when calling authCreateCredentials.');
}
@@ -120,7 +123,7 @@ export class ApiAuthService {
}
/**
*
*
* Internal. Creates a new user auth entry. Intended for internal use.
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@@ -165,7 +168,7 @@ export class ApiAuthService {
}
/**
*
*
* Internal. Edit a users\&#39; auth data properties
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@@ -210,7 +213,7 @@ export class ApiAuthService {
}
/**
*
*
* Internal. Return fixed users mode status
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@@ -255,7 +258,7 @@ export class ApiAuthService {
}
/**
*
*
* Internal. Returns all existing credential keys for the authenticated user. Note: Only credential keys are returned.
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@@ -299,11 +302,56 @@ export class ApiAuthService {
);
}
/**
*
* Internal. Get a token for the specified user. Intended for internal use.
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
/**
*
* Internal. Updates the label of the existing credentials for the authenticated user.
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public authEditCredentials(request: AuthEditCredentialsRequest, options?: any, observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
if (request === null || request === undefined) {
throw new Error('Required parameter request was null or undefined when calling authEditCredentials.');
}
let headers = this.defaultHeaders;
if (options && options.async_enable) {
headers = headers.set(this.configuration.asyncHeader, '1');
}
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
const httpHeaderAcceptSelected: string | undefined = this.configuration.selectHeaderAccept(httpHeaderAccepts);
if (httpHeaderAcceptSelected != undefined) {
headers = headers.set("Accept", httpHeaderAcceptSelected);
}
// to determine the Content-Type header
const consumes: string[] = [
];
const httpContentTypeSelected:string | undefined = this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected != undefined) {
headers = headers.set("Content-Type", httpContentTypeSelected);
}
return this.apiRequest.post<AuthEditCredentialsResponse>(`${this.basePath}/auth.edit_credentials`,
request,
{
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
*
* Internal. Get a token for the specified user. Intended for internal use.
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public authGetTokenForUser(request: AuthGetTokenForUserRequest, options?: any, observe: any = 'body', reportProgress: boolean = false ): Observable<any> {
@@ -345,7 +393,7 @@ export class ApiAuthService {
}
/**
*
*
* Internal. Get a token based on supplied credentials (key/secret). Intended for use by users with key/secret credentials that wish to obtain a token for use with other services.
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@@ -390,7 +438,7 @@ export class ApiAuthService {
}
/**
*
*
* Internal. Removes the authentication cookie from the current session
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@@ -435,7 +483,7 @@ export class ApiAuthService {
}
/**
*
*
* Internal. Revokes (and deletes) a set (key, secret) of credentials for the authenticated user.
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@@ -480,7 +528,7 @@ export class ApiAuthService {
}
/**
*
*
* Internal. Validate a token and return user identity if valid. Intended for internal use.
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.

View File

@@ -0,0 +1,20 @@
/**
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
export interface AuthCreateCredentialsRequest {
/**
* Optional credentials label
*/
label?: string;
}

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -0,0 +1,24 @@
/**
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
export interface AuthEditCredentialsRequest {
/**
* Existing credentials key
*/
access_key: string;
/**
* New credentials label
*/
label?: string;
}

View File

@@ -0,0 +1,20 @@
/**
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
export interface AuthEditCredentialsResponse {
/**
* Number of credentials updated
*/
updated?: number;
}

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.
@@ -14,16 +14,19 @@
export interface CredentialKey {
/**
*
*
*/
access_key?: string;
/**
*
* Optional credentials label
*/
label?: string;
/**
*
*/
last_used?: string;
/**
*
*/
last_used_from?: string;
label?: string; // (nir) until BE will implement
/**
*
*/
last_used_from?: string;
}

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.
@@ -21,4 +21,8 @@ export interface Credentials {
* Credentials secret key
*/
secret_key?: string;
/**
* Optional credentials label
*/
label?: string;
}

View File

@@ -1,3 +1,4 @@
export * from '././authCreateCredentialsRequest';
export * from '././authCreateCredentialsResponse';
export * from '././authCreateUserRequest';
export * from '././authCreateUserResponse';

View File

@@ -2,7 +2,7 @@
* auth
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 2.18
*
*
* NOTE: This class is auto generated by the swagger code generator program.

View File

@@ -2,12 +2,12 @@ import { Injectable } from '@angular/core';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import * as actions from '../../webapp-common/core/actions/projects.actions';
import {Store} from '@ngrx/store';
import {selectSelectedProjectId} from '../../webapp-common/core/reducers/projects.reducer';
import {selectSelectedProjectId} from '@common/core/reducers/projects.reducer';
import {catchError, finalize, mergeMap, switchMap, withLatestFrom} from 'rxjs/operators';
import {deactivateLoader} from '../../webapp-common/core/actions/layout.actions';
import {ALL_PROJECTS_OBJECT} from '../../webapp-common/core/effects/projects.effects';
import {requestFailed} from '../../webapp-common/core/actions/http.actions';
import {ApiProjectsService} from '../../business-logic/api-services/projects.service';
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';
@@ -15,14 +15,21 @@ import {ApiProjectsService} from '../../business-logic/api-services/projects.ser
export class ProjectsEffects {
private fetchingExampleExperiment: string = null;
constructor(private actions$: Actions, private store: Store, private projectsApi: ApiProjectsService) {}
constructor(
private actions$: Actions,
private store: Store,
private projectsApi: ApiProjectsService
) {}
getSelectedProject = createEffect(() => this.actions$.pipe(
ofType(actions.setSelectedProjectId),
withLatestFrom(this.store.select(selectSelectedProjectId)),
switchMap(([action, selectedProjectId]) => {
if (!action.projectId) {
return [actions.setSelectedProject({project: null})];
return [
deactivateLoader(action.type),
actions.setSelectedProject({project: null}),
];
}
if (action.projectId === selectedProjectId) {
return [deactivateLoader(action.type)];
@@ -30,8 +37,8 @@ export class ProjectsEffects {
if (action.projectId === '*') {
return [
actions.setSelectedProject({project: ALL_PROJECTS_OBJECT}),
deactivateLoader(action.type)
];
actions.getProjectUsers(action),
deactivateLoader(action.type)];
} else {
this.fetchingExampleExperiment = action.example && action.projectId;
return this.projectsApi.projectsGetAllEx({
@@ -45,6 +52,7 @@ export class ProjectsEffects {
finalize(() => this.fetchingExampleExperiment = null),
mergeMap(({projects}) => [
actions.setSelectedProject({project: projects[0]}),
actions.getProjectUsers(action),
deactivateLoader(action.type),
]
),

View File

@@ -2,6 +2,7 @@ import {createReducer, createSelector, on} from '@ngrx/store';
import {initUsers, users, usersReducerFunctions, UsersState} from '@common/core/reducers/users-reducer';
import {setCurrentUser} from '../actions/users.action';
import {of} from 'rxjs';
export const selectHasDataFeature = createSelector(users, state => false);
export const selectHasUserManagement = createSelector(users, () => false);
@@ -20,3 +21,4 @@ export const selectFeatures = createSelector(users, (state) => []);
// eslint-disable-next-line @typescript-eslint/naming-convention
export const selectTermsOfUse = createSelector(users, state => ({accept_required: null}));
export const selectInvitesPending = createSelector(users, state => []);
export const userAllowedToCreateQueue$ = store => of(true);

View File

@@ -3,8 +3,11 @@
(projectSelected)="projectCardClicked($event)"
(experimentSelected)="taskSelected($event)"
(modelSelected)="modelSelected($event)"
(pipelineSelected)="pipelineSelected($event)"
(activeLinkChanged)="activeLinkChanged($event)"
[projectsList]="projectsResults$ | async"
[pipelinesList]="pipelinesResults$ | async"
[experimentsList]="experimentsResults$ | async"
[modelsList]="modelsResults$ | async"
[activeLink]="activeLink"></sm-search-results-page>
[activeLink]="activeLink">
</sm-search-results-page>

View File

@@ -3,10 +3,13 @@ import {Router} from '@angular/router';
import {combineLatest, Observable, ObservedValueOf, Subscription} from 'rxjs';
import {filter, skip} from 'rxjs/operators';
import {Store} from '@ngrx/store';
import {Project} from '../../../../business-logic/model/projects/project';
import {Model} from '../../../../business-logic/model/models/model';
import {DashboardSearchComponentBase} from '../../../../webapp-common/dashboard/dashboard-search.component.base';
import {SearchClear} from '../../../../webapp-common/dashboard-search/dashboard-search.actions';
import {Project} from '~/business-logic/model/projects/project';
import {Model} from '~/business-logic/model/models/model';
import {DashboardSearchComponentBase} from '@common/dashboard/dashboard-search.component.base';
import {SearchClear} from '@common/dashboard-search/dashboard-search.actions';
export type ActiveSearchLink = 'projects' | 'experiments' | 'models' | 'pipelines';
@Component({
selector : ' sm-dashboard-search',

View File

@@ -1,21 +1,12 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {SMSharedModule} from '../../../webapp-common/shared/shared.module';
import {SMSharedModule} from '@common/shared/shared.module';
import {StoreModule} from '@ngrx/store';
import {EffectsModule} from '@ngrx/effects';
import {DashboardSearchEffects} from '../../../webapp-common/dashboard-search/dashboard-search.effects';
import {ExperimentsSearchResultsComponent} from '../../../webapp-common/dashboard-search/dumb/experiments-search-results/experiments-search-results.component';
import {ModelsSearchResultsComponent} from '../../../webapp-common/dashboard-search/dumb/models-search-results/models-search-results.component';
import {ProjectsSearchResultsComponent} from '../../../webapp-common/dashboard-search/dumb/projects-search-results/projects-search-results.component';
import {DashboardSearchEffects} from '@common/dashboard-search/dashboard-search.effects';
import {ProjectsSharedModule} from '../../projects/shared/projects-shared.module';
import {SharedModule} from '../../../shared/shared.module';
import {dashboardSearchReducer} from '../../../webapp-common/dashboard-search/dashboard-search.reducer';
const declarations = [
ModelsSearchResultsComponent,
ProjectsSearchResultsComponent,
ExperimentsSearchResultsComponent,
];
import {SharedModule} from '~/shared/shared.module';
import {dashboardSearchReducer} from '@common/dashboard-search/dashboard-search.reducer';
@NgModule({
imports : [
@@ -26,8 +17,6 @@ const declarations = [
EffectsModule.forFeature([DashboardSearchEffects]),
SharedModule
],
declarations: [declarations],
exports : [...declarations]
})
export class DashboardSearchModule {
}

View File

@@ -5,15 +5,16 @@ import {ExperimentSharedModule} from '../experiments/shared/experiment-shared.mo
import {DashboardRoutingModule} from './dashboard-routing.module';
import {StoreModule} from '@ngrx/store';
import {GettingStartedCardComponent} from './dumb/getting-started-card/getting-started-card.component';
import {SMSharedModule} from '../../webapp-common/shared/shared.module';
import {CommonDashboardModule} from '../../webapp-common/dashboard/common-dashboard.module';
import {commonDashboardReducer} from '../../webapp-common/dashboard/common-dashboard.reducer';
import {SMSharedModule} from '@common/shared/shared.module';
import {CommonDashboardModule} from '@common/dashboard/common-dashboard.module';
import {commonDashboardReducer} from '@common/dashboard/common-dashboard.reducer';
import {DashboardSearchComponent} from './containers/dashboard-search/dashboard-search.component';
import {SearchResultsPageComponent} from './dumb/search-results-page/search-results-page.component';
import {SharedModule} from '../../shared/shared.module';
import {SharedModule} from '~/shared/shared.module';
import {DashboardSearchModule} from './dashboard-search/dashboard-search.module';
import {ProjectDialogModule} from '../../webapp-common/shared/project-dialog/project-dialog.module';
import {ProjectDialogModule} from '@common/shared/project-dialog/project-dialog.module';
import {ProjectsSharedModule} from '../projects/shared/projects-shared.module';
import {SearchResultsComponent} from '@common/dashboard-search/dumb/search-results/search-results.component';
@NgModule({
imports: [
@@ -28,7 +29,7 @@ import {ProjectsSharedModule} from '../projects/shared/projects-shared.module';
SharedModule,
DashboardSearchModule
],
declarations : [DashboardComponent, GettingStartedCardComponent, DashboardSearchComponent, SearchResultsPageComponent]
declarations : [DashboardComponent, GettingStartedCardComponent, DashboardSearchComponent, SearchResultsPageComponent, SearchResultsComponent]
})
export class DashboardModule {
}

View File

@@ -7,22 +7,51 @@
(click)="activeLinkChanged.emit('experiments')">EXPERIMENTS {{'(' + experimentsList.length + ')'}} </span>
<span [ngClass]="{'active': activeLink === 'models'}" class="pointer category-link"
(click)="activeLinkChanged.emit('models')">MODELS {{'(' + modelsList.length + ')'}} </span>
<span [ngClass]="{'active': activeLink === 'pipelines'}" class="pointer category-link"
(click)="activeLinkChanged.emit('pipelines')">PIPELINES ({{pipelinesList?.length}}) </span>
</div>
</div>
<div *ngIf="activeLink === 'projects'" class="page-container">
<sm-projects-search-results [projectsList]="projectsList"
(projectClicked)="projectClicked($event)"></sm-projects-search-results>
<div class="page-container">
<sm-search-results
[cardTemplate]="
activeLink === 'projects' ? ProjectTemplate :
activeLink === 'experiments' ? ExperimentTemplate :
activeLink === 'models' ? ModelsTemplate :
activeLink === 'pipelines' ? PipelineTemplate :
ProjectTemplate"
[results]="getResults()"
[cardHeight]="getCardHeight()"
(resultClicked)="projectClicked($event)">
</sm-search-results>
</div>
<ng-template #ProjectTemplate let-project="result">
<sm-project-card
[project]="project"
(projectCardClicked)="projectClicked($event)"
[hideMenu]="true"
></sm-project-card>
</ng-template>
<div *ngIf="activeLink === 'experiments'" class="page-container">
<sm-experiments-search-results [experimentsList]="experimentsList"
(experimentClicked)="experimentClicked($event)"></sm-experiments-search-results>
</div>
<ng-template #ExperimentTemplate let-experiment="result">
<sm-experiment-card
[experiment]="experiment"
(experimentCardClicked)="experimentClicked($event)"
></sm-experiment-card>
</ng-template>
<div *ngIf="activeLink === 'models'" class="page-container">
<sm-models-search-results [modelsList]="modelsList"
(modelClicked)="modelClicked($event)"></sm-models-search-results>
</div>
<ng-template #ModelsTemplate let-model="result">
<sm-model-card
[model]="model"
(modelCardClicked)="modelClicked($event)"
></sm-model-card>
</ng-template>
<ng-template #PipelineTemplate let-pipeline="result">
<sm-pipeline-card
[project]="pipeline"
[hideMenu]="true"
(projectCardClicked)="pipelineClicked($event)"
></sm-pipeline-card>
</ng-template>
</div>

View File

@@ -1,8 +1,9 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Project} from '../../../../business-logic/model/projects/project';
import {Task} from '../../../../business-logic/model/tasks/task';
import {ITask} from '../../../../business-logic/model/al-task';
import {Model} from '../../../../business-logic/model/models/model';
import {Project} from '~/business-logic/model/projects/project';
import {Task} from '~/business-logic/model/tasks/task';
import {ITask} from '~/business-logic/model/al-task';
import {Model} from '~/business-logic/model/models/model';
import {ActiveSearchLink} from '~/features/dashboard/containers/dashboard-search/dashboard-search.component';
@Component({
selector : 'sm-search-results-page',
@@ -13,12 +14,14 @@ export class SearchResultsPageComponent {
@Input() projectsList: Array<Project> = [];
@Input() experimentsList: Array<Task> = [];
@Input() modelsList: Array<Model> = [];
@Input() activeLink = 'projects';
@Input() pipelinesList: Array<Project> = [];
@Input() activeLink: ActiveSearchLink;
@Output() projectSelected = new EventEmitter<Project>();
@Output() activeLinkChanged = new EventEmitter<string>();
@Output() experimentSelected = new EventEmitter<ITask>();
@Output() modelSelected = new EventEmitter<Model>();
@Output() pipelineSelected = new EventEmitter<Project>();
public projectClicked(project: Project) {
this.projectSelected.emit(project);
@@ -32,4 +35,25 @@ export class SearchResultsPageComponent {
this.modelSelected.emit(model);
}
public pipelineClicked(pipeline: Project) {
this.pipelineSelected.emit(pipeline);
}
getResults() {
return this[`${this.activeLink}List`];
}
getCardHeight() {
switch (this.activeLink) {
case 'projects':
return 246;
case 'experiments':
case 'models':
return 264;
case 'pipelines':
return 226;
default:
return 250;
}
}
}

View File

@@ -0,0 +1,58 @@
<nav [smOverflows]="splitSize" (onOverflows)="navbarOverflowed($event)" [class.minimized]="minimized">
<span [routerLink]="['execution']" routerLinkActive #rlaExecution="routerLinkActive" queryParamsHandling="preserve">
<sm-navbar-item header="execution" [active]="rlaExecution.isActive" class="small-nav"></sm-navbar-item>
</span>
<span [routerLink]="['hyper-params/hyper-param/_empty_']" queryParamsHandling="merge">
<sm-navbar-item header="configuration"
class="small-nav"
[active]="(routerConfig$| async)?.includes('hyper-params')"></sm-navbar-item>
</span>
<span [routerLink]="['artifacts']" routerLinkActive #rlaModel="routerLinkActive" queryParamsHandling="preserve">
<sm-navbar-item header="artifacts"
class="small-nav"
[active]="rlaModel.isActive"></sm-navbar-item>
</span>
<span [routerLink]="['general']" routerLinkActive #rlaGeneral="routerLinkActive" queryParamsHandling="preserve">
<sm-navbar-item header="info"
class="small-nav"
[active]="rlaGeneral.isActive"></sm-navbar-item>
</span>
<span [matMenuTriggerFor]="results" *ngIf="overflow">
<sm-navbar-item header="results"
class="small-nav"
[multi]="true"
[active]="rlaDebug.isActive || rlaPlots.isActive || rlaScalars.isActive || rlaLog.isActive"></sm-navbar-item>
</span>
<div class="d-inline-block" [style.visibility]="overflow ? 'hidden' : 'visible'">
<span [routerLink]="baseInfoRoute.concat(['log'])" routerLinkActive queryParamsHandling="preserve"
#rlaLog="routerLinkActive">
<sm-navbar-item class="small-nav" header="console" [active]="rlaLog.isActive"></sm-navbar-item>
</span>
<span [routerLink]="baseInfoRoute.concat(['metrics','scalar'])" routerLinkActive queryParamsHandling="preserve"
#rlaScalars="routerLinkActive">
<sm-navbar-item class="small-nav" header="Scalars" [active]="rlaScalars.isActive"></sm-navbar-item>
</span>
<span [routerLink]="baseInfoRoute.concat(['metrics','plots'])" routerLinkActive queryParamsHandling="preserve"
#rlaPlots="routerLinkActive">
<sm-navbar-item class="small-nav" header="PLOTS" [active]="rlaPlots.isActive"></sm-navbar-item>
</span>
<span [routerLink]="baseInfoRoute.concat(['debugImages'])" routerLinkActive queryParamsHandling="preserve"
#rlaDebug="routerLinkActive">
<sm-navbar-item class="small-nav" header="DEBUG SAMPLES" [active]="rlaDebug.isActive"></sm-navbar-item>
</span>
</div>
<mat-menu #results="matMenu">
<button mat-menu-item [routerLink]="baseInfoRoute.concat(['log'])" [class.active]="rlaLog.isActive">CONSOLE</button>
<button mat-menu-item [routerLink]="baseInfoRoute.concat(['metrics','scalar'])"
[class.active]="rlaScalars.isActive">SCALARS
</button>
<button mat-menu-item [routerLink]="baseInfoRoute.concat(['metrics','plots'])" [class.active]="rlaPlots.isActive">
PLOTS
</button>
<button mat-menu-item [routerLink]="baseInfoRoute.concat(['debugImages'])" [class.active]="rlaDebug.isActive">DEBUG
SAMPLES
</button>
</mat-menu>
<ng-content select="[refresh]"></ng-content>
</nav>

View File

@@ -0,0 +1,32 @@
@import "variables";
$output-tabs-height: 40px;
nav {
height: $output-tabs-height;
position: relative;
text-align: center;
border-bottom: 1px solid #efefef;
//overflow: hidden;
padding: 0 24px;
.refresh-position {
position: absolute;
right: 16px;
top: 6px;
display: flex;
align-items: center;
}
.refreshIcon{
margin-right: 10px;
}
span.disabled {
pointer-events: none;
}
}
.mat-menu-item {
padding-left: 22px;
&.active {
border-left: 6px solid $purple;
padding-left: 16px;
}
}

View File

@@ -0,0 +1,38 @@
import {Component, Input} from '@angular/core';
import {FeaturesEnum} from '../../../../business-logic/model/users/featuresEnum';
import {selectRouterConfig} from '../../../../webapp-common/core/reducers/router-reducer';
import {Store} from '@ngrx/store';
import {IExperimentInfoState} from '../../reducers/experiment-info.reducer';
import {Observable} from 'rxjs';
@Component({
selector: 'sm-experiment-info-navbar',
templateUrl: './experiment-info-navbar.component.html',
styleUrls: ['./experiment-info-navbar.component.scss']
})
export class ExperimentInfoNavbarComponent {
public featuresEnum = FeaturesEnum;
public routerConfig$: Observable<string[]>;
public baseInfoRoute: string[];
public overflow: boolean;
private _minimized: boolean;
@Input() set minimized(minimized: boolean) {
this.baseInfoRoute = minimized ? ['info-output'] : [];
this._minimized = minimized;
}
get minimized(){
return this._minimized;
}
@Input() splitSize: number;
constructor(private store: Store<IExperimentInfoState>,) {
this.routerConfig$ = this.store.select(selectRouterConfig);
}
navbarOverflowed($event: boolean) {
this.overflow = $event;
}
}

View File

@@ -1,41 +0,0 @@
<sm-experiment-info-header-status-progress-bar
[status]="(tableSelectedExperiment$| async)?.status || selectedExperiment?.status"
[editable]="editable$ | async"
[showMaximize]="true"
(closeInfoClicked)="deselectExperiment()"
(maximizedClicked)="maximize()">
</sm-experiment-info-header-status-progress-bar>
<div class="experiment-info-container light-theme">
<sm-experiment-info-header
[experiment]="(tableSelectedExperiment$| async) || selectedExperiment"
[infoData]="infoData$ | async"
[editable]="!isExample"
[showMenu]="true"
[backdropActive]="backdropActive$|async"
(experimentNameChanged)="updateExperimentName($event)"
>
</sm-experiment-info-header>
<nav>
<span routerLink="execution" [routerLinkActive]="'disabled'" queryParamsHandling="merge">
<sm-navbar-item header="execution" [active]="routerConfig.includes('execution')"></sm-navbar-item>
</span>
<span [routerLink]="['hyper-params/hyper-param/_empty_']" [class.disabled]="routerConfig.includes('hyper-params')" queryParamsHandling="merge">
<sm-navbar-item header="configuration" [active]="routerConfig.includes('hyper-params')"></sm-navbar-item>
</span>
<span [routerLink]="['artifacts']" [routerLinkActive]="'disabled'" queryParamsHandling="preserve">
<sm-navbar-item header="artifacts" [active]="routerConfig.includes('artifacts')"></sm-navbar-item>
</span>
<span [routerLink]="['general']" [routerLinkActive]="'disabled'" queryParamsHandling="preserve">
<sm-navbar-item header="info" [active]="routerConfig.includes('general')"></sm-navbar-item>
</span>
<span [routerLink]="['info-output']" [routerLinkActive]="'disabled'" queryParamsHandling="preserve">
<sm-navbar-item header="results" [active]="routerConfig.includes('info-output')"></sm-navbar-item>
</span>
</nav>
<div class="experiment-info-body" #scrollContainer>
<router-outlet (activate)="onActivate($event, scrollContainer)"></router-outlet>
</div>
</div>

View File

@@ -1,38 +0,0 @@
@import "../../../../webapp-common/shared/ui-components/styles/variables";
@import "../../../../webapp-common/layout/layout.scss";
:host {
display: block;
height: 100%;
.experiment-info-body {
height: calc(100% - #{$experiment-info-header-height + $experiment-info-tabs-height});
flex-grow: 1;
overflow: auto;
}
.experiment-info-container {
height: calc(100% - #{$project-info-progress-height});
padding: 15px 0 0 5px;
display: flex;
flex-direction: column;
nav {
height: $experiment-info-tabs-height;
border-bottom: 1px solid rgba(2, 2, 2, 0.07);
span.disabled {
pointer-events: none;
}
}
}
::ng-deep nav {
border-bottom: 1px solid rgba(2, 2, 2, 0.07);
}
::ng-deep sm-simple-table-2 .headers {
font-size: 12px;
}
}

View File

@@ -1,131 +0,0 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {ActivatedRoute, Params, Router} from '@angular/router';
import {Store} from '@ngrx/store';
import {get} from 'lodash/fp';
import {Observable, Subscription} from 'rxjs';
import {debounceTime, distinctUntilChanged, filter, map, tap} from 'rxjs/operators';
import {MESSAGES_SEVERITY} from '~/app.constants';
import {IExperimentInfoState} from '../../reducers/experiment-info.reducer';
import {selectExperimentInfoData, selectIsExperimentEditable, selectSelectedExperiment} from '../../reducers';
import {selectBackdropActive} from '@common/core/reducers/view.reducer';
import {isReadOnly} from '@common/shared/utils/shared-utils';
import {selectRouterConfig, selectRouterParams, selectRouterQueryParams} from '@common/core/reducers/router-reducer';
import * as commonInfoActions from '../../../../webapp-common/experiments/actions/common-experiments-info.actions';
import {ExperimentDetailsUpdated} from '@common/experiments/actions/common-experiments-info.actions';
import {addMessage} from '@common/core/actions/layout.actions';
import {IExperimentInfo} from '../../shared/experiment-info.model';
import {selectSelectedTableExperiment} from '@common/experiments/reducers';
import {ITableExperiment} from '@common/experiments/shared/common-experiment-model.model';
import {setTableMode} from '@common/experiments/actions/common-experiments-view.actions';
@Component({
selector: 'sm-experiment-info',
templateUrl: './experiment-info.component.html',
styleUrls: ['./experiment-info.component.scss']
})
export class ExperimentInfoComponent implements OnInit, OnDestroy {
private paramsSubscription: Subscription;
public selectedExperiment: IExperimentInfo;
private selectedExperimentSubscription: Subscription;
public editable$: Observable<boolean>;
public infoData$: Observable<IExperimentInfo>;
public backdropActive$: Observable<any>;
public isExample: boolean;
private projectId: string;
private experimentId: string;
public resultsTab: boolean;
public queryParams$: Observable<Params>;
public routerConfig: string[];
private routerConfigSubscription: Subscription;
public tableSelectedExperiment$: Observable<ITableExperiment>;
private toMaximize = false;
constructor(
private router: Router,
private store: Store<IExperimentInfoState>,
private route: ActivatedRoute
) {
this.editable$ = this.store.select(selectIsExperimentEditable);
this.infoData$ = this.store.select(selectExperimentInfoData);
this.backdropActive$ = this.store.select(selectBackdropActive);
this.queryParams$ = this.store.select(selectRouterQueryParams);
this.tableSelectedExperiment$ = this.store.select(selectSelectedTableExperiment);
}
ngOnInit() {
this.selectedExperimentSubscription = this.store.select(selectSelectedExperiment)
.subscribe(experiment => {
this.selectedExperiment = experiment;
this.isExample = isReadOnly(experiment);
});
this.routerConfigSubscription = this.store.select(selectRouterConfig).subscribe(routerConfig => {
this.routerConfig = routerConfig;
});
this.paramsSubscription = this.store.select(selectRouterParams)
.pipe(
tap((params) => {
this.projectId = get('projectId', params);
this.resultsTab = 'info-output' === this.route.firstChild.routeConfig.path;
}),
debounceTime(150),
map(params => get('experimentId', params)),
filter(experimentId => !!experimentId),
distinctUntilChanged()
)
.subscribe(experimentId => {
this.experimentId = experimentId;
// We already have GetExperimentInfo in output (results) component
if (!this.resultsTab) {
this.store.dispatch(new commonInfoActions.ResetExperimentInfo());
this.store.dispatch(new commonInfoActions.GetExperimentInfo(experimentId));
}
});
}
ngOnDestroy(): void {
this.paramsSubscription.unsubscribe();
this.selectedExperimentSubscription.unsubscribe();
this.routerConfigSubscription.unsubscribe();
if (!this.toMaximize) {
this.store.dispatch(new commonInfoActions.SetExperiment(null));
this.store.dispatch(new commonInfoActions.ResetExperimentInfo());
}
}
updateExperimentName(name) {
if (name.trim().length > 2) {
this.store.dispatch(new ExperimentDetailsUpdated({id: this.selectedExperiment.id, changes: {name}}));
} else {
this.store.dispatch(addMessage(MESSAGES_SEVERITY.ERROR, 'Name must be more than three letters long'));
}
}
deselectExperiment() {
this.navigateAfterExperimentSelectionChanged();
}
navigateAfterExperimentSelectionChanged() {
this.store.dispatch(setTableMode({mode: 'table'}));
this.router.navigate([`projects/${this.projectId}/experiments`], {queryParamsHandling: 'merge'});
}
onActivate(e, scrollContainer) {
scrollContainer.scrollTop = 0;
}
maximize() {
if (window.location.pathname.includes('info-output')) {
const resultsPath = this.route.firstChild?.firstChild?.routeConfig?.path || this.route.firstChild.routeConfig.path;
this.router.navigateByUrl(`projects/${this.projectId}/experiments/${this.experimentId}/output/${resultsPath}`);
} else {
const parts = window.location.pathname.split('/');
parts.splice(5, 0, 'output');
this.router.navigateByUrl(parts.join('/'));
}
this.toMaximize = true;
}
}

View File

@@ -1,4 +1,4 @@
import { Component} from '@angular/core';
import {Component, Input} from '@angular/core';
import {ExperimentMenuComponent} from '@common/experiments/shared/components/experiment-menu/experiment-menu.component';
@Component({
@@ -7,7 +7,6 @@ import {ExperimentMenuComponent} from '@common/experiments/shared/components/exp
styleUrls: ['../../../../webapp-common/experiments/shared/components/experiment-menu/experiment-menu.component.scss']
})
export class ExperimentMenuExtendedComponent extends ExperimentMenuComponent{
set contextMenu(data) {}
get contextMenu() {
return this;

View File

@@ -1,60 +1,24 @@
<sm-overlay [backdropActive]="backdropActive$|async"></sm-overlay>
<div class="experiment-output-container light-theme">
<sm-experiment-info-header-status-icon-label
[status]=" (selectedExperiment)?.status || selectedExperiment?.status"
[development]="isDevelopment"
></sm-experiment-info-header-status-icon-label>
<div class="experiment-output-container light-theme" [class.minimized]="minimized">
<sm-experiment-info-header
*ngIf="!minimized"
(minimizeClicked)="minimizeView()"
[experiment]="selectedExperiment"
[infoData]="infoData$ | async"
[editable]="!isExample"
[showMenu]="true"
[showMinimize]="true"
[minimized]="minimized"
[isSharedAndNotOwner]="isSharedAndNotOwner$ | async"
(experimentNameChanged)="updateExperimentName($event)"
(closeInfoClicked)="closePanel()"
(minimizeClicked)="minimizeView()"
(maximizedClicked)="maximize()"
>
</sm-experiment-info-header>
<nav [class.minimized]="minimized" [smOverflows]="'nav'" (onOverflows)="overflow = $event">
<ng-container *ngIf="!minimized">
<span [routerLink]="['execution']" queryParamsHandling="preserve">
<sm-navbar-item header="execution" [active]="routerConfig.includes('execution')"></sm-navbar-item>
</span>
<span [routerLink]="['hyper-params/hyper-param/_empty_']" [class.disabled]="routerConfig.includes('hyper-params')" queryParamsHandling="merge">
<sm-navbar-item header="Configuration" [active]="routerConfig.includes('hyper-params')"></sm-navbar-item>
</span>
<span [routerLink]="['artifacts']" [routerLinkActive]="'disabled'">
<sm-navbar-item header="artifacts" [active]="routerConfig.includes('artifacts')"></sm-navbar-item>
</span>
<span [routerLink]="['general']" queryParamsHandling="preserve">
<sm-navbar-item header="info" [active]="routerConfig.includes('general')"></sm-navbar-item>
</span>
</ng-container>
<span [matMenuTriggerFor]="results" *ngIf="!minimized && overflow">
<sm-navbar-item header="results" [multi]="true" [active]="console.active || scalar.active || plots.active || samples.active"></sm-navbar-item>
</span>
<mat-menu #results="matMenu">
<button mat-menu-item [routerLink]="['log']" [class.active]="routerConfig.includes('log')">CONSOLE</button>
<button mat-menu-item [routerLink]="['metrics','scalar']" [class.active]="routerConfig.includes('metrics') && routerConfig.includes('scalar')">SCALARS</button>
<button mat-menu-item [routerLink]="['metrics','plots']" [class.active]="routerConfig.includes('metrics') && routerConfig.includes('plots')">PLOTS</button>
<button mat-menu-item [routerLink]="['debugImages']" [class.active]="routerConfig.includes('debugImages')">DEBUG SAMPLES</button>
</mat-menu>
<div class="d-inline-block" [style.visibility]="overflow && !minimized ? 'hidden' : 'visible'">
<span [routerLink]="['log']" queryParamsHandling="preserve">
<sm-navbar-item #console header="console" [active]="routerConfig.includes('log')"></sm-navbar-item>
</span>
<span [routerLink]="['metrics','scalar']" queryParamsHandling="preserve">
<sm-navbar-item #scalar header="Scalars" [active]="routerConfig.includes('metrics') && routerConfig.includes('scalar')"></sm-navbar-item>
</span>
<span [routerLink]="['metrics','plots']" queryParamsHandling="preserve">
<sm-navbar-item #plots header="PLOTS" [active]="routerConfig.includes('metrics') && routerConfig.includes('plots')"></sm-navbar-item>
</span>
<span [routerLink]="['debugImages']" queryParamsHandling="preserve">
<sm-navbar-item #samples header="DEBUG SAMPLES" [active]="routerConfig.includes('debugImages')"></sm-navbar-item>
</span>
</div>
<span class="refresh-position">
<sm-experiment-info-navbar [minimized]="minimized" [splitSize]="selectSplitSize$ | async">
<span class="refresh-position" refresh>
<sm-experiment-settings
[class.maximized]="!minimized"
[showSettings]="routerConfig.includes('scalar') && minimized"
@@ -67,8 +31,8 @@
>
</sm-refresh-button>
</span>
</nav>
<div class="output-body" [class.minimized]="minimized">
<router-outlet class="output-outlet"></router-outlet>
</sm-experiment-info-navbar>
<div class="output-body" [class.minimized]="minimized" #scrollContainer>
<router-outlet class="output-outlet" (activate)="onActivate($event, scrollContainer)"></router-outlet>
</div>
</div>

View File

@@ -13,7 +13,6 @@ export const routes: Routes = [
imports: [
SMSharedModule,
RouterModule.forChild(routes),
],
exports: [RouterModule, SelectableListComponent, SelectableFilterListComponent]
})

View File

@@ -21,7 +21,6 @@ import {SMMaterialModule} from '@common/shared/material/material.module';
import {ExperimentsCommonModule} from '@common/experiments/common-experiments.module';
import {CommonLayoutModule} from '@common/layout/layout.module';
import {EXPERIMENTS_STORE_KEY} from '@common/experiments/shared/common-experiments.const';
import {ExperimentInfoComponent} from './containers/experiment-info/experiment-info.component';
import {DebugImagesModule} from '@common/debug-images/debug-images.module';
import {ExperimentInfoExecutionComponent} from '@common/experiments/containers/experiment-info-execution/experiment-info-execution.component';
import {MatSidenavModule} from '@angular/material/sidenav';
@@ -29,6 +28,7 @@ import {MatListModule} from '@angular/material/list';
import {ExperimentOutputComponent} from './containers/experiment-ouptut/experiment-output.component';
import {merge, pick} from 'lodash/fp';
import {EXPERIMENTS_PREFIX} from '@common/experiments/actions/common-experiments-view.actions';
import {ExperimentInfoNavbarComponent} from './containers/experiment-info-navbar/experiment-info-navbar.component';
export const experimentSyncedKeys = [
@@ -90,9 +90,9 @@ const getExperimentsConfig = () => ({
],
declarations: [
ExperimentsComponent,
ExperimentInfoComponent,
ExperimentInfoExecutionComponent,
ExperimentOutputComponent,
ExperimentInfoNavbarComponent
],
providers: [
AdminService,

View File

@@ -1,16 +1,8 @@
export function isDeletableProject(readyForDeletion) {
return (readyForDeletion.experiments.unarchived + readyForDeletion.models.unarchived) === 0;
}
export const isDeletableProject = readyForDeletion => (readyForDeletion.experiments.unarchived + readyForDeletion.models.unarchived) === 0;
export function getDeletePopupEntitiesList(): string {
return 'experiments or models';
}
export const getDeletePopupEntitiesList = (): string => 'experiments or models';
export function getDeleteProjectPopupStatsBreakdown(readyForDeletion, statsSubset: 'archived' | 'unarchived' | 'total'): string {
return `${readyForDeletion.experiments[statsSubset] > 0 ? readyForDeletion.experiments[statsSubset] + ' experiments ' : ''}
export const getDeleteProjectPopupStatsBreakdown = (readyForDeletion, statsSubset: 'archived' | 'unarchived' | 'total', experimentCaption): string => `${readyForDeletion.experiments[statsSubset] > 0 ? `${readyForDeletion.experiments[statsSubset]} ${experimentCaption} ` : ''}
${readyForDeletion.models[statsSubset] > 0 ? readyForDeletion.models[statsSubset] + ' models ' : ''}`;
}
export function readyForDeletionFilter(readyForDeletion) {
return !(readyForDeletion.experiments === null || readyForDeletion.models === null);
}
export const readyForDeletionFilter = readyForDeletion => !(readyForDeletion.experiments === null || readyForDeletion.models === null);

View File

@@ -5,22 +5,26 @@ import {SMSharedModule} from '@common/shared/shared.module';
import {ProjectCardComponent} from '@common/shared/ui-components/panel/project-card/project-card.component';
import {ProjectCardMenuExtendedComponent} from '~/features/projects/containers/project-card-menu-extended/project-card-menu-extended.component';
import {ProjectCardMenuComponent} from '@common/shared/ui-components/panel/project-card-menu/project-card-menu.component';
import {PipelineCardComponent} from '../../../webapp-common/pipelines/pipeline-card/pipeline-card.component';
import {PipelineCardMenuComponent} from '../../../webapp-common/pipelines/pipeline-card-menu/pipeline-card-menu.component';
const _declarations = [
ProjectCardComponent,
ProjectCardMenuComponent,
ProjectCardMenuExtendedComponent
ProjectCardMenuExtendedComponent,
PipelineCardComponent,
PipelineCardMenuComponent,
];
@NgModule({
imports : [
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
SMSharedModule
],
declarations : [..._declarations],
exports : [..._declarations]
declarations: [..._declarations],
exports: [..._declarations]
})
export class ProjectsSharedModule {
}

View File

@@ -1,6 +1,6 @@
<sm-dialog-template>
<sm-dialog-template header="CREATE CREDENTIALS" iconClass="al-ico-access-key">
<sm-admin-dialog-template
[newCredential]="(newCredential$ | async) || {}"
(onCreateCredentials)="onCreateCredentials($event)"
[newCredential]="(newCredential$ | async)"
(updateLabel)="updateLabel($event)"
></sm-admin-dialog-template>
</sm-dialog-template>

View File

@@ -1,7 +1,7 @@
import {Component, Inject} from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import {Store} from '@ngrx/store';
import {createCredential} from '@common/core/actions/common-auth.actions';
import {updateCredentialLabel} from '@common/core/actions/common-auth.actions';
import {OrganizationGetUserCompaniesResponseCompanies} from '~/business-logic/model/organization/organizationGetUserCompaniesResponseCompanies';
import {Observable} from 'rxjs';
import {CredentialKeyExt, selectNewCredential} from '@common/core/reducers/common-auth-reducer';
@@ -21,7 +21,7 @@ import {CredentialKeyExt, selectNewCredential} from '@common/core/reducers/commo
this.newCredential$ = this.store.select(selectNewCredential);
}
onCreateCredentials({label}) {
this.store.dispatch(createCredential({workspace: this.data.workspace, label}));
updateLabel({credential, label}) {
this.store.dispatch(updateCredentialLabel({credential, label}));
}
}

View File

@@ -4,9 +4,11 @@
matTooltipPosition="above"></i>
</div>
<div class="section mb-4" *ngFor="let cred of credentials$ | async | keyvalue">
<sm-admin-credential-table [credentials]="cred?.value"
(credentialRevoked)="onCredentialRevoked($event)">
</sm-admin-credential-table>
<sm-admin-credential-table
[credentials]="cred?.value"
(updateCredentialLabel)="updateLabel($event)"
(credentialRevoked)="onCredentialRevoked($event)"
></sm-admin-credential-table>
<span
class="add-button d-flex align-items-center pointer"
[class.disabled]="creatingCredentials"

View File

@@ -1,6 +1,8 @@
@import "../../../../../webapp-common/shared/ui-components/styles/variables";
@import "variables";
:host {
max-width: 900px;
.title {
color: $white;
font-size: 16px;

View File

@@ -3,7 +3,7 @@ import {Observable, Subscription} from 'rxjs';
import {CredentialKeyExt, selectCredentials, selectNewCredential} from '@common/core/reducers/common-auth-reducer';
import {selectCurrentUser} from '@common/core/reducers/users-reducer';
import {filter, take} from 'rxjs/operators';
import {credentialRevoked, getAllCredentials, resetCredential} from '@common/core/actions/common-auth.actions';
import {createCredential, credentialRevoked, getAllCredentials, resetCredential, updateCredentialLabel} from '@common/core/actions/common-auth.actions';
import {Store} from '@ngrx/store';
import {GetCurrentUserResponseUserObject} from '~/business-logic/model/users/getCurrentUserResponseUserObject';
import {MatDialog} from '@angular/material/dialog';
@@ -41,19 +41,23 @@ export class UserCredentialsComponent implements OnInit, OnDestroy {
this.creatingCredentials = true;
this.dialog.open(CreateCredentialDialogComponent, {
data: {workspace : this.user.company},
width: '816px'
width: '788px'
}
).afterClosed().subscribe(() => {
this.creatingCredentials = false;
this.store.dispatch(resetCredential());
});
// this.store.dispatch(createCredential({workspace: this.user.company, openCredentialsPopup: true}));
this.store.dispatch(createCredential({workspace: this.user.company, openCredentialsPopup: true}));
}
onCredentialRevoked(accessKey) {
this.store.dispatch(credentialRevoked({accessKey, workspaceId: this.user.company.id}));
}
updateLabel({credential, label}) {
this.store.dispatch(updateCredentialLabel({credential: {...credential, company: this.user.company.id}, label}));
}
ngOnDestroy(): void {
this.newCredentialSub.unsubscribe();
}

View File

@@ -21,12 +21,12 @@ export const selectBreadcrumbsStringsBase = createSelector(
(project, experiment, model, projects) =>
({project, experiment, model, projects}) as IBreadcrumbs);
export const prepareNames = (data: IBreadcrumbs, noSubProjects?: boolean) => {
export const prepareNames = (data: IBreadcrumbs, isPipeline?: boolean, fullScreen = false) => {
const project = prepareLinkData(data.project, true);
if (data.project) {
const subProjects = [];
let subProjectsNames = [data.project?.name];
if (!noSubProjects) {
if (!isPipeline) {
subProjectsNames = data.project?.name?.split('/');
}
let currentName = '';
@@ -41,9 +41,11 @@ export const prepareNames = (data: IBreadcrumbs, noSubProjects?: boolean) => {
});
const subProjectsLinks = subProjects.map(subProject => ({
name: subProject?.name.substring(subProject?.name.lastIndexOf('/') + 1),
url: (noSubProjects || (subProject?.name === data.project?.name && data.project?.sub_projects?.length === 0)) ?
`projects/${subProject?.id}` :
`projects/${subProject?.id}/projects`
url: isPipeline ? `pipelines/${subProject?.id}/experiments` :
fullScreen ? `projects/${subProject?.id}/experiments/${data.experiment.id}` :
subProject?.name === data.project?.name && data.project?.sub_projects?.length === 0 ?
`projects/${subProject?.id}` :
`projects/${subProject?.id}/projects`
})) as { name: string; url: string }[];
project.name = project?.name.substring(project.name.lastIndexOf('/') + 1);
project.subCrumbs = subProjectsLinks;
@@ -56,7 +58,7 @@ export const prepareNames = (data: IBreadcrumbs, noSubProjects?: boolean) => {
const models = formatStaticCrumb('models');
const compare = formatStaticCrumb('compare-experiments');
return {
...(project.url !=='*' && {':projectId': project}),
...(project.url !== '*' && {':projectId': project}),
':taskId' : task,
':controllerId': experiment,
'compare-experiments': compare,

View File

@@ -2,7 +2,7 @@
@font-face {
font-family: '#{$icomoon-font-family}';
src: url('./#{$icomoon-font-family}.ttf?medlz4') format('truetype');
src: url('./#{$icomoon-font-family}.ttf?2i0eh5') format('truetype');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -1166,9 +1166,9 @@
content: $al-ico-greater-than;
}
}
.al-ico-toogle-graph {
.al-ico-eye-outline {
&:before {
content: $al-ico-toogle-graph;
content: $al-ico-eye-outline;
}
}
.al-ico-status-draft {

View File

@@ -225,7 +225,7 @@ $al-ico-code-square: "\e9c4";
$al-ico-video: "\e9c5";
$al-ico-less-than: "\e9c6";
$al-ico-greater-than: "\e9c7";
$al-ico-toogle-graph: "\e9c8";
$al-ico-eye-outline: "\e9c8";
$al-ico-status-draft: "\e902";
$al-ico-status-published: "\e906";
$al-ico-status-aborted-sec: "\e918";

View File

@@ -449,7 +449,7 @@ body .clean-list {
box-shadow: 0 -2px 8px 0 rgba(0, 0, 0, 0.2);
font-family: 'Heebo', sans-serif;
font-size: 11px;
line-height: 1.45;
line-height: 1.55;
letter-spacing: 0.3px;
color: #ffffff;
word-break: break-word;
@@ -465,11 +465,6 @@ body .clean-list {
}
}
.mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
line-height: 30px !important;
font-size: 11px !important;
}
// --------------------------------old------------------------------------------
@import "shared/ui-components/styles/index";

View File

@@ -12,6 +12,16 @@ export const createCredential = createAction(
AUTH_PREFIX + 'CREATE_CREDENTIAL (API)',
props<{workspace: GetCurrentUserResponseUserObjectCompany; openCredentialsPopup?: boolean; label?: string}>()
);
export const updateCredentialLabel = createAction(
AUTH_PREFIX + 'UPDATE_CREDENTIAL_LABEL',
props<{credential: CredentialKeyExt; label?: string}>()
);
export const setCredentialLabel = createAction(
AUTH_PREFIX + 'SET_CREDENTIAL_LABEL',
props<{credential: CredentialKeyExt; label?: string}>()
);
export const addCredential = createAction(
AUTH_PREFIX + 'ADD_CREDENTIAL',
props<{ newCredential: CredentialKeyExt; workspaceId: string }>()
@@ -57,3 +67,7 @@ export const setSignedUrl = createAction(
props<{url: string; signed: string; expires: number}>()
);
export const removeSignedUrl = createAction(
AUTH_PREFIX + '[remove signed url]',
props<{url: string}>()
);

View File

@@ -82,3 +82,7 @@ export const openAppsAwarenessDialog = createAction(VIEW_PREFIX + '[apps awarene
props<{appsYouTubeIntroVideoId}>()
);
export const toggleUserFocus = createAction(
VIEW_PREFIX + '[toggle user focus in header',
props<{show: boolean}>()
);

View File

@@ -13,6 +13,7 @@ import {TasksStopManyResponse} from '~/business-logic/model/tasks/tasksStopManyR
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
import {MetricColumn} from '@common/shared/utils/tableParamEncode';
import {ProjectStatsGraphData} from '@common/core/reducers/projects.reducer';
import {User} from '~/business-logic/model/users/user';
export const PROJECTS_PREFIX = '[ROOT_PROJECTS] ';
@@ -25,8 +26,6 @@ export const getAllSystemProjects = createAction(
PROJECTS_PREFIX + 'GET_PROJECTS'
);
export const updateProject = createAction(
PROJECTS_PREFIX + 'UPDATE_PROJECT',
props<{ id: string; changes: Partial<ProjectsUpdateRequest> }>()
@@ -136,3 +135,24 @@ export const setGraphData = createAction(
PROJECTS_PREFIX + '[set project stats]',
props<{ stats: ProjectStatsGraphData[] }>()
);
export const getProjectUsers = createAction(
PROJECTS_PREFIX + '[get current project users]',
props<{projectId: string}>()
);
export const setProjectUsers = createAction(
PROJECTS_PREFIX + '[set current project users]',
props<{users: User[]}>()
);
export const setAllProjectUsers = createAction(
PROJECTS_PREFIX + '[set all projects users]',
props<{users: User[]}>()
);
export const setProjectExtraUsers = createAction(
PROJECTS_PREFIX + '[set extra users]',
props<{users: User[]}>()
);
export const getFilteredUsers = createAction(
PROJECTS_PREFIX + 'GET_FILTERED_USERS',
props<{filteredUsers: string[]}>()
);

View File

@@ -15,6 +15,7 @@ import {EMPTY, of} from 'rxjs';
import {SignResponse} from '@common/settings/admin/base-admin.service';
import {S3AccessResolverComponent} from '@common/layout/s3-access-resolver/s3-access-resolver.component';
import {MatDialog} from '@angular/material/dialog';
import {setCredentialLabel} from '../actions/common-auth.actions';
@Injectable()
export class CommonAuthEffects {
@@ -74,6 +75,20 @@ export class CommonAuthEffects {
))
));
updateCredentialLabel = createEffect(() => this.actions.pipe(
ofType(authActions.updateCredentialLabel),
mergeMap(action => this.credentialsApi.authEditCredentials({access_key: action.credential.access_key, label: action.label}).pipe(
mergeMap(() => [
setCredentialLabel({credential: action.credential, label: action.label}),
deactivateLoader(action.type)
]),
catchError(error => [
requestFailed(error),
setServerError(error, null, 'Unable to update credentials'),
deactivateLoader(action.type)])
))
));
signUrl = createEffect(() => this.actions.pipe(
ofType(authActions.getSignedUrl),
filter(action => !!action.url),
@@ -83,7 +98,7 @@ export class CommonAuthEffects {
this.store.select(state => selectSignedUrl(action.url)(state))
),
switchMap(([, signedUrl]) =>
(!signedUrl?.expires || signedUrl.expires < (new Date()).getTime()) ?
(!signedUrl?.expires || signedUrl.expires < (new Date()).getTime() || action.config?.disableCache) ?
this.adminService.signUrlIfNeeded(action.url, action.config) : of({type: 'none'})
),
filter(res => !!res),

View File

@@ -3,18 +3,6 @@ import {Action, Store} from '@ngrx/store';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {ApiProjectsService} from '~/business-logic/api-services/projects.service';
import * as actions from '../actions/projects.actions';
import {
fetchGraphData, getAllSystemProjects,
getCompanyTags, getProjectsTags,
getTags,
openMoreInfoPopup,
openTagColorsMenu, refetchProjects,
resetProjects, resetProjectSelection,
setCompanyTags,
setGraphData, setLastUpdate,
setTags
} from '../actions/projects.actions';
import {catchError, filter, map, mergeMap, switchMap, withLatestFrom} from 'rxjs/operators';
import {requestFailed} from '../actions/http.actions';
import {activeLoader, deactivateLoader, setServerError} from '../actions/layout.actions';
@@ -24,14 +12,18 @@ import {MatDialog} from '@angular/material/dialog';
import {ApiOrganizationService} from '~/business-logic/api-services/organization.service';
import {OrganizationGetTagsResponse} from '~/business-logic/model/organization/organizationGetTagsResponse';
import {selectRouterParams} from '../reducers/router-reducer';
import {forkJoin} from 'rxjs';
import {forkJoin, of} from 'rxjs';
import {ProjectsGetTaskTagsResponse} from '~/business-logic/model/projects/projectsGetTaskTagsResponse';
import {ProjectsGetModelTagsResponse} from '~/business-logic/model/projects/projectsGetModelTagsResponse';
import {selectLastUpdate, selectSelectedMetricVariantForCurrProject, selectSelectedProjectId} from '../reducers/projects.reducer';
import {
selectAllProjectsUsers,
selectLastUpdate,
selectSelectedMetricVariantForCurrProject,
selectSelectedProjectId
} from '../reducers/projects.reducer';
import {OperationErrorDialogComponent} from '@common/shared/ui-components/overlay/operation-error-dialog/operation-error-dialog.component';
import {ApiTasksService} from '~/business-logic/api-services/tasks.service';
import {createMetricColumn} from '@common/shared/utils/tableParamEncode';
import {get} from 'lodash/fp';
import {ITask} from '~/business-logic/model/al-task';
import {TasksGetAllExRequest} from '~/business-logic/model/tasks/tasksGetAllExRequest';
import {setSelectedExperiments} from '../../experiments/actions/common-experiments-view.actions';
@@ -39,6 +31,8 @@ import {selectShowHidden} from '~/features/projects/projects.reducer';
import {setActiveWorkspace} from '@common/core/actions/users.actions';
import {ProjectsGetAllExResponse} from '~/business-logic/model/projects/projectsGetAllExResponse';
import {Project} from '~/business-logic/model/projects/project';
import {ApiUsersService} from '~/business-logic/api-services/users.service';
import { get } from 'lodash/fp';
export const ALL_PROJECTS_OBJECT = {id: '*', name: 'All Experiments'};
@@ -50,9 +44,9 @@ export class ProjectsEffects {
constructor(
private actions$: Actions, private projectsApi: ApiProjectsService, private orgApi: ApiOrganizationService,
private store: Store<any>, private dialog: MatDialog, private tasksApi: ApiTasksService
) {
}
private store: Store<any>, private dialog: MatDialog, private tasksApi: ApiTasksService,
private usersApi: ApiUsersService,
) {}
activeLoader = createEffect(() => this.actions$.pipe(
ofType(actions.setSelectedProjectId),
@@ -77,9 +71,9 @@ export class ProjectsEffects {
if (res.projects.length >= this.pageSize) {
this.scrollId = res.scroll_id;
this.lastUpdateSoFar = res.projects[res.projects.length - 1].last_update;
resultsActions.push(getAllSystemProjects());
resultsActions.push(actions.getAllSystemProjects());
} else {
resultsActions.push(setLastUpdate({lastUpdate: res.projects[res.projects.length - 1]?.last_update || this.lastUpdateSoFar || lastUpdate}));
resultsActions.push(actions.setLastUpdate({lastUpdate: res.projects[res.projects.length - 1]?.last_update || this.lastUpdateSoFar || lastUpdate}));
this.scrollId = null;
this.lastUpdateSoFar = null;
}
@@ -91,11 +85,11 @@ export class ProjectsEffects {
resetProjects$ = createEffect(() => this.actions$.pipe(
ofType(actions.resetSelectedProject),
mergeMap(() => [resetProjectSelection()])
mergeMap(() => [actions.resetProjectSelection()])
));
resetProjectSelections$ = createEffect(() => this.actions$.pipe(
ofType(resetProjectSelection),
ofType(actions.resetProjectSelection),
mergeMap(() => [setSelectedExperiments({experiments: []}), setSelectedModels({models: []})])
));
@@ -117,7 +111,7 @@ export class ProjectsEffects {
));
openTagColor = createEffect(() => this.actions$.pipe(
ofType(openTagColorsMenu),
ofType(actions.openTagColorsMenu),
map(() => {
this.dialog.open(TagColorMenuComponent);
})
@@ -125,29 +119,29 @@ export class ProjectsEffects {
//getAll but not projects'
getAllTags = createEffect(() => this.actions$.pipe(
ofType(getCompanyTags),
ofType(actions.getCompanyTags),
// eslint-disable-next-line @typescript-eslint/naming-convention
switchMap(() => this.orgApi.organizationGetTags({include_system: true})
.pipe(
map((res: OrganizationGetTagsResponse) => setCompanyTags({tags: res.tags, systemTags: res.system_tags})),
map((res: OrganizationGetTagsResponse) => actions.setCompanyTags({tags: res.tags, systemTags: res.system_tags})),
catchError(error => [requestFailed(error)])
)
)
));
getProjectsTags = createEffect(() => this.actions$.pipe(
ofType(getProjectsTags),
ofType(actions.getProjectsTags),
// eslint-disable-next-line @typescript-eslint/naming-convention
switchMap(() => this.projectsApi.projectsGetProjectTags({filter: {system_tags: ['pipeline']}})
.pipe(
map((res: OrganizationGetTagsResponse) => setTags({tags: res.tags})),
map((res: OrganizationGetTagsResponse) => actions.setTags({tags: res.tags})),
catchError(error => [requestFailed(error)])
)
)
));
getTagsEffect = createEffect(() => this.actions$.pipe(
ofType(getTags),
ofType(actions.getTags),
withLatestFrom(this.store.select(selectRouterParams).pipe(
map(params => (params === null || params?.projectId === '*') ? [] : [params.projectId]))),
switchMap(([action, projects]) => forkJoin([
@@ -157,7 +151,7 @@ export class ProjectsEffects {
map((res: [ProjectsGetTaskTagsResponse, ProjectsGetModelTagsResponse]) =>
Array.from(new Set(res[0].tags.concat(res[1].tags))).sort()),
mergeMap((tags: string[]) => [
setTags({tags}),
actions.setTags({tags}),
deactivateLoader(action.type)
]),
catchError(error => [
@@ -169,7 +163,7 @@ export class ProjectsEffects {
));
openMoreInfoPopupEffect = createEffect(() => this.actions$.pipe(
ofType(openMoreInfoPopup),
ofType(actions.openMoreInfoPopup),
switchMap(action => this.dialog.open(OperationErrorDialogComponent, {
data: {
title: `${action.operationName} ${action.entityType}`,
@@ -181,7 +175,7 @@ export class ProjectsEffects {
), {dispatch: false});
fetchProjectStats = createEffect(() => this.actions$.pipe(
ofType(fetchGraphData),
ofType(actions.fetchGraphData),
withLatestFrom(
this.store.select(selectSelectedProjectId),
this.store.select(selectSelectedMetricVariantForCurrProject)
@@ -204,7 +198,7 @@ export class ProjectsEffects {
/* eslint-enable @typescript-eslint/naming-convention */
} as unknown as TasksGetAllExRequest).pipe(
map((res) =>
setGraphData({
actions.setGraphData({
stats: res.tasks.map((task: ITask) => {
const started = new Date(task.started).getTime();
const end = started + (task.active_duration ?? 0) * 1000;
@@ -224,12 +218,66 @@ export class ProjectsEffects {
));
resetRootProjects = createEffect(() => this.actions$.pipe(
ofType(setActiveWorkspace, refetchProjects),
ofType(setActiveWorkspace, actions.refetchProjects),
mergeMap(() => [
resetProjects(),
getAllSystemProjects()
actions.resetProjects(),
actions.getAllSystemProjects()
])
));
getAllProjectsUsersEffect = createEffect(() => this.actions$.pipe(
ofType(actions.getAllSystemProjects),
switchMap(() => this.usersApi.usersGetAllEx({
order_by: ['name'],
only_fields: ['name'],
}, null, 'body', true).pipe(
mergeMap(res => [actions.setAllProjectUsers(res)]),
catchError(error => [
requestFailed(error),
setServerError(error, null, 'Fetch all projects users failed')]
)
))
));
getUsersEffect = createEffect(() => this.actions$.pipe(
ofType(actions.getProjectUsers),
withLatestFrom(
this.store.select(selectAllProjectsUsers)
),
switchMap(([action, all]) => (!action.projectId || action.projectId === '*' ?
of({users: all}) :
this.usersApi.usersGetAllEx({
order_by: ['name'],
only_fields: ['name'],
active_in_projects: [action.projectId]
}, null, 'body', true)).pipe(
mergeMap(res => [actions.setProjectUsers(res)]),
catchError(error => [
requestFailed(error),
setServerError(error, null, 'Fetch users failed')]
)
))
));
getExtraUsersEffect = createEffect(() => this.actions$.pipe(
ofType(actions.getFilteredUsers),
switchMap(action => this.usersApi.usersGetAllEx({
order_by: ['name'],
only_fields: ['name'],
id: action.filteredUsers || []
}, null, 'body', true).pipe(
mergeMap(res => [
actions.setProjectExtraUsers(res),
deactivateLoader(action.type)
]),
catchError(error => [
requestFailed(error),
deactivateLoader(action.type),
setServerError(error, null, 'Fetch users failed')]
)
))
));
}

View File

@@ -2,16 +2,16 @@ import {createSelector, on, ReducerTypes, select, Store} from '@ngrx/store';
import {
addCredential,
cancelS3Credentials,
removeCredential, resetCredential,
removeCredential, removeSignedUrl, resetCredential,
resetDontShowAgainForBucketEndpoint,
saveS3Credentials, setSignedUrl,
saveS3Credentials, setCredentialLabel, setSignedUrl,
showLocalFilePopUp,
updateAllCredentials,
updateS3Credential
} from '../actions/common-auth.actions';
import {CredentialKey} from '~/business-logic/model/auth/credentialKey';
import {inBucket} from '@common/settings/admin/base-admin.service';
import {filter, map, timeoutWith} from 'rxjs/operators';
import {filter, map, takeWhile, timeoutWith} from 'rxjs/operators';
export interface Credentials {
Bucket?: string;
@@ -68,7 +68,10 @@ export const getSignedUrlOrOrigin$ = (url: string, store: Store) => store.pipe(
filter(signed => !!signed?.signed),
map(signed => signed?.signed),
timeoutWith(900, store.select(selectSignedUrl(url))
.pipe(map(signed => signed?.signed || url))
.pipe(
takeWhile( signed => signed !== null),
map(signed => signed?.signed || url)
)
),
);
@@ -103,7 +106,8 @@ export const commonAuthReducer = [
}
}),
on(resetCredential, state => ({...state, newCredential: initAuth.newCredential})),
on(addCredential, (state, action) => ({ ...state,
on(addCredential, (state, action) => ({
...state,
newCredential: {...action.newCredential, company: action.workspaceId},
credentials: {
...state.credentials,
@@ -111,6 +115,14 @@ export const commonAuthReducer = [
...(state.credentials[action.workspaceId] || []),
...(Object.keys(action.newCredential).length > 0 ? [{...action.newCredential, company: action.workspaceId}] : [])
]}})),
on(setCredentialLabel, (state, action) => ({
...state,
newCredential: {...state.newCredential, label: action.label},
credentials: {
...state.credentials,
[action.credential.company]: state.credentials[action.credential.company]?.map(cred =>
cred.access_key === action.credential.access_key ? {...action.credential, label: action.label} : cred)
}})),
on(removeCredential, (state, action) => ({ ...state, credentials: {
...state.credentials,
[action.workspaceId]: state.credentials[action.workspaceId].filter((cred => cred.access_key !== action.accessKey))
@@ -119,4 +131,5 @@ export const commonAuthReducer = [
...state,
credentials: {[action.credentials[0]?.company || action.workspace]: action.credentials, ...action.extra}, revokeSucceed: false})),
on(setSignedUrl, (state, action) => ({...state, signedUrls: {...state.signedUrls, [action.url]: {signed: action.signed, expires: action.expires}}})),
on(removeSignedUrl, (state, action) => ({...state, signedUrls: {...state.signedUrls, [action.url]: null}})),
] as ReducerTypes<AuthState, any>[];

View File

@@ -7,6 +7,7 @@ import {ITableExperiment} from '../../experiments/shared/common-experiment-model
import {MetricColumn} from '@common/shared/utils/tableParamEncode';
import {sortByField} from '@common/tasks/tasks.utils';
import {ProjectsGetAllResponseSingle} from '~/business-logic/model/projects/projectsGetAllResponseSingle';
import {User} from '~/business-logic/model/users/user';
export interface ProjectStatsGraphData {
@@ -32,6 +33,9 @@ export interface RootProjects {
graphVariant: { [project: string]: MetricColumn };
graphData: ProjectStatsGraphData[];
lastUpdate: string;
users: User[];
allUsers: User[];
extraUsers: User[];
}
const initRootProjects: RootProjects = {
@@ -46,7 +50,10 @@ const initRootProjects: RootProjects = {
tagsFilterByProject: true,
graphVariant: {},
graphData: null,
lastUpdate: null
lastUpdate: null,
users: [],
allUsers: [],
extraUsers: [],
};
export const projects = state => state.rootProjects as RootProjects;
@@ -72,6 +79,11 @@ export const selectSelectedMetricVariantForCurrProject = createSelector(
selectSelectedProjectsMetricVariant, selectSelectedProjectId,
(projectsVariant, projectId) => projectsVariant[projectId]);
export const selectGraphData = createSelector(projects, state => state.graphData);
export const selectProjectUsers = createSelector(projects, state => state.extraUsers.length ?
Array.from(new Set([...state.users, ...state.extraUsers])) :
state.users
);
export const selectAllProjectsUsers = createSelector(projects, state => state.allUsers);
export const projectsReducer = createReducer(
initRootProjects,
@@ -101,12 +113,12 @@ export const projectsReducer = createReducer(
graphData: initRootProjects.graphData,
};
}),
on(projectsActions.setSelectedProject, (state, action) => ({...state, selectedProject: action.project})),
on(projectsActions.setSelectedProject, (state, action) => ({...state, selectedProject: action.project, extraUsers: []})),
on(projectsActions.deletedProjectFromRoot, (state, action) => {
const projectIdsToDelete = [action.project.id].concat(action.project.sub_projects.map(project=> project.id))
return {...state, projects: state.projects.filter(project=> !projectIdsToDelete.includes(project.id))};
}),
on(projectsActions.resetSelectedProject, state => ({...state, selectedProject: initRootProjects.selectedProject})),
on(projectsActions.resetSelectedProject, state => ({...state, selectedProject: initRootProjects.selectedProject, users: [], extraUsers: []})),
on(projectsActions.updateProjectCompleted, (state, action) => ({
...state,
selectedProject: {...state.selectedProject, ...action.changes},
@@ -124,4 +136,7 @@ export const projectsReducer = createReducer(
})),
on(projectsActions.setGraphData, (state, action) => ({...state, graphData: action.stats})),
on(projectsActions.setLastUpdate, (state, action) => ({...state, lastUpdate: action.lastUpdate})),
on(projectsActions.setProjectUsers, (state, action) => ({...state, users: action.users, extraUsers: []})),
on(projectsActions.setAllProjectUsers, (state, action) => ({...state, allUsers: action.users})),
on(projectsActions.setProjectExtraUsers, (state, action) => ({...state, extraUsers: action.users})),
);

View File

@@ -18,6 +18,7 @@ export interface ViewState {
plotlyReady: boolean;
aceReady: boolean;
preferencesReady: boolean;
showUserFocus: boolean;
}
export const initViewState: ViewState = {
@@ -36,7 +37,8 @@ export const initViewState: ViewState = {
neverShowPopupAgain: [],
plotlyReady: false,
aceReady: false,
preferencesReady: false
preferencesReady: false,
showUserFocus: false,
};
export const views = state => state.views as ViewState;
@@ -56,6 +58,7 @@ export const selectFirstLoginAt = createSelector(views, state => state.firstLogi
export const selectPlotlyReady = createSelector(views, state => state.plotlyReady);
export const selectAceReady = createSelector(views, state => state.aceReady);
export const selectNeverShowPopups = createSelector(views, (state): string[] => state.neverShowPopupAgain);
export const selectShowUserFocus = createSelector(views, state => state.showUserFocus);
export const viewReducers = [
@@ -90,6 +93,7 @@ export const viewReducers = [
...state,
neverShowPopupAgain: action.reset ? state.neverShowPopupAgain.filter(popups => popups !== action.popupId) : Array.from(new Set([...state.neverShowPopupAgain, action.popupId]))
})),
on(layoutActions.toggleUserFocus, (state, action) => ({...state, showUserFocus: action.show}))
] as ReducerTypes<ViewState, any>[];
export const viewReducer = createReducer(

View File

@@ -1,9 +1,9 @@
import {Action, createAction, props} from '@ngrx/store';
import {ISmAction} from '../core/models/actions';
import {SEARCH_ACTIONS} from './dashboard-search.consts';
import {Project} from '../../business-logic/model/projects/project';
import {Task} from '../../business-logic/model/tasks/task';
import {Model} from '../../business-logic/model/models/model';
import {Project} from '~/business-logic/model/projects/project';
import {Task} from '~/business-logic/model/tasks/task';
import {Model} from '~/business-logic/model/models/model';
export const searchSetTerm = createAction(
@@ -37,6 +37,16 @@ export const searchProjects = createAction(
props<{query: string; regExp?: boolean}>()
);
export const searchPipelines = createAction(
SEARCH_ACTIONS.SEARCH_PIPELINES,
props<{query: string; regExp?: boolean}>()
);
export const setPipelinesResults = createAction(
'Set Pipelines Results',
props<{pipelines: Project[]}>()
);
export class SetProjectsResults implements ISmAction {
public type = SEARCH_ACTIONS.SET_PROJECTS;
public payload: { projects: Array<Project> };

View File

@@ -5,6 +5,7 @@ export const SEARCH_ACTIONS = {
SET_TERM : SEARCH_PREFIX + 'SET_TERM',
SEARCH_START : SEARCH_PREFIX + 'SEARCH_START',
SEARCH_PROJECTS : SEARCH_PREFIX + 'SEARCH_PROJECTS',
SEARCH_PIPELINES : SEARCH_PREFIX + 'SEARCH_PIPELINES',
SEARCH_EXPERIMENTS: SEARCH_PREFIX + 'SEARCH_EXPERIMENTS',
SEARCH_MODELS : SEARCH_PREFIX + 'SEARCH_MODELS',
SEARCH_COMPLETE : SEARCH_PREFIX + 'SEARCH_COMPLETE',

View File

@@ -2,7 +2,17 @@ import {Injectable} from '@angular/core';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {activeLoader, deactivateLoader} from '../core/actions/layout.actions';
import {
SearchActivate, SearchClear, searchExperiments, searchModels, searchProjects, searchSetTerm, searchStart, SetExperimentsResults, SetModelsResults, SetProjectsResults
SearchActivate,
SearchClear,
searchExperiments,
searchModels,
searchPipelines,
searchProjects,
searchSetTerm,
searchStart,
SetExperimentsResults,
SetModelsResults, setPipelinesResults,
SetProjectsResults
} from './dashboard-search.actions';
import {EXPERIMENT_SEARCH_ONLY_FIELDS, SEARCH_ACTIONS, SEARCH_PAGE_SIZE} from './dashboard-search.consts';
import {ApiProjectsService} from '~/business-logic/api-services/projects.service';
@@ -28,7 +38,7 @@ export class DashboardSearchEffects {
/* eslint-disable @typescript-eslint/naming-convention */
activeLoader = createEffect(() => this.actions.pipe(
ofType(SEARCH_ACTIONS.SEARCH_PROJECTS, SEARCH_ACTIONS.SEARCH_MODELS, SEARCH_ACTIONS.SEARCH_EXPERIMENTS),
ofType(SEARCH_ACTIONS.SEARCH_PROJECTS, SEARCH_ACTIONS.SEARCH_MODELS, SEARCH_ACTIONS.SEARCH_EXPERIMENTS, SEARCH_ACTIONS.SEARCH_PIPELINES),
map(action => activeLoader(action.type))
));
// add actions for each search
@@ -43,6 +53,7 @@ export class DashboardSearchEffects {
}
actionsToFire.push(searchSetTerm(action));
actionsToFire.push(searchProjects(action));
actionsToFire.push(searchPipelines(action));
actionsToFire.push(searchExperiments(action));
actionsToFire.push(searchModels(action));
return actionsToFire;
@@ -56,16 +67,37 @@ export class DashboardSearchEffects {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query) + '[^/]*$'}),
fields: ['name', 'id']
},
include_stats_filter: {system_tags: ['-pipeline']},
stats_for_state: ProjectsGetAllExRequest.StatsForStateEnum.Active,
scroll_id: null,
size: SEARCH_PAGE_SIZE,
include_stats: true,
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination']
}).pipe(
} as ProjectsGetAllExRequest).pipe(
mergeMap(res => [new SetProjectsResults(res.projects), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])))
));
searchPipelines = createEffect(() => this.actions.pipe(
ofType(searchPipelines.type),
switchMap((action: ReturnType<typeof searchPipelines>) => this.projectsApi.projectsGetAllEx({
_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query) + '[^/]*$'}),
fields: ['name', 'id']
},
search_hidden: true,
shallow_search: false,
system_tags: ['pipeline'],
stats_for_state: ProjectsGetAllExRequest.StatsForStateEnum.Active,
scroll_id: null,
size: SEARCH_PAGE_SIZE,
include_stats: true,
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination', 'tags', 'system_tags']
} as ProjectsGetAllExRequest).pipe(
mergeMap(res => [setPipelinesResults({pipelines:res.projects}), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])))
));
searchModels = createEffect(() => this.actions.pipe(
ofType(searchModels.type),
switchMap((action: ReturnType<typeof searchModels>) => this.modelsApi.modelsGetAllEx({

View File

@@ -4,13 +4,14 @@ import {Task} from '../../business-logic/model/tasks/task';
import {createFeatureSelector, createSelector} from '@ngrx/store';
import {SEARCH_ACTIONS} from './dashboard-search.consts';
import {Model} from '../../business-logic/model/models/model';
import {searchSetTerm} from './dashboard-search.actions';
import {searchSetTerm, setPipelinesResults} from './dashboard-search.actions';
import {ICommonSearchState} from '../common-search/common-search.reducer';
export interface ISearchState {
projects: Project[];
experiments: Task[];
models: Model[];
pipelines: Project[];
users: User[];
resultsCounter: number;
term: ICommonSearchState['searchQuery'];
@@ -20,14 +21,15 @@ export interface ISearchState {
export const searchInitialState: ISearchState = {
term : null,
term: null,
forceSearch: false,
projects : [],
users : [],
experiments : [],
models : [],
projects: [],
pipelines: [],
users: [],
experiments: [],
models: [],
resultsCounter: 0,
active : false
active: false
};
export function dashboardSearchReducer<ActionReducer>(state: ISearchState = searchInitialState, action) {
@@ -42,6 +44,8 @@ export function dashboardSearchReducer<ActionReducer>(state: ISearchState = sear
}
case SEARCH_ACTIONS.SET_PROJECTS:
return {...state, projects: action.payload.projects, resultsCounter: state.resultsCounter + 1};
case setPipelinesResults.type:
return {...state, pipelines: action.pipelines, resultsCounter: state.resultsCounter + 1};
case SEARCH_ACTIONS.SET_EXPERIMENTS:
return {...state, experiments: action.payload.experiments, resultsCounter: state.resultsCounter + 1};
case SEARCH_ACTIONS.SET_MODELS:
@@ -60,10 +64,11 @@ export function dashboardSearchReducer<ActionReducer>(state: ISearchState = sear
}
export const selectSearch = createFeatureSelector<ISearchState>('search');
export const selectProjectsResults = createSelector(selectSearch, (state: ISearchState): Array<Project> => state.projects);
export const selectSearch = createFeatureSelector<ISearchState>('search');
export const selectProjectsResults = createSelector(selectSearch, (state: ISearchState): Array<Project> => state.projects);
export const selectExperimentsResults = createSelector(selectSearch, (state: ISearchState): Array<Task> => state.experiments);
export const selectModelsResults = createSelector(selectSearch, (state: ISearchState): Array<Model> => state.models);
export const selectActiveSearch = createSelector(selectSearch, (state: ISearchState): boolean => state.term?.query?.length >= 3 || state.forceSearch);
export const selectSearchTerm = createSelector(selectSearch, (state: ISearchState) => state.term);
export const selectResultsCounter = createSelector(selectSearch, (state: ISearchState): number => state.resultsCounter);
export const selectModelsResults = createSelector(selectSearch, (state: ISearchState): Array<Model> => state.models);
export const selectPipelinesResults = createSelector(selectSearch, (state: ISearchState): Array<Project> => state.pipelines);
export const selectActiveSearch = createSelector(selectSearch, (state: ISearchState): boolean => state.term?.query?.length >= 3 || state.forceSearch);
export const selectSearchTerm = createSelector(selectSearch, (state: ISearchState) => state.term);
export const selectResultsCounter = createSelector(selectSearch, (state: ISearchState): number => state.resultsCounter);

View File

@@ -1,7 +0,0 @@
<div class="sm-card-list-layout">
<sm-experiment-card
*ngFor="let experiment of experimentsList"
[experiment]="experiment"
(experimentCardClicked)="experimentCardClicked($event)"
></sm-experiment-card>
</div>

View File

@@ -1,18 +0,0 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Task} from '../../../../business-logic/model/tasks/task';
import {ITask} from '../../../../business-logic/model/al-task';
@Component({
selector : 'sm-experiments-search-results',
templateUrl: './experiments-search-results.component.html',
styleUrls : ['./experiments-search-results.component.scss']
})
export class ExperimentsSearchResultsComponent {
@Input() experimentsList: Array<Task>;
@Output() experimentClicked = new EventEmitter<ITask>();
public experimentCardClicked(experiment: ITask) {
this.experimentClicked.emit(experiment);
}
}

View File

@@ -1,7 +0,0 @@
<div class="sm-card-list-layout">
<sm-model-card
*ngFor="let model of modelsList"
[model]="model"
(modelCardClicked)="modelCardClicked($event)"
></sm-model-card>
</div>

View File

@@ -1,17 +0,0 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Model} from '../../../../business-logic/model/models/model';
@Component({
selector : 'sm-models-search-results',
templateUrl: './models-search-results.component.html',
styleUrls : ['./models-search-results.component.scss']
})
export class ModelsSearchResultsComponent {
@Input() modelsList: Array<Model>;
@Output() modelClicked = new EventEmitter<Model>();
public modelCardClicked(model: Model) {
this.modelClicked.emit(model);
}
}

View File

@@ -1,8 +0,0 @@
<div class="sm-card-list-layout">
<sm-project-card
*ngFor="let project of projectsList"
[project]="project"
(projectCardClicked)="projectCardClicked($event)"
[hideMenu]="true"
></sm-project-card>
</div>

View File

@@ -1,17 +0,0 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Project} from '../../../../business-logic/model/projects/project';
@Component({
selector : 'sm-projects-search-results',
templateUrl: './projects-search-results.component.html',
styleUrls : ['./projects-search-results.component.scss']
})
export class ProjectsSearchResultsComponent {
@Input() projectsList: Array<Project>;
@Output() projectClicked = new EventEmitter<Project>();
public projectCardClicked(project: Project) {
this.projectClicked.emit(project);
}
}

View File

@@ -0,0 +1,7 @@
<cdk-virtual-scroll-viewport class="card-container" [itemSize]="cardHeight + 32">
<div class="card-row" [style.width.px]="rowWidth" *cdkVirtualFor="let row of resultRows$ | async">
<ng-container *ngFor="let result of row; trackBy: trackById">
<ng-container *ngTemplateOutlet="cardTemplate; context: {result}"></ng-container>
</ng-container>
</div>
</cdk-virtual-scroll-viewport>

View File

@@ -0,0 +1,15 @@
:host {
.card-container {
height: 100%;
padding: 0 24px 24px;
.card-row {
display: grid;
grid-template-columns: repeat(auto-fit, 352px);
grid-gap: 32px 24px;
margin: 0 auto;
padding: 16px 0;
}
}
}

View File

@@ -0,0 +1,54 @@
import {Component, EventEmitter, Input, Output, TemplateRef, ViewChild} from '@angular/core';
import {BehaviorSubject, combineLatest, Observable} from 'rxjs';
import {BreakpointObserver, BreakpointState} from '@angular/cdk/layout';
import {map, take} from 'rxjs/operators';
import {chunk} from 'lodash/fp';
import {trackById} from '@common/shared/utils/forms-track-by';
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
import {Store} from '@ngrx/store';
import {selectScaleFactor} from '@common/core/reducers/view.reducer';
const SIDE_NAV_PLUS_PAD = 64 + 24 + 24;
const CARD_WIDTH = 352;
@Component({
selector: 'sm-search-results',
templateUrl: './search-results.component.html',
styleUrls: ['./search-results.component.scss']
})
export class SearchResultsComponent {
private cardLayoutChange$: Observable<BreakpointState>;
private results$ = new BehaviorSubject<any[]>(null);
public resultRows$: Observable<any[][]>;
public trackById = trackById;
public rowWidth = 300;
@Input() cardTemplate: TemplateRef<any>;
@Input() set results(results: any[]) {
this.results$.next(results);
this.viewPort?.scrollToIndex(0);
}
@Input() cardHeight = 246;
@Output() resultClicked = new EventEmitter<any>();
@ViewChild(CdkVirtualScrollViewport) viewPort : CdkVirtualScrollViewport;
constructor(private store: Store, private breakpointObserver: BreakpointObserver) {
this.store.select(selectScaleFactor)
.pipe(take(1), map(factor => 100 / factor))
.subscribe(factor => {
const points = {} as {[point: string]: number};
[2,3,4,5,6].forEach(num =>
points[`(min-width: ${num === 2 ? 0 : ((num - 2) * 24 + (num - 1) * CARD_WIDTH + SIDE_NAV_PLUS_PAD) * factor}px) and ` +
`(max-width: ${num === 6 ? 20000 : ((num - 1) * 24 + num * CARD_WIDTH + SIDE_NAV_PLUS_PAD) * factor}px)`] = num);
this.cardLayoutChange$ = breakpointObserver.observe(Object.keys(points));
this.resultRows$ = combineLatest([this.cardLayoutChange$, this.results$])
.pipe(map(([match, results]) => {
const point = Object.entries(match.breakpoints).find(([, val]) => val);
const cards = point ? points[point[0]] - 1 : 3;
this.rowWidth = cards * CARD_WIDTH + (cards - 1) * 24
return chunk(cards, results);
}));
});
}
}

View File

@@ -38,12 +38,13 @@ export class CommonDashboardEffects {
this.projectsApi.projectsGetAllEx({
stats_for_state: ProjectsGetAllExRequest.StatsForStateEnum.Active,
include_stats: true,
include_stats_filter: {system_tags: ['-pipeline']},
order_by: ['featured', '-last_update'],
page: 0,
page_size: CARDS_IN_ROW,
active_users: (showOnlyUserWork ? [user.id] : null),
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination']
}).pipe(
} as ProjectsGetAllExRequest).pipe(
mergeMap(({projects}) => [setRecentProjects({projects}), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])
)

View File

@@ -1,26 +1,26 @@
import {InitSearch, ResetSearch} from '../common-search/common-search.actions';
import {skip} from 'rxjs/operators';
import {Model} from '../../business-logic/model/models/model';
import {Model} from '~/business-logic/model/models/model';
import {SearchDeactivate, searchStart} from '../dashboard-search/dashboard-search.actions';
import {IRecentTask} from './common-dashboard.reducer';
import {ITask} from '../../business-logic/model/al-task';
import {ITask} from '~/business-logic/model/al-task';
import {Observable} from 'rxjs';
import {ICommonSearchState, selectSearchQuery} from '../common-search/common-search.reducer';
import {Store} from '@ngrx/store';
import {
selectActiveSearch, selectExperimentsResults, selectModelsResults, selectProjectsResults,
selectActiveSearch, selectExperimentsResults, selectModelsResults, selectPipelinesResults, selectProjectsResults,
selectResultsCounter,
selectSearchTerm
} from '../dashboard-search/dashboard-search.reducer';
import {Project} from '../../business-logic/model/projects/project';
import {Project} from '~/business-logic/model/projects/project';
import {setSelectedProjectId} from '../core/actions/projects.actions';
import {isExample} from '../shared/utils/shared-utils';
import {ActiveSearchLink} from '~/features/dashboard/containers/dashboard-search/dashboard-search.component';
export abstract class DashboardSearchComponentBase {
abstract store;
abstract router;
public activeLink: string = 'projects';
public activeLink = 'projects' as ActiveSearchLink;
private searchSubs;
public searchQuery$: Observable<ICommonSearchState['searchQuery']>;
public activeSearch$: Observable<boolean>;
@@ -29,12 +29,14 @@ export abstract class DashboardSearchComponentBase {
public projectsResults$: Observable<Array<Project>>;
public experimentsResults$: Observable<any>;
public searchTerm$: Observable<ICommonSearchState['searchQuery']>;
public pipelinesResults$: Observable<Project[]>;
constructor(store: Store<any>){
this.searchQuery$ = store.select(selectSearchQuery);
this.activeSearch$ = store.select(selectActiveSearch);
this.resultsCounter$ = store.select(selectResultsCounter);
this.modelsResults$ = store.select(selectModelsResults);
this.pipelinesResults$ = store.select(selectPipelinesResults);
this.projectsResults$ = store.select(selectProjectsResults);
this.experimentsResults$ = store.select(selectExperimentsResults);
this.searchTerm$ = store.select(selectSearchTerm);
@@ -73,6 +75,10 @@ export abstract class DashboardSearchComponentBase {
this.router.navigateByUrl(`projects/${project.id}`);
this.store.dispatch(setSelectedProjectId({projectId: project.id, example: isExample(project)}));
}
pipelineSelected(project: Project) {
this.router.navigateByUrl(`pipelines/${project.id}/experiments`);
this.store.dispatch(setSelectedProjectId({projectId: project.id, example: isExample(project)}));
}
public taskSelected(task: IRecentTask | ITask) {
// TODO ADD task.id to route
@@ -85,7 +91,7 @@ export abstract class DashboardSearchComponentBase {
this.activeLink = activeLink;
}
setFirstActiveLink(allResults, tabsIndexes: string[]) {
setFirstActiveLink(allResults, tabsIndexes) {
if (!(allResults[tabsIndexes.indexOf(this.activeLink)].length > 0)) {
const firstTabIndex = allResults.findIndex(list => list.length > 0);
if (firstTabIndex > -1) {

View File

@@ -1,7 +1,7 @@
<sm-table [tableData]="tasks"
[columns]="cols"
[rowHeight]="52"
[selectionMode]="null"
selectionMode="single"
[scrollable]="true"
(rowClicked)="onExperimentSelected($event)"
>

View File

@@ -1,5 +1,5 @@
<ng-container *ngIf="(source$ | async) as source">
<div *ngIf="!isFailed" class="item snippet" [ngClass]="{'loading' : isLoading}">
<div *ngIf="!isFailed" class="item-snippet" [ngClass]="{'loading' : isLoading}">
<ng-container [ngSwitch]="type">
<img
*ngSwitchCase="'image'"

View File

@@ -1,14 +1,9 @@
@import "../../shared/ui-components/styles/variables";
.item {
.item-snippet {
width: 180px;
height: 180px;
margin-top: 8px;
margin-right: 8px;
}
.snippet {
border-radius: 3px;
border-radius: 4px;
position: relative;
text-align: center;
background: #efefef;
@@ -35,13 +30,12 @@
.image-var {
position: absolute;
color: $blue-300;
padding: 1px 5px 0px 5px;
bottom: 1px;
color: $blue-500;
bottom: 2px;
left: 50%;
height: 20px;
transform: translateX(-50%);
width: 100%;
width: calc(100% - 18px);
}
.html-wrap {
@@ -59,6 +53,9 @@
transform-origin: 0 0;
}
.html-snippet {
border: 1px solid $blue-200;
border-radius: 4px;
&:hover {
.toolbar {
opacity: 1;
@@ -70,7 +67,8 @@
display: flex;
position: absolute;
bottom: 8px;
left: 16px;
left: 50%;
transform: translateX(-50%);
opacity: 0;
visibility: hidden;
transition: opacity 0.5s, visibility 0.5s;
@@ -78,8 +76,9 @@
}
.clickable-icon {
background-color: #efefef;
padding: 4px 24px;
background-color: $blue-400;
color: $blue-50;
padding: 6px 18px;
border-radius: 4px;
}

View File

@@ -3,7 +3,7 @@ import {isHtmlPage, isTextFileURL} from '../../shared/utils/shared-utils';
import {IsAudioPipe} from '../../shared/pipes/is-audio.pipe';
import {IsVideoPipe} from '../../shared/pipes/is-video.pipe';
import {addMessage} from '../../core/actions/layout.actions';
import {MESSAGES_SEVERITY} from '../../../app.constants';
import {MESSAGES_SEVERITY} from '~/app.constants';
import {Store} from '@ngrx/store';
import {ThemeEnum} from '../../experiments/shared/common-experiments.const';
import {getSignedUrlOrOrigin$} from '../../core/reducers/common-auth-reducer';

View File

@@ -4,12 +4,12 @@
<mat-panel-title> {{iteration.iter}}</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="d-flex justify-content flex-wrap">
<div class="d-flex justify-content flex-wrap sample-row">
<sm-debug-image-snippet
*ngFor="let frame of iteration.events; trackBy:trackFrame"
[frame]="frame"
(imageError)="imageUrlError({frame: frame, experimentId: experimentId})"
(imageClicked)="imageClicked.emit({frame: frame, snippetKey: frame.key, frames: iterationEvents})">
(imageError)="imageUrlError({frame, experimentId})"
(imageClicked)="imageClicked.emit({frame})">
</sm-debug-image-snippet>
</div>
</ng-template>

View File

@@ -44,7 +44,7 @@
.mat-expansion-panel {
width: 100%;
margin-top: 4px;
margin-bottom: 12px;
box-shadow: unset;
&:hover{
background:transparent;
@@ -53,7 +53,7 @@
::ng-deep .mat-expansion-panel-content {
.mat-expansion-panel-body {
padding: 0 12px 48px 12px;
padding: 0 0 12px 28px;
}
}
@@ -100,4 +100,8 @@
background: transparent;
}
}
.sample-row {
gap: 24px;
}
}

View File

@@ -1,5 +1,6 @@
import {Component, Input, Output} from '@angular/core';
import {EventEmitter} from '@angular/core';
import {Iteration, Event} from '@common/debug-images/debug-images.component';
@Component({
selector: 'sm-debug-images-view',
@@ -10,26 +11,17 @@ export class DebugImagesViewComponent {
public trackKey = (index: number, item: any) => item.iter;
public trackFrame = (index: number, item: any) => `${item?.key} ${item?.timestamp}`;
public iterationEvents;
@Input() experimentId;
@Input() isMergeIterations;
@Input() title;
private _iterations;
@Input() set iterations(iters) {
this._iterations = iters;
this.iterationEvents = [];
iters.forEach(iteration => this.iterationEvents.push(iteration.events));
}
get iterations() {
return this._iterations;
}
@Input() iterations: Iteration[];
@Input() isDarkTheme = false;
@Output() imageClicked = new EventEmitter();
@Output() refreshClicked = new EventEmitter();
@Output() urlError = new EventEmitter();
public imageUrlError(data: { frame: string; experimentId: string }) {
public imageUrlError(data: { frame: Event; experimentId: string }) {
this.urlError.emit(data);
}
}

View File

@@ -59,10 +59,10 @@
[title]="experimentNames && experimentNames[experimentId]"
[isMergeIterations]="mergeIterations"
[isDarkTheme]="isDarkTheme"
(imageClicked)="imageClicked($event)"
(imageClicked)="imageClicked($event, experimentId)"
(urlError)="urlError()"
>
</sm-debug-images-view>
</div>
</div>
</div>

View File

@@ -8,6 +8,7 @@ import {MatDialog} from '@angular/material/dialog';
import * as debugActions from './debug-images-actions';
import {fetchExperiments, getDebugImagesMetrics, resetDebugImages} from './debug-images-actions';
import {
ITaskOptionalMetrics,
selectBeginningOfTime,
selectDebugImages,
selectNoMore,
@@ -30,7 +31,7 @@ import {getSignedUrl} from '../core/actions/common-auth.actions';
import {addMessage} from '../core/actions/layout.actions';
import {RefreshService} from '@common/core/services/refresh.service';
interface Event {
export interface Event {
timestamp: number;
type?: string;
task?: string;
@@ -43,7 +44,7 @@ interface Event {
worker?: string;
}
interface Iteration {
export interface Iteration {
events: Event[];
iter: number;
}
@@ -85,8 +86,8 @@ export class DebugImagesComponent implements OnInit, OnDestroy {
public allowAutorefresh: boolean = false;
public noMoreData$: Observable<boolean>;
public optionalMetrics$: Observable<any>;
public optionalMetrics: any;
public optionalMetrics$: Observable<ITaskOptionalMetrics[]>;
public optionalMetrics: {[experimentId: string]: string};
public selectedMetrics: { [taskId: string]: string } = {};
public beginningOfTime: any;
private beginningOfTimeSubscription: Subscription;
@@ -270,11 +271,8 @@ export class DebugImagesComponent implements OnInit, OnDestroy {
// this.adminService.checkImgUrl(frame.oldSrc || frame.src);
}
imageClicked({frame, frames}) {
let iterationSnippets = [];
Object.entries(frames).map(iteration => {
iterationSnippets = iterationSnippets.concat(iteration[1]);
});
imageClicked({frame}, experimentId: string) {
const iterationSnippets = this.debugImages?.[experimentId]?.data.map(iter => iter.events).flat();
const sources = iterationSnippets.map(img => img.url);
const index = iterationSnippets.findIndex(img => img.url === frame.url);
this.dialog.open(ImageDisplayerComponent, {
@@ -326,10 +324,6 @@ export class DebugImagesComponent implements OnInit, OnDestroy {
return (this.thereAreNoMetrics(experiment) && this.optionalMetrics && this.optionalMetrics[experiment]) || (this.thereAreNoDebugImages(experiment) && this.debugImages && this.debugImages[experiment]);
}
// buildUrl() {
// return ['../../', 'experiments', ];
// }
copyIdToClipboard() {
this.store.dispatch(addMessage('success', 'Copied to clipboard'));
}

View File

@@ -102,15 +102,15 @@ $extra-header-min-height: 50px;
pre {
margin-bottom: 0;
margin-top: 0;
padding-right: 20px;
padding-right: 30px;
padding-left: 6px;
.extend-toggle {
visibility: hidden;
cursor: pointer;
position: absolute;
right: 0;
top: 5px;
right: 6px;
top: 6px;
color: $blue-500;
}
}
@@ -144,6 +144,14 @@ $extra-header-min-height: 50px;
background-color: #f8f9fa;
}
&.hovered {
box-shadow: 0 0 0 1px $blue-250 inset;
&.diff-row {
box-shadow: 0 0 0 1px lighten($strong-red, 20%) inset, 0 0 0 2px white inset;
}
}
&.selected-diff {
background-color: $faint-gray;
position: relative;
@@ -197,6 +205,14 @@ $extra-header-min-height: 50px;
cursor: default;
}
&.hovered {
box-shadow: 0 0 0 1px $blue-250 inset;
&.al-danger {
box-shadow: 0 0 0 1px lighten($strong-red, 20%) inset, 0 0 0 2px white inset;
}
}
&.selected-diff {
background-color: $faint-gray;
position: relative;

View File

@@ -54,6 +54,7 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
public originalExperiments: { [id: string]: IExperimentDetail | ExperimentParams } = {};
public allPathsDiffs: any = {};
public selectedPath: string = null;
public hoveredPath: string = null;
private selectedPathIndex: number = -1;
public onlyDiffsPaths: string[];
@@ -462,12 +463,17 @@ export abstract class ExperimentCompareBase extends ExperimentCompareDetailsBase
}
}
rowHovered(path) {
this.hoveredPath = path;
}
keyClicked(data) {
const path = data.path;
this.selectedPathClicked(path);
}
checkIfSelectedPath = (data: any) => this.selectedPath === (data.path);
checkIfSelectedPath = (data: any) => this.selectedPath === data.path;
checkIfHoveredPath = (data: any) => this.hoveredPath === data.path;
// checkIfFoundPathPath = (data: any) => this.foundPath === (data.path);

View File

@@ -23,26 +23,32 @@
<cdk-virtual-scroll-viewport #virtualScrollRef class="virtual-scroll-container" [class.is-not-origin]="i > 0" itemSize="28" minBufferPx="280" maxBufferPx="560">
<ng-container *cdkVirtualFor="let node of dataSource; let i = index">
<div class="node" [class.parent]="node.hasChildren">
<div *ngIf="node.hasChildren" class="section" [ngClass]="'depth-' + node.level">
<div *ngIf="node.hasChildren" class="section" [ngClass]="'depth-' + node.level" (mouseenter)="rowHovered(node.data?.path)">
<div class="content"
(click)="toggleNode(node)"
[ngClass]="node?.data?.classStyle"
[class.selected-diff]="checkIfSelectedPath(node.data)"
[class.hovered]="checkIfHoveredPath(node.data)"
[class.identical-row]="!allPaths[node.data.path]">
<i class="fas" [style.margin-left.px]="2 + node.level * 20" [ngClass]="treeControl.isExpanded(node) ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
<span
class="title-key"
[smTooltip]="(renameMap[node.data.key] || node.data.key) | hideHashTitle"
[matTooltipShowDelay]="500"
matTooltipPosition="above"
smShowTooltipIfEllipsis
[class.ellipsis]="showEllipsis"
[style.width.px]="showEllipsis ? nativeWidth - 45 - node.level * 20 : null"
[style.width.px]="showEllipsis ? nativeWidth - 65 - node.level * 20 : null"
>{{(renameMap[node.data.key] || node.data.key) | hideHashTitle}}</span>
<i *ngIf="node.data.tooltip" class="al-icon sm al-ico-description node-icon" customClass="hyper-parameters-tooltip" [smTooltip]="node.data.tooltip"></i>
</div>
</div>
<div *ngIf="!node.hasChildren" class="section" (click)="keyClicked(node.data)">
<div *ngIf="!node.hasChildren" class="section" (click)="keyClicked(node.data)" (mouseenter)="rowHovered(node.data?.path)">
<div [style.padding-left.px]="2 + node.level * 20" [ngClass]="{
'node-item-container': true,
'identical-row': checkIfIdenticalRow(node.data),
'selected-diff': checkIfSelectedPath(node.data),
'hovered': checkIfHoveredPath(node.data),
'not-existing-on-origin': !node.data.existOnOrigin,
'not-existing-on-compared': !node.data.existOnCompared,
'diff-row': !node.data.isValueEqualToOrigin,
@@ -51,9 +57,9 @@
}">
<div>
<pre *ngIf="(node.data.value !== undefined) || (node.data.existOnOrigin && node.data.existOnCompared)"
[class.no-ellipsis]="((node.data.key | hideHash) + node.data.value).length < 51"
[class.no-ellipsis]="((node.data.key | hideHash) + node.data.value).length < 45"
[class.with-ellipsis]="showEllipsis"
[style.width.px]="showEllipsis ? nativeWidth - 45 - node.level * 20 : null"
[style.width.px]="showEllipsis ? nativeWidth - 55 - node.level * 20 : null"
>{{node.data.key | hideHash}}{{node.data.value}}<i class="al-icon sm al-ico-line-expand extend-toggle" [class.fa-rotate-180]="!showEllipsis" (click)="toggleEllipsis(); $event.stopPropagation()"></i>
</pre>
</div>

View File

@@ -45,6 +45,6 @@ $list-width: 300px;
}
sm-experiment-graphs {
height: calc(100% - 64px);
height: 100%;
}
}

View File

@@ -63,7 +63,8 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
private changeDetection: ChangeDetectorRef,
private refresh: RefreshService
) {
this.listOfHidden = this.store.pipe(select(selectSelectedSettingsHiddenScalar));
this.listOfHidden = this.store.select(selectSelectedSettingsHiddenScalar)
.pipe(distinctUntilChanged(isEqual));
this.searchTerm$ = this.store.pipe(select(selectExperimentMetricsSearchTerm));
this.smoothWeight$ = this.store.select(selectCompareSelectedSettingsSmoothWeight);
this.xAxisType$ = this.store.select(selectCompareSelectedSettingsxAxisType);
@@ -161,7 +162,7 @@ export class ExperimentCompareScalarChartsComponent implements OnInit, OnDestroy
}
searchTermChanged(searchTerm: string) {
this.store.dispatch(new SetExperimentMetricsSearchTerm({searchTerm: searchTerm}));
this.store.dispatch(new SetExperimentMetricsSearchTerm({searchTerm}));
}
resetMetrics() {

View File

@@ -9,17 +9,26 @@
</sm-experiment-compare-general-data>
</div>
<div class="tree-card-body" smSyncScroll *compareCardBody="let experiment; let i = index;">
<div *ngFor="let node of comparedTasks[i]" class="node" [ngClass]="node?.metaData?.classStyle">
<div class="content" (click)="collapsedToggled(node)">
<i class="fas" [style.margin-left.px]="2 + node.level * 20" [ngClass]="realIsNodeOpen(node) ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
{{node.data.key}}
<div *ngFor="let node of comparedTasks[i]"
class="node"
[ngClass]="node?.metaData?.classStyle"
[class.hovered]="!hoveredTable && hoveredRow === node.data.key"
(mouseenter)="onRowHovered(node.data?.key, null)">
<div class="content"
(click)="collapsedToggled(node)"
(mouseenter)="onRowHovered(node.data?.key, null)">
<i class="node-chevron fas" [style.margin-left.px]="2 + node.level * 20" [ngClass]="realIsNodeOpen(node) ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
<span class="node-key">{{node.data.key}}</span>
</div>
<al-table-diff *ngIf="realIsNodeOpen(node)"
[valueTitle]="valuesMode.name | uppercase" keyTitle="VARIANT"
[isOrigin]="i===0"
(sortByChanged)="metricSortChanged($event,node.data)"
[sortConfig]="(sortOrder$| async)[node.data.key] || {order:'asc', keyOrValue: 'key'}"
[keyValueArray]="node.data | getKeyValueArrayPipe:allKeysEmptyObject:(sortOrder$| async)[node.data.key]:valuesMode.key">
[keyValueArray]="node.data | getKeyValueArrayPipe:allKeysEmptyObject:(sortOrder$| async)[node.data.key]:valuesMode.key"
[hoveredRow]="hoveredTable === node.data.key && hoveredRow"
(sortByChanged)="metricSortChanged($event,node.data)"
(rowHovered)="onRowHovered($event, node.data.key)"
>
<ng-template
let-metricValue>
{{metricValue}}

View File

@@ -40,14 +40,39 @@
position: relative;
display: block;
&:after {
position: absolute;
content: " ";
width: 100%;
height: 100%;
top: 0;
left: 0;
.content {
background: #f2f3f6;
pointer-events: none;
.node-key, .node-chevron {
visibility: hidden;
}
}
al-table-diff {
display: block;
position: relative;
pointer-events: none;
&:after {
position: absolute;
content: " ";
width: 100%;
height: 100%;
top: 0;
left: 0;
background: #f2f3f6;
}
}
}
&.hovered {
.content, &:after {
box-shadow: 0 0 0 1px $blue-250 inset;
&.diff-row {
box-shadow: 0 0 0 1px lighten($strong-red, 20%) inset, 0 0 0 2px white inset;
}
}
}
}

View File

@@ -53,6 +53,8 @@ export class ExperimentCompareMetricValuesComponent implements OnInit, OnDestroy
public experiments = [];
private taskIds: string;
public valuesMode: ValueMode;
public hoveredRow: string;
public hoveredTable: string;
constructor(
private router: Router,
@@ -149,6 +151,7 @@ export class ExperimentCompareMetricValuesComponent implements OnInit, OnDestroy
} else {
this.paths = this.paths.filter(path => path !== node.data.key);
}
this.hoveredRow = node.data.key;
}
metricSortChanged(event, nodeData) {
@@ -194,4 +197,9 @@ export class ExperimentCompareMetricValuesComponent implements OnInit, OnDestroy
copyIdToClipboard() {
this.store.dispatch(addMessage('success', 'Copied to clipboard'));
}
onRowHovered(tableKey: string, tableName: string) {
this.hoveredRow = tableKey;
this.hoveredTable = tableName;
}
}

View File

@@ -22,21 +22,23 @@
<cdk-virtual-scroll-viewport #virtualScrollRef class="virtual-scroll-container" [class.is-not-origin]="i > 0" itemSize="28" minBufferPx="280" maxBufferPx="560">
<ng-container *cdkVirtualFor="let node of dataSource; let i = index">
<div class="node" [class.parent]="node.hasChildren">
<div *ngIf="node.hasChildren" class="section" [ngClass]="'depth-' + node.level">
<div *ngIf="node.hasChildren" class="section" [ngClass]="'depth-' + node.level" (mouseenter)="rowHovered(node.data?.path)">
<div class="content"
(click)="toggleNode(node)"
[ngClass]="node?.data?.classStyle"
[class.selected-diff]="checkIfSelectedPath(node.data)"
[class.hovered]="checkIfHoveredPath(node.data)"
[class.identical-row]="!allPaths[node.data.path]">
<i class="fas" [style.margin-left.px]="2 + node.level * 20" [ngClass]="treeControl.isExpanded(node) ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
<span class="title-key" [class.ellipsis]="showEllipsis">{{(renameMap[node.data.key] || node.data.key)}}</span>
</div>
</div>
<div *ngIf="!node.hasChildren" class="section" (click)="keyClicked(node.data)">
<div *ngIf="!node.hasChildren" class="section" (click)="keyClicked(node.data)" (mouseenter)="rowHovered(node.data?.path)">
<div [style.padding-left.px]="2 + node.level * 20" [ngClass]="{
'node-item-container': true,
'identical-row': checkIfIdenticalRow(node.data),
'selected-diff': checkIfSelectedPath(node.data),
'hovered': checkIfHoveredPath(node.data),
'not-existing-on-origin': !node.data.existOnOrigin,
'not-existing-on-compared': !node.data.existOnCompared,
'diff-row': !node.data.isValueEqualToOrigin,
@@ -50,7 +52,7 @@
>{{node.data.key}}<i class="al-icon sm al-ico-line-expand extend-toggle" [class.fa-rotate-180]="!showEllipsis" (click)="toggleEllipsis(); $event.stopPropagation()"></i>
</pre>
<pre class="node-val"
[style.width]="showEllipsis ? (section.clientWidth - 250) + 'px' : null"
[style.width]="showEllipsis ? (section.clientWidth - 320) + 'px' : null"
[class.no-ellipsis]="node.data.value.length < 15"
[class.ellipsis]="showEllipsis">{{node.data.value}}<i class="al-icon sm al-ico-line-expand extend-toggle" [class.fa-rotate-180]="!showEllipsis" (click)="toggleEllipsis(); $event.stopPropagation()"></i>
</pre>

View File

@@ -16,6 +16,7 @@
justify-content: flex-start;
.node-key {
flex: 1;
position: relative;
cursor: pointer;
white-space: nowrap;
@@ -33,6 +34,7 @@
}
.node-val {
flex: 1;
position: relative;
cursor: pointer;
white-space: nowrap;

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