Release v1.6 (#31)

This commit is contained in:
shyallegro
2022-07-10 14:10:46 +03:00
committed by GitHub
parent c7c4e73645
commit b96f723af1
339 changed files with 9471 additions and 7768 deletions

8510
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.5.0",
"version": "1.6.0",
"license": "",
"scripts": {
"ng": "ng",
@@ -20,30 +20,30 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^13.2.2",
"@angular/cdk": "^13.2.2",
"@angular/common": "^13.2.2",
"@angular/compiler": "^13.2.2",
"@angular/core": "^13.2.2",
"@angular/forms": "^13.2.2",
"@angular/material": "^13.2.2",
"@angular/platform-browser": "^13.2.2",
"@angular/platform-browser-dynamic": "^13.2.2",
"@angular/platform-server": "^13.2.2",
"@angular/router": "^13.2.2",
"@angular/service-worker": "^13.2.2",
"@angular/youtube-player": "^13.2.2",
"@aws-sdk/client-s3": "^3.53.1",
"@aws-sdk/s3-request-presigner": "^3.53.1",
"@ngneat/dag": "^1.1.0",
"@ngrx/effects": "^13.0.2",
"@ngrx/entity": "^13.0.2",
"@ngrx/router-store": "^13.0.2",
"@ngrx/store": "^13.0.2",
"ace-builds": "^1.4.14",
"@angular/animations": "^13.3.7",
"@angular/cdk": "^13.3.7",
"@angular/common": "^13.3.7",
"@angular/compiler": "^13.3.7",
"@angular/core": "^13.3.7",
"@angular/forms": "^13.3.7",
"@angular/material": "^13.3.7",
"@angular/platform-browser": "^13.3.7",
"@angular/platform-browser-dynamic": "^13.3.7",
"@angular/platform-server": "^13.3.7",
"@angular/router": "^13.3.7",
"@angular/service-worker": "^13.3.7",
"@angular/youtube-player": "^13.3.7",
"@aws-sdk/client-s3": "^3.88.0",
"@aws-sdk/s3-request-presigner": "^3.88.0",
"@ngneat/dag": "^2.0.0",
"@ngrx/effects": "^13.2.0",
"@ngrx/entity": "^13.2.0",
"@ngrx/router-store": "^13.2.0",
"@ngrx/store": "^13.2.0",
"ace-builds": "^1.5.0",
"angular-google-tag-manager": "^1.5.0",
"angular-resizable-element": "^5.0.0",
"angular-split": "^13.1.0",
"angular-split": "^13.2.0",
"ansi-to-html": "^0.7.2",
"bootstrap": "^4.6.1",
"britecharts": "^2.18.0",
@@ -56,49 +56,48 @@
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"lucene": "^2.1.1",
"ngx-clipboard": "^15.0.1",
"ngx-color-picker": "^12.0.0",
"ngx-clipboard": "^15.1.0",
"ngx-color-picker": "^12.0.1",
"ngx-markdown-editor": "^4.0.0",
"ngx-window-token": "^6.0.0",
"object-hash": "^2.2.0",
"object-hash": "^3.0.0",
"primeicons": "^5.0.0",
"primeng": "^13.0.4",
"primeng": "^13.4.0",
"process": "^0.11.10",
"rxjs": "^7.5.5",
"string-to-color": "^2.2.2",
"tslib": "^2.3.1",
"tslib": "^2.4.0",
"url": "^0.11.0",
"uuid": "^8.3.2",
"zone.js": "~0.11.4"
"zone.js": "~0.11.5"
},
"devDependencies": {
"@angular-devkit/build-angular": "^13.2.3",
"@angular-devkit/core": "^13.2.3",
"@angular-devkit/schematics": "^13.2.3",
"@angular-devkit/schematics-cli": "^13.2.3",
"@angular-eslint/builder": "^13.1.0",
"@angular-eslint/eslint-plugin": "^13.1.0",
"@angular-eslint/eslint-plugin-template": "^13.1.0",
"@angular-eslint/schematics": "13.1.0",
"@angular-eslint/template-parser": "^13.1.0",
"@angular/cli": "^13.2.3",
"@angular/compiler-cli": "^13.2.2",
"@angular/language-service": "^13.2.2",
"@fortawesome/fontawesome-free": "^6.0.0",
"@ngrx/schematics": "^13.0.2",
"@ngrx/store-devtools": "^13.0.2",
"@angular-devkit/build-angular": "^13.3.5",
"@angular-devkit/core": "^13.3.5",
"@angular-devkit/schematics": "^13.3.5",
"@angular-devkit/schematics-cli": "^13.3.5",
"@angular-eslint/builder": "^13.2.1",
"@angular-eslint/eslint-plugin": "^13.2.1",
"@angular-eslint/eslint-plugin-template": "^13.2.1",
"@angular-eslint/schematics": "13.2.1",
"@angular-eslint/template-parser": "^13.2.1",
"@angular/cli": "^13.3.5",
"@angular/compiler-cli": "^13.3.7",
"@angular/language-service": "^13.3.7",
"@fortawesome/fontawesome-free": "^6.1.1",
"@ngrx/schematics": "^13.2.0",
"@ngrx/store-devtools": "^13.2.0",
"@types/d3-selection": "^3.0.2",
"@types/lodash": "^4.14.178",
"@types/lodash": "^4.14.182",
"@types/node": "^16.11.19",
"@types/plotly.js": "^1.54.20",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "5.9.0",
"@typescript-eslint/parser": "5.9.0",
"codelyzer": "^6.0.2",
"eslint": "^8.9.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-jsdoc": "37.9.1",
"@typescript-eslint/eslint-plugin": "5.23.0",
"@typescript-eslint/parser": "5.23.0",
"eslint": "^8.15.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsdoc": "39.2.9",
"eslint-plugin-prefer-arrow": "1.2.3",
"typescript": "^4.5.5"
"typescript": "~4.6.4"
}
}

View File

@@ -3,6 +3,7 @@ import {Routes} from '@angular/router';
import {AdminComponent} from '@common/settings/admin/admin.component';
*/
import {ProjectRedirectGuardGuard} from '@common/shared/guards/project-redirect.guard';
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
export const routes: Routes = [
@@ -42,7 +43,7 @@ export const routes: Routes = [
{
path: 'compare-experiments',
loadChildren: () => import('./webapp-common/experiments-compare/experiments-compare.module').then(m => m.ExperimentsCompareModule),
data: {search: false}
data: {entityType: EntityTypeEnum.experiment},
},
]
},
@@ -68,12 +69,18 @@ export const routes: Routes = [
},
{
path: 'compare-experiments',
data: {entityType: EntityTypeEnum.controller},
loadChildren: () => import('./webapp-common/experiments-compare/experiments-compare.module').then(m => m.ExperimentsCompareModule)
},
]
},
]
},
{
path: 'datasets',
data: {search: true},
loadChildren: () => import('./features/datasets/datasets.module').then(m => m.DatasetsModule)
},
{path: 'workers-and-queues', loadChildren: () => import('./features/workers-and-queues/workers-and-queues.module').then(m => m.WorkersAndQueuesModule)},
{path: '404', loadChildren: () => import('./features/not-found/not-found.module').then(m => m.NotFoundModule)},
{path: '**', loadChildren: () => import('./features/not-found/not-found.module').then(m => m.NotFoundModule)},

View File

@@ -59,6 +59,11 @@ import { BASE_PATH, COLLECTION_FORMATS } from '../variables'
import { Configuration } from '../configuration';
import {EventsScalarMetricsIterRawRequest} from "~/business-logic/model/events/eventsScalarMetricsIterRawRequest";
import {EventsScalarMetricsIterRawResponse} from "~/business-logic/model/events/eventsScalarMetricsIterRawResponse";
import {EventsNextPlotSampleRequest} from '~/business-logic/model/events/eventsNextPlotSampleRequest';
import {PlotSampleResponse} from '~/business-logic/model/events/plotSampleResponse';
import {EventsGetPlotSampleRequest} from '~/business-logic/model/events/eventsGetPlotSampleRequest';
import {EventsGetTaskSingleValueMetricsRequest} from '~/business-logic/model/events/eventsGetTaskSingleValueMetricsRequest';
import {EventsGetTaskSingleValueMetricsResponse} from '~/business-logic/model/events/eventsGetTaskSingleValueMetricsResponse';
@Injectable()
@@ -453,7 +458,97 @@ export class ApiEventsService {
);
}
/**
/**
*
* Get the plot for the next variant for the same iteration or for the next iteration
* @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 eventsNextPlotSample(request: EventsNextPlotSampleRequest, 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 eventsNextPlotSample.');
}
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<PlotSampleResponse>(`${this.basePath}/events.next_plot_sample`,
request,
{
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
*
* Return the plot per metric and variant for the provided iteration
* @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 eventsGetPlotSample(request: EventsGetPlotSampleRequest, 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 eventsGetPlotSample.');
}
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<PlotSampleResponse>(`${this.basePath}/events.get_plot_sample`,
request,
{
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
*
* get task scalar metrics and variants
* @param request request body
@@ -723,6 +818,51 @@ export class ApiEventsService {
);
}
/**
*
* Get single value metrics for the passed tasks
* @param request request body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public eventsGetTaskSingleValueMetrics(request: EventsGetTaskSingleValueMetricsRequest, 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 eventsGetTaskSingleValueMetrics.');
}
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<EventsGetTaskSingleValueMetricsResponse>(`${this.basePath}/events.get_task_single_value_metrics`,
request,
{
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
*
*

View File

@@ -25,6 +25,8 @@ import { Observable } from 'rxjs';
import { OrganizationGetTagsRequest } from '../model/organization/organizationGetTagsRequest';
import { OrganizationGetTagsResponse } from '../model/organization/organizationGetTagsResponse';
import { OrganizationGetUserCompaniesResponse } from '../model/organization/organizationGetUserCompaniesResponse';
import { OrganizationGetEntitiesCountRequest } from '../model/organization/organizationGetEntitiesCountRequest';
import { OrganizationGetEntitiesCountResponse } from '../model/organization/organizationGetEntitiesCountResponse';
import { BASE_PATH, COLLECTION_FORMATS } from '../variables';
import { Configuration } from '../configuration';
@@ -152,4 +154,48 @@ export class ApiOrganizationService {
);
}
/**
*
* Get counts for the company entities according to the passed search criteria
* @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 organizationGetEntitiesCount(request: OrganizationGetEntitiesCountRequest, 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 organizationGetEntitiesCount.');
}
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<OrganizationGetEntitiesCountResponse>(`${this.basePath}/organization.get_entities_count`,
request,
{
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
}

View File

@@ -37,4 +37,8 @@ export interface EventsGetDebugImageSampleRequest {
* Scroll ID from the previous call to get_debug_image_sample or empty
*/
scroll_id?: string;
/**
* If set then subsequent navigation with next_debug_image_sample is done on the debug images for the passed metric only. Otherwise for all the metrics
*/
navigate_current_metric?: boolean;
}

View File

@@ -0,0 +1,44 @@
/**
* events
* 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 EventsGetPlotSampleRequest {
/**
* Task ID
*/
task: string;
/**
* Metric name
*/
metric: string;
/**
* Metric variant
*/
variant: string;
/**
* The iteration to bring plot from. If not specified then the latest reported plot is retrieved
*/
iteration?: number;
/**
* If set then scroll state will be refreshed to reflect the latest changes in the plots
*/
refresh?: boolean;
/**
* Scroll ID from the previous call to get_plot_sample or empty
*/
scroll_id?: string;
/**
* If set then subsequent navigation with next_plot_sample is done on the plots for the passed metric only. Otherwise for all the metrics
*/
navigate_current_metric?: boolean;
}

View File

@@ -0,0 +1,20 @@
/**
* events
* 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 EventsGetTaskSingleValueMetricsRequest {
/**
* List of task Task IDs
*/
tasks: Array<string>;
}

View File

@@ -0,0 +1,21 @@
/**
* events
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
import { EventsGetTaskSingleValueMetricsResponseTasks } from '././eventsGetTaskSingleValueMetricsResponseTasks';
export interface EventsGetTaskSingleValueMetricsResponse {
/**
* Single value metrics grouped by task
*/
tasks?: Array<EventsGetTaskSingleValueMetricsResponseTasks>;
}

View File

@@ -0,0 +1,22 @@
/**
* events
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 999.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
import { EventsGetTaskSingleValueMetricsResponseValues } from '././eventsGetTaskSingleValueMetricsResponseValues';
export interface EventsGetTaskSingleValueMetricsResponseTasks {
/**
* Task ID
*/
task?: string;
values?: Array<EventsGetTaskSingleValueMetricsResponseValues>;
}

View File

@@ -0,0 +1,20 @@
/**
* events
* 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 EventsGetTaskSingleValueMetricsResponseValues {
metric?: string;
variant?: string;
value?: number;
timestamp?: number;
}

View File

@@ -0,0 +1,28 @@
/**
* events
* 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 EventsNextPlotSampleRequest {
/**
* Task ID
*/
task: string;
/**
* Scroll ID from the previous call to get_plot_sample
*/
scroll_id: string;
/**
* If set then get the either previous variant event from the current iteration or (if does not exist) the last variant event from the previous iteration. Otherwise next variant event from the current iteration or first variant event from the next iteration
*/
navigate_earlier?: boolean;
}

View File

@@ -0,0 +1,32 @@
/**
* events
* 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 PlotSampleResponse {
/**
* Scroll ID to pass to the next calls to get_plot_sample or next_plot_sample
*/
scroll_id?: string;
/**
* Plot event
*/
event?: object;
/**
* minimal valid iteration for the variant
*/
min_iteration?: number;
/**
* maximal valid iteration for the variant
*/
max_iteration?: number;
}

View File

@@ -2,7 +2,7 @@
* models
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* OpenAPI spec version: 2.14
* OpenAPI spec version: 999.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
@@ -92,4 +92,8 @@ export interface ModelsGetAllExRequest {
* The number of models to retrieve
*/
size?: number;
/**
* If true, include models statistic in response
*/
include_stats?: boolean;
}

View File

@@ -0,0 +1,41 @@
/**
* organization
* 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 OrganizationGetEntitiesCountRequest {
/**
* Search criteria for projects
*/
projects?: object;
/**
* Search criteria for experiments
*/
tasks?: object;
/**
* Search criteria for models
*/
models?: object;
/**
* Search criteria for dataviews
*/
dataviews?: object;
/**
* Search criteria for hyper datasets
*/
hyper_datasets?: object;
/**
* Search criteria for pipelines
*/
pipelines?: object;
datasets: object;
}

View File

@@ -0,0 +1,40 @@
/**
* organization
* 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 OrganizationGetEntitiesCountResponse {
/**
* The number of projects matching the criteria
*/
projects?: number;
/**
* The number of experiments matching the criteria
*/
tasks?: number;
/**
* The number of models matching the criteria
*/
models?: number;
/**
* The number of dataviews matching the criteria
*/
dataviews?: number;
/**
* The number of hyper datasets matching the criteria
*/
hyper_datasets?: number;
/**
* The number of pipelines matching the criteria
*/
pipelines?: number;
}

View File

@@ -1,5 +1,6 @@
import {ProjectsGetAllResponseSingleSubProjects} from '~/business-logic/model/projects/projectsGetAllResponseSingleSubProjects';
import {Stats} from '~/business-logic/model/projects/stats';
import {ProjectsGetAllResponseSingleDatasetStats} from '~/business-logic/model/projects/projectsGetAllResponseSingleDatasetStats';
/**
* projects
@@ -24,6 +25,10 @@ export interface Project {
* Project name
*/
name?: string;
/**
* Project base name
*/
basename?: string;
/**
* Project description
*/
@@ -53,6 +58,7 @@ export interface Project {
*/
default_output_destination?: string;
stats?: Stats;
dataset_stats?: ProjectsGetAllResponseSingleDatasetStats;
/**
* Last project update time. Reflects the last time the project metadata was changed or a task in this project has changed status
*/

View File

@@ -94,6 +94,7 @@ export interface ProjectsGetAllExRequest {
*/
size?: number;
stats_with_children?: boolean;
include_stats_filter?: any;
}
export namespace ProjectsGetAllExRequest {
export type StatsForStateEnum = 'active' | 'archived';

View File

@@ -0,0 +1,27 @@
/**
* projects
* 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.
*/
/**
* Project dataset statistics
*/
export interface ProjectsGetAllResponseSingleDatasetStats {
/**
* The number of files stored in the dataset
*/
file_count?: number;
/**
* The total dataset size in bytes
*/
total_size?: number;
}

View File

@@ -12,9 +12,10 @@
export type FeaturesEnum = 'experiments' | 'queues';
export type FeaturesEnum = 'experiments' | 'queues' | 'pipelines';
export const FeaturesEnum = {
Experiments: 'experiments' as FeaturesEnum,
Queues: 'queues' as FeaturesEnum
Queues: 'queues' as FeaturesEnum,
Pipelines: 'pipelines' as FeaturesEnum
};

View File

@@ -25,6 +25,7 @@ export interface GetCurrentUserResponseUserObject {
avatar?: string;
company?: GetCurrentUserResponseUserObjectCompany;
email?: string;
getting_started?: any;
/**
* User preferences
*/

View File

@@ -15,4 +15,5 @@ import { GetCurrentUserResponseUserObject } from '././getCurrentUserResponseUser
export interface UsersGetCurrentUserResponse {
user?: GetCurrentUserResponseUserObject;
getting_started?: object;
}

View File

@@ -1,7 +1,8 @@
import {createAction, props} from '@ngrx/store';
import {USERS_PREFIX} from '../../app.constants';
import {GetCurrentUserResponseUserObject} from '../../business-logic/model/users/getCurrentUserResponseUserObject';
import {USERS_PREFIX} from '~/app.constants';
import {GetCurrentUserResponseUserObject} from '~/business-logic/model/users/getCurrentUserResponseUserObject';
export const setCurrentUser = createAction(USERS_PREFIX + 'SET_CURRENT_USER',
props<{user: GetCurrentUserResponseUserObject; terms_of_use?: any}>()
// eslint-disable-next-line @typescript-eslint/naming-convention
props<{user: GetCurrentUserResponseUserObject; terms_of_use?: any; getting_started?: any}>()
);

View File

@@ -94,7 +94,7 @@ export const localStorageReducer = (reducer: ActionReducer<any>): ActionReducer<
const userPrefMetaFactory = (userPreferences: UserPreferences): MetaReducer<any>[] => [
(reducer: ActionReducer<any>) =>
createUserPrefReducer('users', ['activeWorkspace'], [USERS_PREFIX], userPreferences, reducer),
createUserPrefReducer('users', ['activeWorkspace', 'showOnlyUserWork'], [USERS_PREFIX], userPreferences, reducer),
(reducer: ActionReducer<any>) =>
createUserPrefReducer('rootProjects', ['tagsColors', 'graphVariant'], [ROOT_PROJECTS_PREFIX], userPreferences, reducer),
(reducer: ActionReducer<any>) =>

View File

@@ -8,6 +8,9 @@ import {deactivateLoader} from '@common/core/actions/layout.actions';
import {ALL_PROJECTS_OBJECT} from '@common/core/effects/projects.effects';
import {requestFailed} from '@common/core/actions/http.actions';
import {ApiProjectsService} from '~/business-logic/api-services/projects.service';
import {selectCurrentUser, selectShowOnlyUserWork} from '@common/core/reducers/users-reducer';
import {ProjectsGetAllExRequest} from '~/business-logic/model/projects/projectsGetAllExRequest';
import {selectShowHidden} from '@common/projects/common-projects.reducer';
@@ -23,8 +26,13 @@ export class ProjectsEffects {
getSelectedProject = createEffect(() => this.actions$.pipe(
ofType(actions.setSelectedProjectId),
withLatestFrom(this.store.select(selectSelectedProjectId)),
switchMap(([action, selectedProjectId]) => {
withLatestFrom(
this.store.select(selectSelectedProjectId),
this.store.select(selectCurrentUser),
this.store.select(selectShowOnlyUserWork),
this.store.select(selectShowHidden),
),
switchMap(([action, selectedProjectId, user, showOnlyUserWork, showHidden]) => {
if (!action.projectId) {
return [
deactivateLoader(action.type),
@@ -45,9 +53,12 @@ export class ProjectsEffects {
/* eslint-disable @typescript-eslint/naming-convention */
id: [action.projectId],
include_stats: true,
...((action.example !== false || this.fetchingExampleExperiment === action.projectId) && {check_own_contents: true})
...(!showHidden && {include_stats_filter: {system_tags: ['-pipeline']}}),
...((action.example !== false || this.fetchingExampleExperiment === action.projectId) && {check_own_contents: true}),
...(showOnlyUserWork && {active_users: [user.id]}),
...(showHidden && {search_hidden: true}),
/* eslint-enable @typescript-eslint/naming-convention */
})
} as ProjectsGetAllExRequest)
.pipe(
finalize(() => this.fetchingExampleExperiment = null),
mergeMap(({projects}) => [

View File

@@ -12,6 +12,7 @@ export const usersReducer = createReducer<UsersState>(initUsers,
on(setCurrentUser, (state, action) => ({
...state,
currentUser: action.user,
gettingStarted: action.getting_started,
activeWorkspace: action.user?.company,
userWorkspaces: [action.user?.company],
}))

View File

@@ -0,0 +1,32 @@
export type ActiveSearchLink = 'projects' | 'experiments' | 'models' | 'pipelines' | 'datasets';
export const activeSearchLink = {
projects: 'projects' as ActiveSearchLink,
experiments: 'experiments' as ActiveSearchLink,
models: 'models' as ActiveSearchLink,
pipelines: 'pipelines' as ActiveSearchLink,
openDatasets: 'datasets' as ActiveSearchLink
};
export const activeLinksList = [
{
label: 'PROJECTS',
name: activeSearchLink.projects,
},
{
label: 'DATASETS',
name: activeSearchLink.openDatasets,
},
{
label: 'EXPERIMENTS',
name: activeSearchLink.experiments,
},
{
label: 'MODELS',
name: activeSearchLink.models,
},
{
label: 'PIPELINES',
name: activeSearchLink.pipelines,
}
];

View File

@@ -0,0 +1,26 @@
import {Injectable} from '@angular/core';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {ApiProjectsService} from '~/business-logic/api-services/projects.service';
import {map, switchMap} from 'rxjs/operators';
import {getResultsCount, setResultsCount} from '@common/dashboard-search/dashboard-search.actions';
import {getEntityStatQuery} from '@common/dashboard-search/dashboard-search.effects';
import {ApiOrganizationService} from '~/business-logic/api-services/organization.service';
@Injectable()
export class DashboardSearchEffects {
constructor(
private actions: Actions,
public projectsApi: ApiProjectsService,
public organizationApi: ApiOrganizationService,
) {
}
getResultsCount = createEffect(() => this.actions.pipe(
ofType(getResultsCount),
switchMap(action => this.organizationApi.organizationGetEntitiesCount(getEntityStatQuery(action))),
map(({tasks: experiments, ...rest}) =>
setResultsCount({counts: {...rest, experiments}}))
));
}

View File

@@ -1,13 +0,0 @@
<sm-search-results-page
*ngIf="activeSearch$ | async"
(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>

View File

@@ -1,45 +0,0 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
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 '@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',
templateUrl: './dashboard-search.component.html',
styleUrls : ['./dashboard-search.component.scss']
})
export class DashboardSearchComponent extends DashboardSearchComponentBase implements OnInit, OnDestroy {
readonly tabsIndexes = ['projects', 'experiments', 'models'];
private allResultsJoin$: Observable<[ObservedValueOf<Observable<Array<Project>>>, ObservedValueOf<Observable<any>>, ObservedValueOf<Observable<Array<Model>>>, ObservedValueOf<Observable<number>>]>;
private allResultsSubscription: Subscription;
constructor(public store: Store<any>, public router: Router) {
super(store);
this.allResultsJoin$ = combineLatest([this.projectsResults$, this.experimentsResults$, this.modelsResults$, this.resultsCounter$]);
this.syncAppSearch();
}
public ngOnInit(): void {
this.allResultsSubscription = this.allResultsJoin$.pipe(
skip(1),
filter(allResults => allResults[this.tabsIndexes.length] === this.tabsIndexes.length)
).subscribe(allResults => this.setFirstActiveLink(allResults, this.tabsIndexes));
}
ngOnDestroy(): void {
this.store.dispatch(new SearchClear());
this.searchTermChanged('');
this.stopSyncSearch();
this.allResultsSubscription.unsubscribe();
}
}

View File

@@ -3,7 +3,8 @@ import {CommonModule} from '@angular/common';
import {SMSharedModule} from '@common/shared/shared.module';
import {StoreModule} from '@ngrx/store';
import {EffectsModule} from '@ngrx/effects';
import {DashboardSearchEffects} from '@common/dashboard-search/dashboard-search.effects';
import {DashboardSearchEffects as commonDashboardSearchEffects} from '@common/dashboard-search/dashboard-search.effects';
import {DashboardSearchEffects} from '~/features/dashboard-search/dashboard-search.effects';
import {ProjectsSharedModule} from '../../projects/shared/projects-shared.module';
import {SharedModule} from '~/shared/shared.module';
import {dashboardSearchReducer} from '@common/dashboard-search/dashboard-search.reducer';
@@ -14,7 +15,7 @@ import {dashboardSearchReducer} from '@common/dashboard-search/dashboard-search.
SMSharedModule,
ProjectsSharedModule,
StoreModule.forFeature('search', dashboardSearchReducer),
EffectsModule.forFeature([DashboardSearchEffects]),
EffectsModule.forFeature([DashboardSearchEffects, commonDashboardSearchEffects]),
SharedModule
],
})

View File

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

View File

@@ -7,12 +7,12 @@ import {GetCurrentUserResponseUserObjectCompany} from '~/business-logic/model/us
import {filter, skip, take} from 'rxjs/operators';
import {setDeep} from '@common/core/actions/projects.actions';
import {getRecentProjects, getRecentExperiments} from '@common/dashboard/common-dashboard.actions';
import {selectActiveSearch} from '@common/common-search/common-search.reducer';
import {selectFirstLogin} from '@common/core/reducers/view.reducer';
import {MatDialog} from '@angular/material/dialog';
import {WelcomeMessageComponent} from '@common/layout/welcome-message/welcome-message.component';
import {firstLogin} from '@common/core/actions/layout.actions';
import {IRecentTask, selectRecentTasks} from '@common/dashboard/common-dashboard.reducer';
import {selectActiveSearch} from '@common/dashboard-search/dashboard-search.reducer';
@Component({

View File

@@ -8,13 +8,15 @@ import {GettingStartedCardComponent} from './dumb/getting-started-card/getting-s
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 {DashboardSearchModule} from './dashboard-search/dashboard-search.module';
import {ProjectDialogModule} from '@common/shared/project-dialog/project-dialog.module';
import {ProjectsSharedModule} from '../projects/shared/projects-shared.module';
import {SearchResultsComponent} from '@common/dashboard-search/dumb/search-results/search-results.component';
import {DashboardSearchBaseComponent} from '@common/dashboard/dashboard-search.component.base';
import {DatasetsModule} from '~/features/datasets/datasets.module';
import {DatasetsSharedModule} from '~/features/datasets/shared/datasets-shared.module';
@NgModule({
imports: [
@@ -27,9 +29,11 @@ import {SearchResultsComponent} from '@common/dashboard-search/dumb/search-resul
StoreModule.forFeature('dashboard', commonDashboardReducer),
CommonDashboardModule,
SharedModule,
DashboardSearchModule
DashboardSearchModule,
DatasetsModule,
DatasetsSharedModule
],
declarations : [DashboardComponent, GettingStartedCardComponent, DashboardSearchComponent, SearchResultsPageComponent, SearchResultsComponent]
declarations : [DashboardComponent, GettingStartedCardComponent, DashboardSearchBaseComponent, SearchResultsPageComponent, SearchResultsComponent]
})
export class DashboardModule {
}

View File

@@ -1,27 +1,26 @@
<div class="search-container">
<div class="container">
<div class="d-flex-center">
<div class="pl-3 py-3">
<span [ngClass]="{'active': activeLink === 'projects'}" class="pointer category-link"
(click)="activeLinkChanged.emit('projects')">PROJECTS {{'(' + projectsList.length + ')'}} </span>
<span [ngClass]="{'active': activeLink === 'experiments'}" class="pointer category-link"
(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>
<ng-container *ngFor="let searchTab of activeLinksList">
<span [class.active]="activeLink === searchTab.name" class="pointer category-link"
(click)="activeLinkChanged.emit(searchTab.name)">{{searchTab.label}} ({{resultsCount?.[searchTab.name]}}) </span>
</ng-container>
</div>
</div>
<div class="page-container">
<sm-search-results
[cardTemplate]="
activeLink === 'projects' ? ProjectTemplate :
activeLink === 'experiments' ? ExperimentTemplate :
activeLink === 'models' ? ModelsTemplate :
activeLink === 'pipelines' ? PipelineTemplate :
activeLink === searchPages.projects ? ProjectTemplate :
activeLink === searchPages.experiments ? ExperimentTemplate :
activeLink === searchPages.models ? ModelsTemplate :
activeLink === searchPages.openDatasets ? openDatasetTemplate :
activeLink === searchPages.pipelines ? PipelineTemplate :
ProjectTemplate"
[results]="getResults()"
[cardHeight]="getCardHeight()"
(resultClicked)="projectClicked($event)">
[showLoadMoreButton]="getResults().length < resultsCount?.[activeLink]"
(resultClicked)="projectClicked($event)"
(loadMoreClicked)="loadMoreClicked.emit()">
</sm-search-results>
</div>
@@ -54,4 +53,13 @@
(projectCardClicked)="pipelineClicked($event)"
></sm-pipeline-card>
</ng-template>
<ng-template #openDatasetTemplate let-dataset="result">
<sm-simple-dataset-card
[hideMenu]="true"
[project]="dataset"
(projectCardClicked)="openDatasetClicked($event)"
></sm-simple-dataset-card>
</ng-template>
</div>

View File

@@ -2,7 +2,7 @@
@import "../../../../webapp-common/shared/ui-components/styles/mixins/common";
.category-link{
padding-right: 10px;
padding-right: 24px;
font-size: $font-size-lg;
color: $blue-100;
opacity: 0.3;

View File

@@ -3,25 +3,32 @@ 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';
import {activeLinksList, ActiveSearchLink, activeSearchLink} from '~/features/dashboard-search/dashboard-search.consts';
@Component({
selector : 'sm-search-results-page',
selector: 'sm-search-results-page',
templateUrl: './search-results-page.component.html',
styleUrls : ['./search-results-page.component.scss']
styleUrls: ['./search-results-page.component.scss']
})
export class SearchResultsPageComponent {
public searchPages = activeSearchLink;
public activeLinksList = activeLinksList;
@Input() projectsList: Array<Project> = [];
@Input() experimentsList: Array<Task> = [];
@Input() modelsList: Array<Model> = [];
@Input() modelsList: Array<Model> = [];
@Input() pipelinesList: Array<Project> = [];
@Input() datasetsList: Array<Project> = [];
@Input() activeLink: ActiveSearchLink;
@Input() resultsCount: Map<ActiveSearchLink, number>;
@Output() projectSelected = new EventEmitter<Project>();
@Output() activeLinkChanged = new EventEmitter<string>();
@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>();
@Output() modelSelected = new EventEmitter<Model>();
@Output() pipelineSelected = new EventEmitter<Project>();
@Output() openDatasetSelected = new EventEmitter<Project>();
@Output() loadMoreClicked = new EventEmitter();
public projectClicked(project: Project) {
this.projectSelected.emit(project);
@@ -39,18 +46,20 @@ export class SearchResultsPageComponent {
this.pipelineSelected.emit(pipeline);
}
getResults() {
return this[`${this.activeLink}List`];
public openDatasetClicked(project: Project) {
this.openDatasetSelected.emit(project);
}
getResults = () => this[`${this.activeLink}List`];
getCardHeight() {
switch (this.activeLink) {
case 'projects':
case activeSearchLink.projects:
return 246;
case 'experiments':
case 'models':
case activeSearchLink.experiments:
case activeSearchLink.models:
return 264;
case 'pipelines':
case activeSearchLink.pipelines:
return 226;
default:
return 250;

View File

@@ -0,0 +1,43 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {SimpleDatasetVersionsComponent} from '@common/datasets/simple-dataset-versions/simple-dataset-versions.component';
import {
SimpleDatasetVersionInfoComponent
} from '@common/datasets/simple-dataset-version-info/simple-dataset-version-info.component';
import {SimpleDatasetsComponent} from '@common/datasets/simple-datasets/simple-datasets.component';
import {EntityTypeEnum} from '../../shared/constants/non-common-consts';
const routes: Routes = [
{
path : '',
component: SimpleDatasetsComponent,
data : {search: true}
},
{
path: 'simple/:projectId',
data: {search: true},
children: [
{path: '', redirectTo: 'experiments', pathMatch: 'full'},
{
path: 'experiments',
component: SimpleDatasetVersionsComponent,
children: [
{
path: ':versionId', component: SimpleDatasetVersionInfoComponent,
},
]
},
{
path: 'compare-experiments',
data: {entityType: EntityTypeEnum.dataset},
loadChildren: () => import('@common/experiments-compare/experiments-compare.module').then(m => m.ExperimentsCompareModule)
},
]
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DatasetsRoutingModule {}

View File

@@ -0,0 +1,77 @@
import {CommonModule} from '@angular/common';
import {NgModule, NO_ERRORS_SCHEMA} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {SMSharedModule} from '@common/shared/shared.module';
import {ChipsModule} from '@common/shared/ui-components/buttons/chips/chips.module';
import {SimpleDatasetsComponent} from '@common/datasets/simple-datasets/simple-datasets.component';
import {CommonProjectsModule} from '@common/projects/common-projects.module';
import {ProjectsSharedModule} from '~/features/projects/shared/projects-shared.module';
import {ExperimentSharedModule} from '~/features/experiments/shared/experiment-shared.module';
import {ExperimentCompareSharedModule} from '@common/experiments-compare/shared/experiment-compare-shared.module';
import {ExperimentsCommonModule} from '@common/experiments/common-experiments.module';
import {CommonExperimentSharedModule} from '@common/experiments/shared/common-experiment-shared.module';
import {
SimpleDatasetVersionsComponent
} from '@common/datasets/simple-dataset-versions/simple-dataset-versions.component';
import {AngularSplitModule} from 'angular-split';
import {
SimpleDatasetVersionMenuComponent
} from '@common/datasets/simple-dataset-version-menu/simple-dataset-version-menu.component';
import {
SimpleDatasetVersionInfoComponent
} from '@common/datasets/simple-dataset-version-info/simple-dataset-version-info.component';
import {PipelinesControllerModule} from '@common/pipelines-controller/pipelines-controller.module';
import {DatasetsRoutingModule} from '~/features/datasets/datasets-routing.module';
import {DatasetVersionStepComponent} from '@common/datasets/dataset-version-step/dataset-version-step.component';
import {DatasetsSharedModule} from '~/features/datasets/shared/datasets-shared.module';
import {
SimpleDatasetVersionDetailsComponent
} from '../../webapp-common/datasets/simple-dataset-version-details/simple-dataset-version-details.component';
import {
SimpleDatasetVersionContentComponent
} from '../../webapp-common/datasets/simple-dataset-version-content/simple-dataset-version-content.component';
import {
SimpleDatasetVersionPreviewComponent
} from '../../webapp-common/datasets/simple-dataset-version-preview/simple-dataset-version-preview.component';
import {DebugImagesModule} from '../../webapp-common/debug-images/debug-images.module';
import {
ExperimentOutputLogModule
} from '../../webapp-common/experiments/shared/experiment-output-log/experiment-output-log.module';
@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
SMSharedModule,
ChipsModule,
FormsModule,
AngularSplitModule,
CommonProjectsModule,
ProjectsSharedModule,
CommonExperimentSharedModule,
ExperimentSharedModule,
DatasetsRoutingModule,
ExperimentsCommonModule,
ExperimentCompareSharedModule,
PipelinesControllerModule,
DatasetsSharedModule,
DebugImagesModule,
ExperimentOutputLogModule
],
declarations: [
SimpleDatasetsComponent,
SimpleDatasetVersionsComponent,
SimpleDatasetVersionMenuComponent,
SimpleDatasetVersionInfoComponent,
DatasetVersionStepComponent,
SimpleDatasetVersionDetailsComponent,
SimpleDatasetVersionContentComponent,
SimpleDatasetVersionPreviewComponent,
],
schemas: [NO_ERRORS_SCHEMA],
exports: []
})
export class DatasetsModule {
}

View File

@@ -0,0 +1,28 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {SMSharedModule} from '@common/shared/shared.module';
import {ChipsModule} from '@common/shared/ui-components/buttons/chips/chips.module';
import {SharedModule} from '~/shared/shared.module';
import {SimpleDatasetCardComponent} from '@common/datasets/simple-dataset-card/simple-dataset-card.component';
import {ProjectsSharedModule} from '~/features/projects/shared/projects-shared.module';
const _declerations = [
SimpleDatasetCardComponent
];
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
SMSharedModule,
ChipsModule,
SharedModule,
ProjectsSharedModule,
],
declarations: [..._declerations],
exports : [..._declerations]
})
export class DatasetsSharedModule { }

View File

@@ -25,7 +25,8 @@ export abstract class ExperimentDetailsReverterServiceBase {
tags: exp.tags,
execution: this.revertExecution(exp),
artifacts: this.revertArtifacts(exp),
configuration: this.revertconfiguration(exp.configuration)
configuration: this.revertconfiguration(exp.configuration),
info: this.revertInfo(exp)
};
});
}
@@ -56,4 +57,6 @@ export abstract class ExperimentDetailsReverterServiceBase {
abstract revertArtifacts(exp: ITask);
abstract revertExecution(exp: ITask);
abstract revertInfo(exp: ITask);
}

View File

@@ -4,6 +4,7 @@ export abstract class ExperimentCompareDetailsBase {
artifacts: this.buildSectionTree(experiment, 'artifacts', mergedExperiment),
execution: this.buildSectionTree(experiment, 'execution', mergedExperiment),
configuration: this.buildSectionTree(experiment, 'configuration', mergedExperiment),
info: this.buildSectionTree(experiment, 'info', mergedExperiment),
};
}

View File

@@ -1,58 +1,66 @@
<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>
<nav (smOverflows)="navbarOverflowed($event)" [overflowTrigger]="splitSize" [overflowDelay]="800"
[class.minimized]="minimized">
<span [routerLink]="['execution']" routerLinkActive="active" #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="active" #rlaModel="routerLinkActive" queryParamsHandling="preserve">
<sm-navbar-item header="artifacts" class="small-nav" [active]="rlaModel.isActive"></sm-navbar-item>
</span>
<span [routerLink]="['general']" routerLinkActive="active" #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>
<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>
<span
[routerLink]="baseInfoRoute.concat(['log'])"
routerLinkActive="active"
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="active"
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="active"
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="active"
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>
<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

@@ -1,7 +1,6 @@
<sm-overlay [backdropActive]="backdropActive$|async"></sm-overlay>
<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

View File

@@ -1,19 +1,25 @@
import {Injectable} from '@angular/core';
import {Actions} from '@ngrx/effects';
import {Store} from '@ngrx/store';
import {ApiTasksService} from '../../../business-logic/api-services/tasks.service';
import {ApiAuthService} from '../../../business-logic/api-services/auth.service';
import {BlTasksService} from '../../../business-logic/services/tasks.service';
import {ApiEventsService} from '../../../business-logic/api-services/events.service';
import {CommonExperimentOutputState} from '../../../webapp-common/experiments/reducers/common-experiment-output.reducer';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import { downloadFullLog } from '@common/experiments/actions/common-experiment-output.actions';
import {filter, map} from 'rxjs/operators';
import {HTTP} from '~/app.constants';
@Injectable()
export class ExperimentOutputEffects {
constructor(private actions$: Actions, private store: Store<CommonExperimentOutputState>, private apiTasks: ApiTasksService,
private authApi: ApiAuthService, private taskBl: BlTasksService, private eventsApi: ApiEventsService) {
constructor(private actions$: Actions) {
}
downloadFullLog$ = createEffect(() => this.actions$.pipe(
ofType(downloadFullLog),
filter(action => !!action.experimentId),
map(action => {
const a = document.createElement('a');
a.href = `${HTTP.API_BASE_URL}/events.download_task_log?line_type=text&task=${action.experimentId}`;
a.target = '_blank';
a.download = 'Log';
a.click();
})
), {dispatch: false});
}

View File

@@ -1,7 +1,5 @@
import {TaskStatusEnum} from '../../../business-logic/model/tasks/taskStatusEnum';
import {TaskStatusEnum} from '~/business-logic/model/tasks/taskStatusEnum';
import {ExperimentTableColFieldsEnum} from './experiments.model';
import {TaskTypeEnum} from '~/business-logic/model/tasks/taskTypeEnum';
import {Model} from '~/business-logic/model/models/model';
export type experimentSectionsEnum =
'MODEL_INPUT'
@@ -62,3 +60,11 @@ export const EXPERIMENTS_STATUS_LABELS = {
export const DevWarningEnabled = false;
export const excludeTypes = [];
export const DATASETS_STATUS_LABEL = {
[TaskStatusEnum.InProgress]: 'Uploading',
[TaskStatusEnum.Completed]: 'Final',
Running: 'Uploading',
Completed: 'Final'
};

View File

@@ -1,13 +1,13 @@
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 {ProjectRouterModule} from './projects-routing.module';
import {projectsReducer} from './projects.reducer';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {CommonProjectsModule} from '../../webapp-common/projects/common-projects.module';
import {CommonProjectsModule} from '@common/projects/common-projects.module';
export const projectSyncedKeys = ['showHidden', 'tableModeAwareness'];
export const projectSyncedKeys = ['showHidden', 'tableModeAwareness', 'orderBy', 'sortOrder'];
@NgModule({
imports : [

View File

@@ -6,7 +6,8 @@ import {selectSelectedTableModel} from '@common/models/reducers';
import {createSelector} from '@ngrx/store';
import {selectSelectedExperiment} from '~/features/experiments/reducers';
import {selectRootProjects, selectSelectedProject} from '@common/core/reducers/projects.reducer';
import {formatStaticCrumb, prepareLinkData} from '@common/layout/breadcrumbs/breadcrumbs-common.utils';
import {formatStaticCrumb as commonFormatStaticCrumb, prepareLinkData} from '@common/layout/breadcrumbs/breadcrumbs-common.utils';
import {IBreadcrumbsLink} from '@common/layout/breadcrumbs/breadcrumbs.component';
export interface IBreadcrumbs {
project: Project;
@@ -16,33 +17,42 @@ export interface IBreadcrumbs {
task: Task;
}
export const formatStaticCrumb = (crumb: string): IBreadcrumbsLink => {
if (!crumb) {
return {url: null, name: null};
}
return commonFormatStaticCrumb(crumb);
};
export const selectBreadcrumbsStringsBase = createSelector(
selectSelectedProject, selectSelectedExperiment, selectSelectedTableModel, selectRootProjects,
(project, experiment, model, projects) =>
({project, experiment, model, projects}) as IBreadcrumbs);
export const prepareNames = (data: IBreadcrumbs, isPipeline?: boolean, fullScreen = false) => {
export const prepareNames = (data: IBreadcrumbs, customProject?: boolean, fullScreen = false) => {
const project = prepareLinkData(data.project, true);
if (data.project) {
const subProjects = [];
let subProjectsNames = [data.project?.name];
if (!isPipeline) {
if (!customProject) {
subProjectsNames = data.project?.name?.split('/');
}
const allProjects = [
...data.projects,
{id: '*', name: 'All Experiments'},
data.project
];
let currentName = '';
subProjectsNames.forEach(name => {
const subProjects = subProjectsNames.map(name => {
currentName += currentName ? ('/' + name) : name;
const foundProject = [
...data.projects,
{id: '*', name: 'All Experiments'},
{...data.project}
].find(proj => currentName === proj.name);
subProjects.push(foundProject);
});
const subProjectsLinks = subProjects.map(subProject => ({
return allProjects.find(proj => currentName === proj.name);
}) || [];
const subProjectsLinks = subProjects.map((subProject, index, arr) => ({
name: subProject?.name.substring(subProject?.name.lastIndexOf('/') + 1),
url: isPipeline ? `pipelines/${subProject?.id}/experiments` :
fullScreen ? `projects/${subProject?.id}/experiments/${data.experiment.id}` :
url: customProject ?
data.project?.system_tags?.includes('pipeline') ? `pipelines/${subProject?.id}/experiments` : '' :
fullScreen && index === (arr.length - 1) ? `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`
@@ -50,16 +60,21 @@ export const prepareNames = (data: IBreadcrumbs, isPipeline?: boolean, fullScree
project.name = project?.name.substring(project.name.lastIndexOf('/') + 1);
project.subCrumbs = subProjectsLinks;
}
const task = prepareLinkData(data.task);
const task = prepareLinkData(data.task);
const experiment = (data.experiment) ? prepareLinkData(data.experiment, true) : {};
const output = formatStaticCrumb('');
const accountAdministration = formatStaticCrumb('account-administration');
const output = formatStaticCrumb('');
const accountAdministration = formatStaticCrumb('account-administration');
const experiments = formatStaticCrumb('experiments');
const models = formatStaticCrumb('models');
const compare = formatStaticCrumb('compare-experiments');
const models = formatStaticCrumb('models');
const compare = customProject ?
data.project?.system_tags?.includes('pipeline') ?
{url: 'compare-experiments', name: 'Compare Runs'} :
{url: 'compare-experiments', name: 'Compare Versions'}
: formatStaticCrumb('compare-experiments');
return {
...(project.url !== '*' && {':projectId': project}),
':taskId' : task,
':taskId': task,
':controllerId': experiment,
'compare-experiments': compare,
output,

View File

@@ -1,53 +1,67 @@
<!-- DON'T FORGET TO ADD LINK TO 404 PAGE TOO -->
<div class="side-nav">
<div class="item logo">
<div class="item-icon">
<img src="assets/c-logo.svg?v=1" class="logo-a" alt="logo">
</div>
<div class="caption">
<!-- <img src="../../../assets/logo-white.png" class="logo-full">-->
</div>
</div>
<ng-container *ngIf="currentUser">
<a class="item d-block" #rlaHome="routerLinkActive" routerLink="/dashboard" routerLinkActive (click)="resetSearch()"
smTooltip="DASHBOARD" [matTooltipShowDelay]="0" matTooltipPosition="right">
<a class="item d-block"
routerLink="/dashboard"
routerLinkActive="active"
(click)="resetSearch()"
smTooltip="DASHBOARD"
[matTooltipShowDelay]="0"
matTooltipPosition="right"
>
<div class="item-icon">
<i [class]="'al-icon al-ico-home al-color ' + (rlaHome.isActive ? 'neon-yellow': 'blue-300')" ></i>
<i class="al-icon al-ico-home al-color blue-300"></i>
</div>
<div class="caption">dashboard</div>
</a>
<a class="item d-block" smTooltip="PROJECTS" [matTooltipShowDelay]="0" matTooltipPosition="right" routerLink="/projects"
[routerLinkActive] #rlaProjects="routerLinkActive">
<div class="item-icon">
<i [class]="'al-icon al-ico-projects al-color ' + (rlaProjects.isActive ? 'neon-yellow': 'blue-300')" ></i>
<a class="item d-block"
smTooltip="PROJECTS"
[matTooltipShowDelay]="0"
matTooltipPosition="right"
routerLink="/projects"
routerLinkActive="active"
>
<div class="item-icon">
<i class="al-icon al-ico-projects al-color blue-300"></i>
</div>
<div class="caption">projects</div>
</a>
<a class="item d-block"
routerLink="/datasets"
routerLinkActive="active"
smTooltip="DATASETS"
[matTooltipShowDelay]="0"
matTooltipPosition="right">
<div class="item-icon">
<i class="al-icon al-ico-datasets al-color blue-300"></i>
</div>
<div class="caption">datasets</div>
</a>
<a class="item d-block"
routerLink="/pipelines"
routerLinkActive
#rlaDatasets="routerLinkActive"
routerLinkActive="active"
smTooltip="PIPELINES"
[matTooltipShowDelay]="0"
matTooltipPosition="right">
<div class="item-icon">
<i [class]="'al-icon al-ico-pipelines al-color ' + (rlaDatasets.isActive ? 'neon-yellow': 'blue-300')"></i>
<i class="al-icon al-ico-pipelines al-color blue-300"></i>
</div>
<div class="caption">pipelines</div>
</a>
<a class="item d-block"
*smCheckPermission="true"
routerLink="/workers-and-queues"
routerLinkActive
#rlaQueues="routerLinkActive"
routerLinkActive="active"
smTooltip="WORKERS & QUEUES"
[matTooltipShowDelay]="0"
matTooltipPosition="right">
<div class="item-icon">
<i [class]="'al-icon al-ico-queues al-color ' + (rlaQueues.isActive ? 'neon-yellow': 'blue-300')" ></i>
<i class="al-icon al-ico-queues al-color blue-300"></i>
</div>
<div class="caption">workers & queues</div>
</a>
@@ -66,14 +80,13 @@
</div>
<div class="caption">Ignite</div>
</a>
<a class="item d-block" href="https://github.com/allegroai/clearml" target="_blank"
smTooltip="GitHub Repository"
[matTooltipShowDelay]="0"
matTooltipPosition="right">
<a *ngIf="environment.slackLink" class="item d-block" [href]="environment.slackLink"
target="_blank"
smTooltip="Community support on Slack" [matTooltipShowDelay]="0" matTooltipPosition="right">
<div class="item-icon">
<i class="fab fa-github fa-24 al-color blue-300"></i>
<i class="al-icon al-ico-slack md al-color blue-300"></i>
</div>
<div class="caption">GitHub</div>
<div class="caption">Slack</div>
</a>
</div>
</div>

View File

@@ -21,7 +21,6 @@ $transition-speed: 0.15;
transition: opacity $transition-speed + $transition-delay + s;
opacity: 1;
height: 100%;
}
&:hover {
@@ -29,6 +28,7 @@ $transition-speed: 0.15;
box-shadow: unset;
}
}
a:hover {
@@ -67,8 +67,11 @@ $transition-speed: 0.15;
height: 64px;
width: 64px;
}
.neon {
color: $neon-yellow-betterinchrome;
}
&.active {
.item-icon i {
color: $neon-yellow
}
}

View File

@@ -5,7 +5,7 @@ import {selectSelectedProjectId} from '../../webapp-common/core/reducers/project
import {Observable} from 'rxjs';
import {Router} from '@angular/router';
import {ConfigurationService} from '../../webapp-common/shared/services/configuration.service';
import {SearchDeactivate} from '../../webapp-common/dashboard-search/dashboard-search.actions';
import {searchDeactivate} from '../../webapp-common/dashboard-search/dashboard-search.actions';
@Component({
selector : 'sm-side-nav',
@@ -25,7 +25,7 @@ export class SideNavComponent {
public resetSearch() {
this.store.dispatch(new SearchDeactivate());
this.store.dispatch(searchDeactivate());
}
get guestUser(): boolean {

View File

@@ -5,7 +5,8 @@ export enum EntityTypeEnum {
experiment = 'experiment',
model = 'model',
project = 'project',
controller = 'pipeline run'
controller = 'pipeline run',
dataset = 'version'
}
export enum CircleTypeEnum {

View File

@@ -2,7 +2,7 @@
@font-face {
font-family: '#{$icomoon-font-family}';
src: url('./#{$icomoon-font-family}.ttf?2i0eh5') format('truetype');
src: url('./#{$icomoon-font-family}.ttf?8714dr') format('truetype');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -23,6 +23,26 @@
-moz-osx-font-smoothing: grayscale;
}
.al-ico-upload {
&:before {
content: $al-ico-upload;
}
}
.al-ico-min-panel {
&:before {
content: $al-ico-min-panel;
}
}
.al-ico-max-panel {
&:before {
content: $al-ico-max-panel;
}
}
.al-ico-datasets {
&:before {
content: $al-ico-datasets;
}
}
.al-ico-t-logo-b {
&:before {
content: $al-ico-t-logo-b;
@@ -43,11 +63,6 @@
content: $al-ico-projects;
}
}
.al-ico-datasets {
&:before {
content: $al-ico-datasets;
}
}
.al-ico-queues {
&:before {
content: $al-ico-queues;
@@ -1171,6 +1186,11 @@
content: $al-ico-eye-outline;
}
}
.al-ico-csv {
&:before {
content: $al-ico-csv;
}
}
.al-ico-status-draft {
&:before {
content: $al-ico-status-draft;
@@ -1191,9 +1211,9 @@
content: $al-ico-status-pending;
}
}
.al-ico-status-skiped {
.al-ico-status-skipped {
&:before {
content: $al-ico-status-skiped;
content: $al-ico-status-skipped;
}
}
.al-ico-status-cached {
@@ -1226,9 +1246,4 @@
content: $al-ico-status-completed;
}
}
.al-ico-status-queued {
&:before {
content: $al-ico-enqueue;
}
}

View File

@@ -1,11 +1,14 @@
$icomoon-font-family: "trains" !default;
$icomoon-font-path: "fonts" !default;
$al-ico-upload: "\e9cc";
$al-ico-min-panel: "\e9ca";
$al-ico-max-panel: "\e9cb";
$al-ico-datasets: "\e90b";
$al-ico-t-logo-b: "\e908";
$al-ico-bars-menu: "\e933";
$al-ico-home: "\e909";
$al-ico-projects: "\e90a";
$al-ico-datasets: "\e90b";
$al-ico-queues: "\e90c";
$al-ico-annotator: "\e90d";
$al-ico-account: "\e998";
@@ -226,11 +229,12 @@ $al-ico-video: "\e9c5";
$al-ico-less-than: "\e9c6";
$al-ico-greater-than: "\e9c7";
$al-ico-eye-outline: "\e9c8";
$al-ico-csv: "\e9c9";
$al-ico-status-draft: "\e902";
$al-ico-status-published: "\e906";
$al-ico-status-aborted-sec: "\e918";
$al-ico-status-pending: "\e903";
$al-ico-status-skiped: "\e9bd";
$al-ico-status-skipped: "\e9bd";
$al-ico-status-cached: "\e9be";
$al-ico-status-executed: "\e9ac";
$al-ico-status-running: "\e904";

View File

@@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" width="338" height="284" viewBox="0 0 338 284">
<defs>
<style>
.cls-1 {
opacity: 0.996;
}
.cls-2 {
fill: none;
stroke: #5a658e;
stroke-width: 2px;
}
.cls-3 {
fill: rgba(0,154,255,0.2);
}
.cls-4 {
fill: #009aff;
}
.cls-5 {
fill: #153856;
}
.cls-6 {
fill: rgba(90,101,142,0.2);
}
.cls-7 {
fill: #5a658e;
}
.cls-8 {
fill: #1a1e2c;
}
</style>
</defs>
<g id="datasets-empty-state" class="cls-1" transform="translate(-526.845 -206)">
<g id="g" transform="translate(0 -26)">
<g id="connectors" transform="translate(574.84 261.145)">
<g id="connector" transform="translate(124.333 123.032) rotate(180)">
<line id="connector-2" data-name="connector" class="cls-2" y2="24.176" transform="translate(3.429 -3.103)"/>
<path id="connector-head" class="cls-2" d="M936.044,346.708l3.429,3.429,3.429-3.429" transform="translate(-936.044 -328.283)"/>
</g>
<g id="connector-3" data-name="connector" transform="translate(124.333 45.033) rotate(180)">
<line id="connector-4" data-name="connector" class="cls-2" y2="24.176" transform="translate(3.429 -3.103)"/>
<path id="connector-head-2" data-name="connector-head" class="cls-2" d="M936.044,346.708l3.429,3.429,3.429-3.429" transform="translate(-936.044 -328.283)"/>
</g>
<g id="connector-5" data-name="connector" transform="translate(124.333 201.033) rotate(180)">
<path id="connector-head-3" data-name="connector-head" class="cls-2" d="M936.044,346.708l3.429,3.429,3.429-3.429" transform="translate(-936.044 -328.283)"/>
<path id="Path_767" data-name="Path 767" class="cls-2" d="M9.81,21.056c0-44.9,22.7-49.264,31.507-49.264" transform="translate(-6.403)"/>
</g>
<g id="connector-6" data-name="connector" transform="translate(117.474 179.179)">
<path id="connector-head-4" data-name="connector-head" class="cls-2" d="M0,3.429,3.429,0,6.858,3.429" transform="translate(0 0)"/>
<path id="Path_767-2" data-name="Path 767" class="cls-2" d="M0,0C0,44.9,22.7,49.264,31.507,49.264" transform="translate(3.407 0.798)"/>
</g>
</g>
<g id="step" transform="translate(625.146 229)">
<path id="step-2" data-name="step" class="cls-3" d="M4,0H133a4,4,0,0,1,4,4V33a0,0,0,0,1,0,0H0a0,0,0,0,1,0,0V4A4,4,0,0,1,4,0Z" transform="translate(2.699 3)"/>
<path id="step-status" class="cls-4" d="M0,0H137a0,0,0,0,1,0,0V13a4,4,0,0,1-4,4H4a4,4,0,0,1-4-4V0A0,0,0,0,1,0,0Z" transform="translate(2.699 36)"/>
<rect id="time" class="cls-5" width="52" height="4" rx="2" transform="translate(79.699 43)"/>
<rect id="time-2" data-name="time" class="cls-5" width="20" height="4" rx="2" transform="translate(10.699 43)"/>
<rect id="title" class="cls-4" width="55" height="4" rx="2" transform="translate(10.699 17)"/>
</g>
<g id="step-3" data-name="step" transform="translate(625.146 307)">
<path id="step-4" data-name="step" class="cls-3" d="M4,0H133a4,4,0,0,1,4,4V33a0,0,0,0,1,0,0H0a0,0,0,0,1,0,0V4A4,4,0,0,1,4,0Z" transform="translate(2.699 3)"/>
<path id="step-status-2" data-name="step-status" class="cls-4" d="M0,0H137a0,0,0,0,1,0,0V13a4,4,0,0,1-4,4H4a4,4,0,0,1-4-4V0A0,0,0,0,1,0,0Z" transform="translate(2.699 36)"/>
<rect id="time-3" data-name="time" class="cls-5" width="34" height="4" rx="2" transform="translate(97.699 43)"/>
<rect id="time-4" data-name="time" class="cls-5" width="30" height="4" rx="2" transform="translate(10.699 43)"/>
<rect id="title-2" data-name="title" class="cls-4" width="36" height="4" rx="2" transform="translate(10.699 17)"/>
</g>
<g id="step-5" data-name="step" transform="translate(625.146 385)">
<path id="step-6" data-name="step" class="cls-3" d="M4,0H133a4,4,0,0,1,4,4V33a0,0,0,0,1,0,0H0a0,0,0,0,1,0,0V4A4,4,0,0,1,4,0Z" transform="translate(2.699 3)"/>
<path id="step-status-3" data-name="step-status" class="cls-4" d="M0,0H137a0,0,0,0,1,0,0V13a4,4,0,0,1-4,4H4a4,4,0,0,1-4-4V0A0,0,0,0,1,0,0Z" transform="translate(2.699 36)"/>
<rect id="time-5" data-name="time" class="cls-5" width="50" height="4" rx="2" transform="translate(81.699 43)"/>
<rect id="time-6" data-name="time" class="cls-5" width="26" height="4" rx="2" transform="translate(10.699 43)"/>
<rect id="title-3" data-name="title" class="cls-4" width="37" height="4" rx="2" transform="translate(10.699 17)"/>
</g>
<g id="step-7" data-name="step" transform="translate(725.146 463)">
<path id="step-8" data-name="step" class="cls-6" d="M4,0H133a4,4,0,0,1,4,4V33a0,0,0,0,1,0,0H0a0,0,0,0,1,0,0V4A4,4,0,0,1,4,0Z" transform="translate(2.699 3)"/>
<path id="step-status-4" data-name="step-status" class="cls-7" d="M0,0H137a0,0,0,0,1,0,0V13a4,4,0,0,1-4,4H4a4,4,0,0,1-4-4V0A0,0,0,0,1,0,0Z" transform="translate(2.699 36)"/>
<rect id="time-7" data-name="time" class="cls-8" width="50" height="4" rx="2" transform="translate(81.699 43)"/>
<rect id="time-8" data-name="time" class="cls-8" width="26" height="4" rx="2" transform="translate(10.699 43)"/>
<rect id="title-4" data-name="title" class="cls-7" width="37" height="4" rx="2" transform="translate(10.699 17)"/>
</g>
<g id="step-9" data-name="step" transform="translate(524.146 463)">
<path id="step-10" data-name="step" class="cls-6" d="M4,0H133a4,4,0,0,1,4,4V33a0,0,0,0,1,0,0H0a0,0,0,0,1,0,0V4A4,4,0,0,1,4,0Z" transform="translate(2.699 3)"/>
<path id="step-status-5" data-name="step-status" class="cls-7" d="M0,0H137a0,0,0,0,1,0,0V13a4,4,0,0,1-4,4H4a4,4,0,0,1-4-4V0A0,0,0,0,1,0,0Z" transform="translate(2.699 36)"/>
<rect id="time-9" data-name="time" class="cls-8" width="50" height="4" rx="2" transform="translate(81.699 43)"/>
<rect id="time-10" data-name="time" class="cls-8" width="26" height="4" rx="2" transform="translate(10.699 43)"/>
<rect id="title-5" data-name="title" class="cls-7" width="37" height="4" rx="2" transform="translate(10.699 17)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -1,53 +1,20 @@
import {Action, createAction, props} from '@ngrx/store';
import {createAction, props} from '@ngrx/store';
import {SearchState} from '@common/common-search/common-search.reducer';
const COMMON_SEARCH_PREFIX = 'CommonSearch_';
export const COMMON_SEARCH_ACTIONS = {
SET_IS_SEARCHING : COMMON_SEARCH_PREFIX + 'SET_IS_SEARCHING',
RESET_SEARCH : COMMON_SEARCH_PREFIX + 'RESET_SEARCH',
INIT_SEARCH : COMMON_SEARCH_PREFIX + 'INIT_SEARCH',
SET_SEARCH_QUERY : COMMON_SEARCH_PREFIX + 'SET_SEARCH_QUERY',
SET_SEARCH_PLACEHOLDER: COMMON_SEARCH_PREFIX + 'SET_SEARCH_PLACEHOLDER',
SET_SEARCH_ACTIVE : COMMON_SEARCH_PREFIX + 'SET_SEARCH_ACTIVE',
};
export class SetIsSearching implements Action {
readonly type = COMMON_SEARCH_ACTIONS.SET_IS_SEARCHING;
constructor(public payload: boolean) {
}
}
export class SetSearchActive implements Action {
readonly type = COMMON_SEARCH_ACTIONS.SET_SEARCH_ACTIVE;
constructor(public payload: boolean) {
}
}
export const setSearchQuery = createAction(
COMMON_SEARCH_ACTIONS.SET_SEARCH_QUERY,
props<{query: string; regExp?: boolean}>()
export const setSearching = createAction(
COMMON_SEARCH_PREFIX + 'SET_IS_SEARCHING',
props<{payload: boolean}>()
);
export class SetSearchPlaceholder implements Action {
readonly type = COMMON_SEARCH_ACTIONS.SET_SEARCH_PLACEHOLDER;
export const setSearchQuery = createAction(
COMMON_SEARCH_PREFIX + 'SET_SEARCH_QUERY',
props<SearchState['searchQuery']>()
);
constructor(public payload: string) {
}
}
export class ResetSearch implements Action {
readonly type = COMMON_SEARCH_ACTIONS.RESET_SEARCH;
constructor() {
}
}
export class InitSearch implements Action {
readonly type = COMMON_SEARCH_ACTIONS.INIT_SEARCH;
constructor(public payload: string) {
}
}
export const resetSearch = createAction(COMMON_SEARCH_PREFIX + 'RESET_SEARCH');
export const initSearch = createAction(
COMMON_SEARCH_PREFIX + 'INIT_SEARCH',
props<{payload: string}>()
);

View File

@@ -3,13 +3,13 @@ import {CommonModule} from '@angular/common';
import {SMSharedModule} from '../shared/shared.module';
import {CommonSearchComponent} from './containers/common-search/common-search.component';
import {StoreModule} from '@ngrx/store';
import {commonSearchReducer} from './common-search.reducer';
import {searchReducer} from './common-search.reducer';
@NgModule({
imports : [
CommonModule,
SMSharedModule,
StoreModule.forFeature('commonSearch', commonSearchReducer),
StoreModule.forFeature('commonSearch', searchReducer),
],
declarations: [CommonSearchComponent],
exports : [CommonSearchComponent]

View File

@@ -1,58 +1,33 @@
import {createFeatureSelector, createSelector} from '@ngrx/store';
import {COMMON_SEARCH_ACTIONS, setSearchQuery} from './common-search.actions';
import {createFeatureSelector, createReducer, createSelector, on} from '@ngrx/store';
import {initSearch, resetSearch, setSearching, setSearchQuery} from './common-search.actions';
export interface ICommonSearchState {
export interface SearchState {
isSearching: boolean;
searchQuery: {query: string; regExp?: boolean};
searchQuery: {query: string; regExp?: boolean; original?: string};
placeholder: string;
active: boolean;
}
// Todo remove selectedProjectId
const commonSearchInitState: ICommonSearchState = {
const searchInitState: SearchState = {
isSearching: false,
searchQuery: null,
placeholder: null,
active : false
};
export function commonSearchReducer<ActionReducer>(state: ICommonSearchState = commonSearchInitState, action): ICommonSearchState {
export const searchReducer = createReducer(
searchInitState,
on(setSearching, (state, action) => ({...state, isSearching: action.payload})),
on(setSearchQuery, (state, action) => ({...state, searchQuery: action})),
on(initSearch, (state, action) => ({...searchInitState, placeholder: action.payload || 'Search'})),
on(resetSearch, () => ({...searchInitState, placeholder: 'Search'})),
);
switch (action.type) {
case COMMON_SEARCH_ACTIONS.SET_IS_SEARCHING:
return {...state, isSearching: action.payload};
case setSearchQuery.type:
return {...state, searchQuery: action as ReturnType<typeof setSearchQuery>};
case COMMON_SEARCH_ACTIONS.SET_SEARCH_PLACEHOLDER:
return {...state, placeholder: action.payload};
case COMMON_SEARCH_ACTIONS.SET_SEARCH_ACTIVE:
return {...state, active: action.payload};
case COMMON_SEARCH_ACTIONS.INIT_SEARCH:
return {
...state,
isSearching: false,
searchQuery: commonSearchInitState.searchQuery,
placeholder: action.payload || 'Search',
active : false
};
case COMMON_SEARCH_ACTIONS.RESET_SEARCH:
return {
...state,
isSearching: false,
searchQuery: commonSearchInitState.searchQuery,
placeholder: 'Search',
active : false
};
default:
return state;
}
}
export const selectCommonSearch = createFeatureSelector<ICommonSearchState>('commonSearch');
export const selectIsSearching = createSelector(selectCommonSearch, (state: ICommonSearchState): boolean => state ? state.isSearching : false);
export const selectSearchQuery = createSelector(selectCommonSearch, (state: ICommonSearchState) => state?.searchQuery || commonSearchInitState.searchQuery);
export const selectPlaceholder = createSelector(selectCommonSearch, (state: ICommonSearchState): string => state ? state.placeholder : '');
export const selectActiveSearch = createSelector(selectCommonSearch, (state: ICommonSearchState): boolean => state ? state.active : false);
export const selectCommonSearch = createFeatureSelector<SearchState>('commonSearch');
export const selectIsSearching = createSelector(selectCommonSearch, (state: SearchState): boolean => state ? state.isSearching : false);
export const selectSearchQuery = createSelector(selectCommonSearch, (state: SearchState) => state?.searchQuery || searchInitState.searchQuery);
export const selectPlaceholder = createSelector(selectCommonSearch, (state: SearchState): string => state ? state.placeholder : '');

View File

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

View File

@@ -33,6 +33,12 @@
color: $blue-900;
background-color: $blue-280;
}
&.al-ico-error-circle {
width: 0;
right: 48px;
}
}
i.fa {
@@ -52,4 +58,10 @@
i.fa-times {
margin-right:4px;
}
::ng-deep sm-search.regex-error input {
padding-right: 56px !important;
border-color: $failed-red !important;
}
}

View File

@@ -1,37 +1,38 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
import {Store} from '@ngrx/store';
import {SetIsSearching, setSearchQuery} from '../../common-search.actions';
import {ICommonSearchState, selectIsSearching, selectPlaceholder, selectSearchQuery} from '../../common-search.reducer';
import {setSearching, setSearchQuery} from '../../common-search.actions';
import {SearchState, selectIsSearching, selectPlaceholder, selectSearchQuery} from '../../common-search.reducer';
import {Observable} from 'rxjs';
import {debounceTime, filter, tap} from 'rxjs/operators';
import {SearchComponent} from '../../../shared/ui-components/inputs/search/search.component';
import {SearchComponent} from '@common/shared/ui-components/inputs/search/search.component';
@Component({
selector : 'sm-common-search',
selector: 'sm-common-search',
templateUrl: './common-search.component.html',
styleUrls : ['./common-search.component.scss'],
styleUrls: ['./common-search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CommonSearchComponent implements OnInit {
public searchQuery$: Observable<ICommonSearchState['searchQuery']>;
public searchQuery$: Observable<SearchState['searchQuery']>;
public isSearching$: Observable<boolean>;
public searchPlaceholder$: Observable<string>;
public searchActive: boolean;
@ViewChild(SearchComponent) searchElem: SearchComponent;
public regExp: boolean = false;
private closeTimer: number;
private queryString: string;
minChars = 3;
public regexError: boolean;
constructor(private store: Store<any>, private router: Router, private route: ActivatedRoute, private cdr: ChangeDetectorRef) {}
constructor(private store: Store<any>, private router: Router, private route: ActivatedRoute, private cdr: ChangeDetectorRef) {
}
ngOnInit() {
this.searchQuery$ = this.store.select(selectSearchQuery).pipe(tap(searchQuery => this.regExp = searchQuery?.regExp));
this.isSearching$ = this.store.select(selectIsSearching);
this.searchQuery$ = this.store.select(selectSearchQuery).pipe(tap(searchQuery => this.regExp = searchQuery?.regExp));
this.isSearching$ = this.store.select(selectIsSearching);
this.searchPlaceholder$ = this.store.select(selectPlaceholder).pipe(debounceTime(0));
this.router.events.pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => {
@@ -42,24 +43,40 @@ export class CommonSearchComponent implements OnInit {
onSearchValueChanged(query: string) {
this.queryString = query;
this.search();
this.cdr.detectChanges();
this.store.dispatch(setSearchQuery({query: this.regExp? this.queryString: this.queryString.trim(), regExp: this.regExp}));
}
private search() {
try {
if (this.regExp) {
new RegExp(this.queryString);
}
this.regexError = null;
this.store.dispatch(setSearchQuery({
query: this.regExp ? this.queryString : this.queryString.trim(),
regExp: this.regExp,
original: this.queryString
}));
} catch (e) {
this.regexError = e.message?.replace(/:.+:/, ':');
}
}
openSearch() {
window.clearTimeout(this.closeTimer);
this.searchElem.searchBarInput.nativeElement.focus();
this.store.dispatch(new SetIsSearching(true));
this.store.dispatch(setSearching({payload: true}));
}
onSearchFocusOut() {
if (!this.searchElem.searchBarInput.nativeElement.value) {
this.closeTimer = window.setTimeout(() => this.store.dispatch(new SetIsSearching(false)), 200);
this.closeTimer = window.setTimeout(() => this.store.dispatch(setSearching({payload: false})), 200);
}
}
private setSearchActive() {
let route = this.route.snapshot;
let route = this.route.snapshot;
let showSearch = false;
while (route.firstChild) {
route = route.firstChild;
@@ -73,7 +90,7 @@ export class CommonSearchComponent implements OnInit {
clearSearch() {
this.searchElem.clear();
this.store.dispatch(new SetIsSearching(false));
this.store.dispatch(setSearching({payload: false}));
document.body.focus();
}
@@ -81,7 +98,7 @@ export class CommonSearchComponent implements OnInit {
this.regExp = !this.regExp;
window.clearTimeout(this.closeTimer);
if (this.queryString?.length >= this.minChars) {
this.store.dispatch(setSearchQuery({query: this.queryString, regExp: this.regExp}));
this.search();
}
}
}

View File

@@ -12,6 +12,7 @@
@import "shared/ui-components/styles/material-palette";
@import "assets/fonts/trains-icons.scss";
@import "layout/layout";
@import "shared/ui-components/styles/overrides/viewer-iterations-slider";
@include mat.core();
//@import "../webapp-common/shared/ui-components/styles/material-theme.scss";
@@ -161,8 +162,8 @@ h5.al-header {
}
span.highlight-text {
background: $neon-yellow-betterinchrome;
border: 1px solid darken($neon-yellow-betterinchrome, 5%);
background: $neon-yellow;
border: 1px solid darken($neon-yellow, 5%);
border-radius: 4px;
padding: 0 2px;
@@ -458,6 +459,10 @@ body .clean-list {
&.validation {
background-color: #ff001f;
}
&.break-line {
white-space: pre-line;
}
}
&.parameter-tooltip {
@@ -563,7 +568,7 @@ as-split {
$type-colors: (
string: #ff8400,
number: $neon-yellow-betterinchrome,
number: $neon-yellow,
boolean: #b938a4,
date: #05668D,
);
@@ -637,7 +642,7 @@ $type-colors: (
display: inline-flex;
}
.image-displayer-dialog {
.image-viewer-dialog {
.mat-dialog-container {
padding: 0;
border-radius: 0;
@@ -744,7 +749,7 @@ button.btn.button-outline-dark {
.sm-card-list-header {
display: flex;
justify-content: space-between;
padding: $projects-header-padding 6px $projects-header-padding * 0.5 0;
padding-top: $projects-header-padding;
.recent-title {
display: flex;

View File

@@ -2,6 +2,7 @@ import {MessageSeverityEnum, VIEW_PREFIX} from '~/app.constants';
import {createAction, props} from '@ngrx/store';
import {omit} from 'lodash/fp';
import {HttpErrorResponse} from '@angular/common/http';
import {Ace} from 'ace-builds';
export const setAutoRefresh = createAction(
VIEW_PREFIX + '[set auto refresh]',
@@ -51,6 +52,14 @@ export const visibilityChanged = createAction(
props<{visible: boolean}>()
);
export const saveAceCaretPosition = createAction(
VIEW_PREFIX + '[save ace caret position]',
props<{id: string; position: Ace.Point}>()
);
export const resetAceCaretsPositions = createAction(VIEW_PREFIX + '[reset ace carets positions]');
export const addMessage = createAction(
VIEW_PREFIX + '[add message]',
(severity: MessageSeverityEnum, msg: string, userActions?: {actions: any[]; name: string}[], suppressNextMessages?: boolean) =>

View File

@@ -61,6 +61,11 @@ export const setSelectedProject = createAction(
props<{ project: Project }>()
);
export const setSelectedProjectStats = createAction(
PROJECTS_PREFIX + '[set selected project statistics]',
props<{ project: Project }>()
);
export const resetSelectedProject = createAction(
PROJECTS_PREFIX + 'RESET_SELECTED_PROJECT'
);

View File

@@ -27,7 +27,10 @@ export const setSelectedWorkspaceTab = createAction(
);
export const setFilterByUser = createAction(USERS_PREFIX +'SET_FILTERED_BY_USER', props<{showOnlyUserWork: boolean}>());
export const setFilterByUser = createAction(
USERS_PREFIX +'SET_FILTERED_BY_USER',
props<{showOnlyUserWork: boolean}>()
);
export const setUserWorkspacesFromUser = createAction(USERS_PREFIX + ' set user workspaces from current user');

View File

@@ -65,7 +65,7 @@ export class LayoutEffects {
}
let resultMessage: string;
const subcode = get('error.meta.result_subcode', action.serverError);
if (subcode) {
if (subcode || subcode === 0) {
resultMessage = `Error ${subcode} : ${get('error.meta.result_msg', action.serverError)}`;
}
this.alertDialogRef = this.dialog.open(AlertDialogComponent, {

View File

@@ -1,9 +1,9 @@
import {Injectable} from '@angular/core';
import {Action, Store} from '@ngrx/store';
import {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 {catchError, filter, map, mergeMap, switchMap, withLatestFrom} from 'rxjs/operators';
import {catchError, expand, filter, map, mergeMap, reduce, switchMap, withLatestFrom} from 'rxjs/operators';
import {requestFailed} from '../actions/http.actions';
import {activeLoader, deactivateLoader, setServerError} from '../actions/layout.actions';
import {setSelectedModels} from '../../models/actions/models-view.actions';
@@ -12,12 +12,12 @@ 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, of} from 'rxjs';
import {EMPTY, forkJoin, of} from 'rxjs';
import {ProjectsGetTaskTagsResponse} from '~/business-logic/model/projects/projectsGetTaskTagsResponse';
import {ProjectsGetModelTagsResponse} from '~/business-logic/model/projects/projectsGetModelTagsResponse';
import {
selectAllProjectsUsers,
selectLastUpdate,
selectLastUpdate, selectRootProjects,
selectSelectedMetricVariantForCurrProject,
selectSelectedProjectId
} from '../reducers/projects.reducer';
@@ -27,20 +27,20 @@ import {createMetricColumn} from '@common/shared/utils/tableParamEncode';
import {ITask} from '~/business-logic/model/al-task';
import {TasksGetAllExRequest} from '~/business-logic/model/tasks/tasksGetAllExRequest';
import {setSelectedExperiments} from '../../experiments/actions/common-experiments-view.actions';
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';
import {get, last} from 'lodash/fp';
import {selectProjects, selectShowHidden} from '@common/projects/common-projects.reducer';
import {setShowHidden} from '@common/projects/common-projects.actions';
import {ProjectsGetAllExRequest} from '~/business-logic/model/projects/projectsGetAllExRequest';
export const ALL_PROJECTS_OBJECT = {id: '*', name: 'All Experiments'};
@Injectable()
export class ProjectsEffects {
private pageSize: number = 500;
private lastUpdateSoFar: string;
private scrollId: string = null;
constructor(
private actions$: Actions, private projectsApi: ApiProjectsService, private orgApi: ApiOrganizationService,
@@ -56,31 +56,42 @@ export class ProjectsEffects {
getProjects$ = createEffect(() => this.actions$.pipe(
ofType(actions.getAllSystemProjects),
withLatestFrom(this.store.select(selectShowHidden), this.store.select(selectLastUpdate)),
switchMap(([, showHidden, lastUpdate]) => this.projectsApi.projectsGetAllEx({
withLatestFrom(
this.store.select(selectShowHidden),
this.store.select(selectLastUpdate),
),
switchMap(([, showHidden, lastUpdate]) => {
const query = {
/* eslint-disable @typescript-eslint/naming-convention */
scroll_id: null,
size: this.pageSize,
scroll_id: this.scrollId,
order_by: ['last_update'],
last_update: lastUpdate ? [lastUpdate, null] : undefined,
only_fields: ['name', 'company', 'parent', 'last_update'], search_hidden: showHidden
...(lastUpdate && {last_update: [lastUpdate, null]}),
only_fields: ['name', 'company', 'parent', 'last_update'],
search_hidden: showHidden
/* eslint-enable @typescript-eslint/naming-convention */
} as any)
.pipe(mergeMap((res: ProjectsGetAllExResponse) => {
const resultsActions: Action[] = [actions.setAllProjects({projects: res.projects as unknown as Project[], updating: !!lastUpdate})];
if (res.projects.length >= this.pageSize) {
this.scrollId = res.scroll_id;
this.lastUpdateSoFar = res.projects[res.projects.length - 1].last_update;
resultsActions.push(actions.getAllSystemProjects());
} else {
resultsActions.push(actions.setLastUpdate({lastUpdate: res.projects[res.projects.length - 1]?.last_update || this.lastUpdateSoFar || lastUpdate}));
this.scrollId = null;
this.lastUpdateSoFar = null;
}
return resultsActions;
})
)
)
} as ProjectsGetAllExRequest;
return this.projectsApi.projectsGetAllEx(query)
.pipe(
expand((res: ProjectsGetAllExResponse) => res.scroll_id && res.projects.length >= this.pageSize ?
this.projectsApi.projectsGetAllEx({
...query,
// eslint-disable-next-line @typescript-eslint/naming-convention
scroll_id: res.scroll_id,
}) :
EMPTY
),
reduce((acc, res: ProjectsGetAllExResponse) => acc.concat(res.projects), []),
);
}),
withLatestFrom(this.store.select(selectRootProjects)),
mergeMap(([projects, rootProjects]) => [
actions.setAllProjects({
projects: projects as Project[],
updating: rootProjects.length > 0
}),
actions.setLastUpdate({lastUpdate: last(projects)?.last_update})
])
));
resetProjects$ = createEffect(() => this.actions$.pipe(
@@ -204,7 +215,7 @@ export class ProjectsEffects {
const end = started + (task.active_duration ?? 0) * 1000;
return {
id: task.id,
y: get(col.id, task),
y: get(col.id, task), // col.id is a path (e.g.) last_metric.x.max_value, must use lodash get
x: end,
name: task.name,
status: task.status,
@@ -218,7 +229,7 @@ export class ProjectsEffects {
));
resetRootProjects = createEffect(() => this.actions$.pipe(
ofType(setActiveWorkspace, actions.refetchProjects),
ofType(setActiveWorkspace, actions.refetchProjects, setShowHidden),
mergeMap(() => [
actions.resetProjects(),
actions.getAllSystemProjects()
@@ -228,8 +239,10 @@ export class ProjectsEffects {
getAllProjectsUsersEffect = createEffect(() => this.actions$.pipe(
ofType(actions.getAllSystemProjects),
switchMap(() => this.usersApi.usersGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */
order_by: ['name'],
only_fields: ['name'],
/* eslint-enable @typescript-eslint/naming-convention */
}, null, 'body', true).pipe(
mergeMap(res => [actions.setAllProjectUsers(res)]),
catchError(error => [
@@ -247,10 +260,12 @@ export class ProjectsEffects {
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(
/* eslint-disable @typescript-eslint/naming-convention */
order_by: ['name'],
only_fields: ['name'],
active_in_projects: [action.projectId]
/* eslint-enable @typescript-eslint/naming-convention */
}, null, 'body', true)).pipe(
mergeMap(res => [actions.setProjectUsers(res)]),
catchError(error => [
requestFailed(error),
@@ -262,9 +277,11 @@ export class ProjectsEffects {
getExtraUsersEffect = createEffect(() => this.actions$.pipe(
ofType(actions.getFilteredUsers),
switchMap(action => this.usersApi.usersGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */
order_by: ['name'],
only_fields: ['name'],
id: action.filteredUsers || []
/* eslint-enable @typescript-eslint/naming-convention */
}, null, 'body', true).pipe(
mergeMap(res => [
actions.setProjectExtraUsers(res),

View File

@@ -11,7 +11,7 @@ import {
} 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, takeWhile, timeoutWith} from 'rxjs/operators';
import {filter, map, takeWhile, timeout} from 'rxjs/operators';
export interface Credentials {
Bucket?: string;
@@ -31,11 +31,11 @@ export interface AuthState {
showLocalFilePopup: boolean;
localFilesPopupURLs: Array<string>;
revokeSucceed: boolean;
credentials: {[workSpaceId: string]: CredentialKeyExt[]};
credentials: { [workSpaceId: string]: CredentialKeyExt[] };
newCredential: CredentialKeyExt;
dontShowAgainForBucketEndpoint: string;
s3BucketCredentials: { bucketCredentials: Credentials[] };
signedUrls: {[url: string]: {signed: string; expires: number}};
signedUrls: { [url: string]: { signed: string; expires: number } };
}
export const initAuth: AuthState = {
@@ -54,12 +54,12 @@ export const initAuth: AuthState = {
export const selectAuth = state => state.auth as AuthState;
// Auth selectors
export const selectRevokeSucceed = createSelector(selectAuth, state => state.revokeSucceed);
export const selectCredentials = createSelector(selectAuth, state => state.credentials);
export const selectNewCredential = createSelector(selectAuth, state => state.newCredential);
export const selectS3BucketCredentials = createSelector(selectAuth, state => state.s3BucketCredentials);
export const selectRevokeSucceed = createSelector(selectAuth, state => state.revokeSucceed);
export const selectCredentials = createSelector(selectAuth, state => state.credentials);
export const selectNewCredential = createSelector(selectAuth, state => state.newCredential);
export const selectS3BucketCredentials = createSelector(selectAuth, state => state.s3BucketCredentials);
export const selectS3BucketCredentialsBucketCredentials = createSelector(selectAuth, state => state.s3BucketCredentials?.bucketCredentials);
export const selectShowLocalFilesPopUp = createSelector(selectAuth, state => state.showLocalFilePopup);
export const selectShowLocalFilesPopUp = createSelector(selectAuth, state => state.showLocalFilePopup);
export const selectDontShowAgainForBucketEndpoint = createSelector(selectAuth, state => state.dontShowAgainForBucketEndpoint);
export const selectSignedUrls = createSelector(selectAuth, state => state.signedUrls);
export const selectSignedUrl = url => createSelector(selectAuth, state => state.signedUrls[url]);
@@ -67,11 +67,14 @@ export const getSignedUrlOrOrigin$ = (url: string, store: Store) => store.pipe(
select(selectSignedUrl(url)),
filter(signed => !!signed?.signed),
map(signed => signed?.signed),
timeoutWith(900, store.select(selectSignedUrl(url))
.pipe(
takeWhile( signed => signed !== null),
map(signed => signed?.signed || url)
)
timeout({
first: 900,
with: () => store.select(selectSignedUrl(url))
.pipe(
takeWhile(signed => signed !== null),
map(signed => signed?.signed || url)
)
}
),
);
@@ -114,7 +117,9 @@ export const commonAuthReducer = [
[action.workspaceId]: [
...(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},
@@ -122,14 +127,18 @@ export const commonAuthReducer = [
...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: {
}
})),
on(removeCredential, (state, action) => ({
...state, credentials: {
...state.credentials,
[action.workspaceId]: state.credentials[action.workspaceId].filter((cred => cred.access_key !== action.accessKey))
}})),
}
})),
on(updateAllCredentials, (state, action) => ({
...state,
credentials: {[action.credentials[0]?.company || action.workspace]: action.credentials, ...action.extra}, revokeSucceed: false})),
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

@@ -114,9 +114,15 @@ export const projectsReducer = createReducer(
};
}),
on(projectsActions.setSelectedProject, (state, action) => ({...state, selectedProject: action.project, extraUsers: []})),
on(projectsActions.setSelectedProjectStats, (state, action) => ({
...state,
selectedProject: {
...state.selectedProject,
stats: action.project?.stats
}})),
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))};
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, users: [], extraUsers: []})),
on(projectsActions.updateProjectCompleted, (state, action) => ({

View File

@@ -18,6 +18,7 @@ export interface UsersState {
workspaces: GetCurrentUserResponseUserObjectCompany[];
showOnlyUserWork: boolean;
serverVersions: { server: string; api: string };
gettingStarted: any;
}
export const initUsers: UsersState = {
@@ -28,6 +29,7 @@ export const initUsers: UsersState = {
workspaces: [],
showOnlyUserWork: false,
serverVersions: null,
gettingStarted: null
};
export const users = state => state.users as UsersState;
@@ -39,6 +41,7 @@ export const selectSelectedWorkspaceTab = createSelector(users, state => state.s
export const selectWorkspaces = createSelector(users, state => state.workspaces);
export const selectShowOnlyUserWork = createSelector(users, state => state.showOnlyUserWork);
export const selectServerVersions = createSelector(users, state => state.serverVersions);
export const selectGettingStarted = createSelector(users, state => state.gettingStarted);
export const usersReducerFunctions = [
on(fetchCurrentUser, state => ({...state})),

View File

@@ -1,6 +1,7 @@
import {createReducer, createSelector, on, ReducerTypes} from '@ngrx/store';
import * as layoutActions from '../actions/layout.actions';
import {apiRequest, requestFailed} from '@common/core/actions/http.actions';
import {Ace} from 'ace-builds';
export interface ViewState {
loading: { [endpoint: string]: boolean };
@@ -17,6 +18,7 @@ export interface ViewState {
neverShowPopupAgain: string[];
plotlyReady: boolean;
aceReady: boolean;
aceCaretPosition: { [key: string]: Ace.Point };
preferencesReady: boolean;
showUserFocus: boolean;
}
@@ -24,7 +26,6 @@ export interface ViewState {
export const initViewState: ViewState = {
loading: {},
dialog: false,
notification: null,
loggedOut: false,
backdropActive: false,
@@ -37,6 +38,7 @@ export const initViewState: ViewState = {
neverShowPopupAgain: [],
plotlyReady: false,
aceReady: false,
aceCaretPosition: {},
preferencesReady: false,
showUserFocus: false,
};
@@ -57,6 +59,7 @@ export const selectFirstLogin = createSelector(views, state => state.firstLogin)
export const selectFirstLoginAt = createSelector(views, state => state.firstLoginAt);
export const selectPlotlyReady = createSelector(views, state => state.plotlyReady);
export const selectAceReady = createSelector(views, state => state.aceReady);
export const selectAceCaretPosition = createSelector(views, state => state.aceCaretPosition);
export const selectNeverShowPopups = createSelector(views, (state): string[] => state.neverShowPopupAgain);
export const selectShowUserFocus = createSelector(views, state => state.showUserFocus);
@@ -77,9 +80,18 @@ export const viewReducers = [
})),
on(layoutActions.visibilityChanged, (state, action) => ({...state, applicationVisible: action.visible})),
on(layoutActions.setScaleFactor, (state, action) => ({...state, scaleFactor: action.scale})),
on(layoutActions.firstLogin, (state, action) => ({...state, firstLogin: action.first, firstLoginAt: new Date().getTime()})),
on(layoutActions.firstLogin, (state, action) => ({
...state,
firstLogin: action.first,
firstLoginAt: new Date().getTime()
})),
on(layoutActions.plotlyReady, (state) => ({...state, plotlyReady: true})),
on(layoutActions.aceReady, (state) => ({...state, aceReady: true})),
on(layoutActions.saveAceCaretPosition, (state, action) => ({
...state,
aceCaretPosition: {...state.aceCaretPosition, [action.id]: action.position}
})),
on(layoutActions.resetAceCaretsPositions, (state, action) => ({...state, aceCaretPosition: {}})),
on(layoutActions.resetLoader, (state) => ({...state, loading: {}})),
on(apiRequest, (state, action) => ({
...state,

View File

@@ -24,7 +24,7 @@ export class RefreshService {
),
filter(([, auto, visible]) => auto && visible)
)
.subscribe(() => this._tick.next(null))
.subscribe(() => this._tick.next(null));
}
trigger(auto = false) {

View File

@@ -1,86 +1,85 @@
import {Action, createAction, props} from '@ngrx/store';
import {ISmAction} from '../core/models/actions';
import {SEARCH_ACTIONS} from './dashboard-search.consts';
import {createAction, props} from '@ngrx/store';
import {SEARCH_PREFIX} 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 {ActiveSearchLink} from '~/features/dashboard-search/dashboard-search.consts';
import {DASHBOARD_PREFIX} from '@common/dashboard/common-dashboard.const';
export const searchSetTerm = createAction(
SEARCH_ACTIONS.SET_TERM,
props<{query: string; regExp?: boolean; force?: boolean}>()
SEARCH_PREFIX + 'SET_TERM',
props<{ query: string; regExp?: boolean; force?: boolean }>()
);
export const searchStart = createAction(
SEARCH_ACTIONS.SEARCH_START,
props<{query: string; regExp?: boolean; force?: boolean}>()
SEARCH_PREFIX + 'SEARCH_START',
props<{ query: string; regExp?: boolean; force?: boolean; activeLink: ActiveSearchLink }>()
);
export class SearchError implements ISmAction {
readonly type = SEARCH_ACTIONS.SEARCH_ERROR;
}
export class SearchClear implements Action {
readonly type = SEARCH_ACTIONS.SEARCH_CLEAR;
}
export class SearchActivate implements Action {
readonly type = SEARCH_ACTIONS.ACTIVATE;
}
export class SearchDeactivate implements Action {
readonly type = SEARCH_ACTIONS.DEACTIVATE;
}
export const searchClear = createAction(SEARCH_PREFIX + 'SEARCH_CLEAR');
export const searchActivate = createAction(SEARCH_PREFIX + 'ACTIVATE');
export const searchDeactivate = createAction(SEARCH_PREFIX + 'DEACTIVATE');
export const searchProjects = createAction(
SEARCH_ACTIONS.SEARCH_PROJECTS,
props<{query: string; regExp?: boolean}>()
SEARCH_PREFIX + 'SEARCH_PROJECTS',
props<{ query: string; regExp?: boolean }>()
);
export const searchPipelines = createAction(
SEARCH_ACTIONS.SEARCH_PIPELINES,
props<{query: string; regExp?: boolean}>()
SEARCH_PREFIX + 'SEARCH_PIPELINES',
props<{ query: string; regExp?: boolean }>()
);
export const searchOpenDatasets = createAction(
SEARCH_PREFIX + 'SEARCH_OPEN_"DATASETS',
props<{ query: string; regExp?: boolean }>()
);
export const setPipelinesResults = createAction(
'Set Pipelines Results',
props<{pipelines: Project[]}>()
props<{ pipelines: Project[]; scrollId: string }>()
);
export class SetProjectsResults implements ISmAction {
public type = SEARCH_ACTIONS.SET_PROJECTS;
public payload: { projects: Array<Project> };
constructor(projects: Array<Project>) {
this.payload = {projects};
}
}
export const searchExperiments= createAction(
SEARCH_ACTIONS.SEARCH_EXPERIMENTS,
props<{query: string; regExp?: boolean}>()
export const setOpenDatasetsResults = createAction(
'Set open datasets Results',
props<{ openDatasets: Project[]; scrollId: string }>()
);
export class SetExperimentsResults implements ISmAction {
public type = SEARCH_ACTIONS.SET_EXPERIMENTS;
public payload: { experiments: Array<Task> };
export const setProjectsResults = createAction(SEARCH_PREFIX + 'SET_PROJECTS',
props<{ projects: Project[]; scrollId: string }>()
);
constructor(experiments: Array<Task>) {
this.payload = {experiments};
}
}
export const searchExperiments = createAction(
SEARCH_PREFIX + 'SEARCH_EXPERIMENTS',
props<{ query: string; regExp?: boolean }>()
);
export const setExperimentsResults = createAction(
SEARCH_PREFIX + 'SET_EXPERIMENTS',
props<{ experiments: Task[]; scrollId: string }>()
);
export const searchModels = createAction(
SEARCH_ACTIONS.SEARCH_MODELS,
props<{query: string; regExp?: boolean}>()
SEARCH_PREFIX + 'SEARCH_MODELS',
props<{ query: string; regExp?: boolean }>()
);
export class SetModelsResults implements ISmAction {
public type = SEARCH_ACTIONS.SET_MODELS;
public payload: { models: Array<Model> };
export const setModelsResults = createAction(
SEARCH_PREFIX + 'SET_MODELS',
props<{ models: Model[]; scrollId: string }>()
);
constructor(models: Array<Model>) {
this.payload = {models};
}
}
export const setResultsCount = createAction(
SEARCH_PREFIX + 'SET_COUNTS',
props<{ counts: Map<ActiveSearchLink, number> }>()
);
export const getCurrentPageResults = createAction(
DASHBOARD_PREFIX + '[get current page results]',
props<{ activeLink: ActiveSearchLink }>()
);
export const getResultsCount = createAction(
DASHBOARD_PREFIX + '[get results count]',
props<{ query: string; regExp?: boolean; force?: boolean }>());
export const clearSearchResults = createAction(DASHBOARD_PREFIX + '[clear search results]');

View File

@@ -1,22 +1,5 @@
const SEARCH_PREFIX = 'SEARCH_';
export const SEARCH_PAGE_SIZE = 1000;
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',
SEARCH_ERROR : SEARCH_PREFIX + 'SEARCH_ERROR',
SEARCH_CLEAR : SEARCH_PREFIX + 'SEARCH_CLEAR',
SET_PROJECTS : SEARCH_PREFIX + 'SET_PROJECTS',
SET_MODELS : SEARCH_PREFIX + 'SET_MODELS',
SET_EXPERIMENTS : SEARCH_PREFIX + 'SET_EXPERIMENTS',
ACTIVATE : SEARCH_PREFIX + 'ACTIVATE',
DEACTIVATE : SEARCH_PREFIX + 'DEACTIVATE'
};
export const SEARCH_PREFIX = 'SEARCH_';
export const SEARCH_PAGE_SIZE = 12;
export const EXPERIMENT_SEARCH_ONLY_FIELDS = ['name', 'created', 'status', 'type', 'user.name', 'id', 'company'];

View File

@@ -2,28 +2,73 @@ import {Injectable} from '@angular/core';
import {Actions, createEffect, ofType} from '@ngrx/effects';
import {activeLoader, deactivateLoader} from '../core/actions/layout.actions';
import {
SearchActivate,
SearchClear,
getCurrentPageResults,
getResultsCount,
searchActivate,
searchClear,
searchExperiments,
searchModels,
searchOpenDatasets,
searchPipelines,
searchProjects,
searchSetTerm,
searchStart,
SetExperimentsResults,
SetModelsResults, setPipelinesResults,
SetProjectsResults
setExperimentsResults,
setModelsResults, setOpenDatasetsResults,
setPipelinesResults,
setProjectsResults
} from './dashboard-search.actions';
import {EXPERIMENT_SEARCH_ONLY_FIELDS, SEARCH_ACTIONS, SEARCH_PAGE_SIZE} from './dashboard-search.consts';
import {EXPERIMENT_SEARCH_ONLY_FIELDS, SEARCH_PAGE_SIZE} from './dashboard-search.consts';
import {ApiProjectsService} from '~/business-logic/api-services/projects.service';
import {requestFailed} from '../core/actions/http.actions';
import {Store} from '@ngrx/store';
import {selectActiveSearch} from './dashboard-search.reducer';
import {selectActiveSearch, selectSearchScrollIds, selectSearchTerm} from './dashboard-search.reducer';
import {ProjectsGetAllExRequest} from '~/business-logic/model/projects/projectsGetAllExRequest';
import {ApiTasksService} from '~/business-logic/api-services/tasks.service';
import {ApiModelsService} from '~/business-logic/api-services/models.service';
import {catchError, mergeMap, map, switchMap, withLatestFrom} from 'rxjs/operators';
import {escapeRegex} from '../shared/utils/shared-utils';
import {isEqual} from 'lodash/fp';
import {activeSearchLink} from '~/features/dashboard-search/dashboard-search.consts';
import {EmptyAction} from '~/app.constants';
export const getEntityStatQuery = action => ({
/* eslint-disable @typescript-eslint/naming-convention */
projects: {
_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
fields: ['basename', 'id']
},
system_tags: ['-pipeline', '-dataset'],
},
tasks: {_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
fields: ['name', 'id']
},
type: ['__$not', 'annotation_manual', '__$not', 'annotation', '__$not', 'dataset_import'],
system_tags: ['-archived', '-pipeline', '-dataset']
},
models: {_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
fields: ['name', 'id']
}},
datasets: {_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
fields: ['basename', 'id']
},
search_hidden: true,
system_tags: ['dataset'],
name: '/\\.datasets/'
},
pipelines: {_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
fields: ['basename', 'id']
},
search_hidden: true,
system_tags: ['pipeline']
}
/* eslint-enable @typescript-eslint/naming-convention */
});
@Injectable()
export class DashboardSearchEffects {
@@ -36,98 +81,158 @@ 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, SEARCH_ACTIONS.SEARCH_PIPELINES),
ofType(searchProjects, searchModels, searchExperiments, searchPipelines),
map(action => activeLoader(action.type))
));
// add actions for each search
startSearch = createEffect(() => this.actions.pipe(
ofType(searchStart.type),
withLatestFrom(this.store.select(selectActiveSearch)),
mergeMap(([action, active]: [ReturnType<typeof searchStart>, boolean]) => {
ofType(searchStart),
withLatestFrom(
this.store.select(selectActiveSearch),
this.store.select(selectSearchTerm)),
mergeMap(([action, active, term]) => {
const actionsToFire = [];
if (!active) {
actionsToFire.push(new SearchClear());
actionsToFire.push(new SearchActivate());
actionsToFire.push(searchClear());
actionsToFire.push(searchActivate());
}
if (!isEqual(term, action)) {
actionsToFire.push(getResultsCount(action));
actionsToFire.push(searchSetTerm(action));
}
actionsToFire.push(searchSetTerm(action));
actionsToFire.push(searchProjects(action));
actionsToFire.push(searchPipelines(action));
actionsToFire.push(searchExperiments(action));
actionsToFire.push(searchModels(action));
return actionsToFire;
})
));
getCurrentPageResults = createEffect(() => this.actions.pipe(
ofType(getCurrentPageResults),
withLatestFrom(
this.store.select(selectSearchTerm)),
map(([action, term]) => {
switch (action.activeLink) {
case activeSearchLink.experiments:
return searchExperiments(term);
case activeSearchLink.models:
return searchModels(term);
case activeSearchLink.projects:
return searchProjects(term);
case activeSearchLink.pipelines:
return searchPipelines(term);
case activeSearchLink.openDatasets:
return searchOpenDatasets(term);
}
return new EmptyAction();
}
)
));
searchProjects = createEffect(() => this.actions.pipe(
ofType(searchProjects.type),
switchMap((action: ReturnType<typeof searchProjects>) => this.projectsApi.projectsGetAllEx({
ofType(searchProjects),
withLatestFrom(this.store.select(selectSearchScrollIds)),
switchMap(([action, scrollIds]) => this.projectsApi.projectsGetAllEx({
_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query) + '[^/]*$'}),
fields: ['name', 'id']
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
fields: ['basename', 'id']
},
include_stats_filter: {system_tags: ['-pipeline']},
/* eslint-disable @typescript-eslint/naming-convention */
system_tags: ['-pipeline', '-dataset'],
stats_for_state: ProjectsGetAllExRequest.StatsForStateEnum.Active,
scroll_id: null,
scroll_id: scrollIds?.[activeSearchLink.projects] || null,
size: SEARCH_PAGE_SIZE,
include_stats: true,
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination']
} as ProjectsGetAllExRequest).pipe(
mergeMap(res => [new SetProjectsResults(res.projects), deactivateLoader(action.type)]),
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination', 'basename']
/* eslint-enable @typescript-eslint/naming-convention */
}).pipe(
mergeMap(res => [setProjectsResults({projects: res.projects, scrollId: res.scroll_id}), 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({
ofType(searchPipelines),
withLatestFrom(this.store.select(selectSearchScrollIds)),
switchMap(([action, scrollIds]) => this.projectsApi.projectsGetAllEx({
_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query) + '[^/]*$'}),
fields: ['name', 'id']
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
fields: ['basename', 'id']
},
/* eslint-disable @typescript-eslint/naming-convention */
search_hidden: true,
shallow_search: false,
system_tags: ['pipeline'],
stats_for_state: ProjectsGetAllExRequest.StatsForStateEnum.Active,
scroll_id: null,
scroll_id: scrollIds?.[activeSearchLink.pipelines] || 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)]),
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination', 'tags', 'system_tags', 'basename']
/* eslint-enable @typescript-eslint/naming-convention */
}).pipe(
mergeMap(res => [setPipelinesResults({pipelines: res.projects, scrollId: res.scroll_id}), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])))
));
searchOpenDatasets = createEffect(() => this.actions.pipe(
ofType(searchOpenDatasets),
withLatestFrom(this.store.select(selectSearchScrollIds)),
switchMap(([action, scrollIds]) => this.projectsApi.projectsGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */
_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
fields: ['basename', 'id']
},
search_hidden: true,
shallow_search: false,
system_tags: ['dataset'],
name: '/\\.datasets/',
stats_for_state: ProjectsGetAllExRequest.StatsForStateEnum.Active,
scroll_id: scrollIds?.[activeSearchLink.openDatasets] || null,
size: SEARCH_PAGE_SIZE,
include_stats: true,
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination', 'tags', 'system_tags', 'basename']
/* eslint-enable @typescript-eslint/naming-convention */
}).pipe(
mergeMap(res => [setOpenDatasetsResults({openDatasets: res.projects, scrollId: res.scroll_id}), 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({
ofType(searchModels),
withLatestFrom(this.store.select(selectSearchScrollIds)),
switchMap(([action, scrollIds]) => this.modelsApi.modelsGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */
_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
fields: ['name', 'id']
},
scroll_id: null,
scroll_id: scrollIds?.[activeSearchLink.models] || null,
size: SEARCH_PAGE_SIZE,
system_tags: ['-archived'],
only_fields: ['labels', 'ready', 'created', 'framework', 'user.name', 'name', 'parent.name', 'task.name', 'id', 'company']
include_stats: true,
only_fields: ['ready', 'created', 'framework', 'user.name', 'name', 'parent.name', 'task.name', 'id', 'company']
/* eslint-enable @typescript-eslint/naming-convention */
}).pipe(
mergeMap(res => [new SetModelsResults(res.models), deactivateLoader(action.type)]),
mergeMap(res => [setModelsResults({models: res.models, scrollId: res.scroll_id}), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])))
));
searchExperiments = createEffect(() => this.actions.pipe(
ofType(searchExperiments.type),
switchMap((action: ReturnType<typeof searchExperiments>) => this.experimentsApi.tasksGetAllEx({
ofType(searchExperiments),
withLatestFrom(this.store.select(selectSearchScrollIds)),
switchMap(([action, scrollIds]) => this.experimentsApi.tasksGetAllEx({
/* eslint-disable @typescript-eslint/naming-convention */
_any_: {
...(action.query && {pattern: action.regExp ? action.query : escapeRegex(action.query)}),
fields: ['name', 'id']
},
scroll_id: null,
scroll_id: scrollIds?.[activeSearchLink.experiments] || null,
size: SEARCH_PAGE_SIZE,
only_fields: EXPERIMENT_SEARCH_ONLY_FIELDS,
type: ['__$not', 'annotation_manual', '__$not', 'annotation', '__$not', 'dataset_import'],
system_tags: ['-archived', '-pipeline']
system_tags: ['-archived', '-pipeline', '-dataset']
/* eslint-enable @typescript-eslint/naming-convention */
}).pipe(
mergeMap(res => [new SetExperimentsResults(res.tasks), deactivateLoader(action.type)]),
mergeMap(res => [setExperimentsResults({experiments: res.tasks, scrollId: res.scroll_id}), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])))
));
}

View File

@@ -1,74 +1,96 @@
import {Project} from '../../business-logic/model/projects/project';
import {User} from '../../business-logic/model/users/user';
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, setPipelinesResults} from './dashboard-search.actions';
import {ICommonSearchState} from '../common-search/common-search.reducer';
import {createFeatureSelector, createSelector, ReducerTypes, on, createReducer} from '@ngrx/store';
import {Project} from '~/business-logic/model/projects/project';
import {User} from '~/business-logic/model/users/user';
import {Task} from '~/business-logic/model/tasks/task';
import {Model} from '~/business-logic/model/models/model';
import {
clearSearchResults,
searchActivate,
searchClear,
searchDeactivate,
searchSetTerm, setExperimentsResults, setModelsResults, setOpenDatasetsResults,
setPipelinesResults,
setProjectsResults, setResultsCount
} from './dashboard-search.actions';
import {SearchState} from '../common-search/common-search.reducer';
import {ActiveSearchLink, activeSearchLink} from '~/features/dashboard-search/dashboard-search.consts';
export interface ISearchState {
export interface DashboardSearchState {
projects: Project[];
experiments: Task[];
models: Model[];
pipelines: Project[];
openDatasets: Project[];
users: User[];
resultsCounter: number;
term: ICommonSearchState['searchQuery'];
term: SearchState['searchQuery'];
forceSearch: boolean;
active: boolean;
resultsCount: Map<ActiveSearchLink, number>;
scrollIds: Map<ActiveSearchLink, string>;
}
export const searchInitialState: ISearchState = {
export const searchInitialState: DashboardSearchState = {
term: null,
forceSearch: false,
projects: [],
pipelines: [],
openDatasets: [],
users: [],
experiments: [],
models: [],
resultsCounter: 0,
resultsCount: null,
scrollIds: null,
active: false
};
export function dashboardSearchReducer<ActionReducer>(state: ISearchState = searchInitialState, action) {
switch (action.type) {
case SEARCH_ACTIONS.ACTIVATE:
return {...state, active: true};
case SEARCH_ACTIONS.DEACTIVATE:
return {...state, active: false, term: searchInitialState.term, forceSearch: false};
case searchSetTerm.type: {
const act = action as ReturnType<typeof searchSetTerm>;
return {...state, term: act, forceSearch: act.force};
}
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:
return {...state, models: action.payload.models, resultsCounter: state.resultsCounter + 1};
case SEARCH_ACTIONS.SEARCH_START:
return {...state, resultsCounter: 0};
case SEARCH_ACTIONS.SEARCH_CLEAR:
return {
...state,
...searchInitialState
};
default: {
return state;
}
}
}
export const dashboardSearchReducers = [
on(searchActivate, (state) => ({...state, active: true})),
on(searchDeactivate, (state) => ({...state, active: false, term: searchInitialState.term, forceSearch: false, scrollIds: null, resultsCount: null})),
on(searchSetTerm, (state, action) => ({...state, term: action, forceSearch: action.force, scrollIds: null})),
on(setProjectsResults, (state, action) => ({
...state,
projects: action.scrollId === state.scrollIds?.[activeSearchLink.projects] ? state.projects.concat(action.projects) : action.projects,
scrollIds: {...state.scrollIds, [activeSearchLink.projects]: action.scrollId}})),
on(setPipelinesResults, (state, action) => ({
...state,
pipelines: action.scrollId === state.scrollIds?.[activeSearchLink.pipelines] ? state.pipelines.concat(action.pipelines) : action.pipelines,
scrollIds: {...state.scrollIds, [activeSearchLink.pipelines]: action.scrollId}})),
on(setOpenDatasetsResults, (state, action) => ({
...state,
openDatasets: action.scrollId === state.scrollIds?.[activeSearchLink.openDatasets] ? state.openDatasets.concat(action.openDatasets) : action.openDatasets,
scrollIds: {...state.scrollIds, [activeSearchLink.openDatasets]: action.scrollId}})),
on(setExperimentsResults, (state, action) => ({
...state,
experiments: action.scrollId === state.scrollIds?.[activeSearchLink.experiments] ? state.experiments.concat(action.experiments) : action.experiments,
scrollIds: {...state.scrollIds, [activeSearchLink.experiments]: action.scrollId}})),
on(setModelsResults, (state, action) => ({
...state,
models: action.scrollId === state.scrollIds?.[activeSearchLink.models] ? state.models.concat(action.models) : action.models,
scrollIds: {...state.scrollIds, [activeSearchLink.models]: action.scrollId}})),
on(setResultsCount, (state, action) => ({...state, resultsCount: action.counts})),
on(clearSearchResults, (state) => ({
...state,
[activeSearchLink.models]: [],
[activeSearchLink.experiments]: [],
[activeSearchLink.pipelines]: [],
[activeSearchLink.projects]: [],
})),
on(searchClear, (state) => ({...state, ...searchInitialState})),
] as ReducerTypes<DashboardSearchState, any>[];
export const dashboardSearchReducer = createReducer(
searchInitialState,
...dashboardSearchReducers
);
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 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);
export const selectSearch = createFeatureSelector<DashboardSearchState>('search');
export const selectProjectsResults = createSelector(selectSearch, (state: DashboardSearchState): Array<Project> => state.projects);
export const selectExperimentsResults = createSelector(selectSearch, (state: DashboardSearchState): Array<Task> => state.experiments);
export const selectModelsResults = createSelector(selectSearch, (state: DashboardSearchState): Array<Model> => state.models);
export const selectPipelinesResults = createSelector(selectSearch, (state: DashboardSearchState): Array<Project> => state.pipelines);
export const selectDatasetsResults = createSelector(selectSearch, (state: DashboardSearchState): Array<Project> => state.openDatasets);
export const selectActiveSearch = createSelector(selectSearch, (state: DashboardSearchState): boolean => state.term?.query?.length >= 3 || state.forceSearch);
export const selectSearchTerm = createSelector(selectSearch, (state: DashboardSearchState): SearchState['searchQuery'] => state.term);
export const selectResultsCount = createSelector(selectSearch, (state: DashboardSearchState): Map<ActiveSearchLink, number> => state.resultsCount);
export const selectSearchScrollIds = createSelector(selectSearch, (state: DashboardSearchState): Map<ActiveSearchLink, string> => state.scrollIds);

View File

@@ -4,4 +4,7 @@
<ng-container *ngTemplateOutlet="cardTemplate; context: {result}"></ng-container>
</ng-container>
</div>
<div class="load-more" *ngIf="showLoadMoreButton && (resultRows$ | async).length">
<button (click)="loadMoreClicked.emit()" class="btn btn-cml-primary load-more-btn">LOAD MORE</button>
</div>
</cdk-virtual-scroll-viewport>

View File

@@ -13,3 +13,14 @@
}
}
}
.load-more {
display: flex;
justify-content: center;
width: 100%;
padding: 15px;
.load-more-btn {
padding: 8px 40px;
}
}

View File

@@ -22,22 +22,33 @@ export class SearchResultsComponent {
public resultRows$: Observable<any[][]>;
public trackById = trackById;
public rowWidth = 300;
private _cardTemplate: TemplateRef<any>;
@Input() set cardTemplate(cardTemplate: TemplateRef<any>) {
this.viewPort?.scrollToIndex(0);
this._cardTemplate = cardTemplate;
}
get cardTemplate() {
return this._cardTemplate;
}
@Input() cardTemplate: TemplateRef<any>;
@Input() set results(results: any[]) {
this.results$.next(results);
this.viewPort?.scrollToIndex(0);
}
@Input() cardHeight = 246;
@Input() showLoadMoreButton = false;
@Output() resultClicked = new EventEmitter<any>();
@ViewChild(CdkVirtualScrollViewport) viewPort : CdkVirtualScrollViewport;
@Output() loadMoreClicked = new EventEmitter();
@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 =>
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));
@@ -46,7 +57,7 @@ export class SearchResultsComponent {
.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
this.rowWidth = cards * CARD_WIDTH + (cards - 1) * 24;
return chunk(cards, results);
}));
});

View File

@@ -15,3 +15,4 @@ export const setRecentExperiments = createAction(
DASHBOARD_PREFIX + '[set recent experiments]',
props<{experiments: IRecentTask[]}>()
);

View File

@@ -44,7 +44,7 @@ export class CommonDashboardEffects {
page_size: CARDS_IN_ROW,
active_users: (showOnlyUserWork ? [user.id] : null),
only_fields: ['name', 'company', 'user', 'created', 'default_output_destination']
} as ProjectsGetAllExRequest).pipe(
}).pipe(
mergeMap(({projects}) => [setRecentProjects({projects}), deactivateLoader(action.type)]),
catchError(error => [deactivateLoader(action.type), requestFailed(error)])
)

View File

@@ -1,59 +1,99 @@
import {InitSearch, ResetSearch} from '../common-search/common-search.actions';
import {skip} from 'rxjs/operators';
import {initSearch, resetSearch} from '../common-search/common-search.actions';
import {filter, skip} from 'rxjs/operators';
import {Model} from '~/business-logic/model/models/model';
import {SearchDeactivate, searchStart} from '../dashboard-search/dashboard-search.actions';
import {clearSearchResults, getCurrentPageResults, searchClear, searchDeactivate, searchStart} from '../dashboard-search/dashboard-search.actions';
import {IRecentTask} from './common-dashboard.reducer';
import {ITask} from '~/business-logic/model/al-task';
import {Observable} from 'rxjs';
import {ICommonSearchState, selectSearchQuery} from '../common-search/common-search.reducer';
import {Observable, Subscription} from 'rxjs';
import {SearchState, selectSearchQuery} from '../common-search/common-search.reducer';
import {Store} from '@ngrx/store';
import {
selectActiveSearch, selectExperimentsResults, selectModelsResults, selectPipelinesResults, selectProjectsResults,
selectResultsCounter,
selectActiveSearch, selectDatasetsResults, selectExperimentsResults, selectModelsResults, selectPipelinesResults, selectProjectsResults, selectResultsCount, selectSearchScrollIds,
selectSearchTerm
} from '../dashboard-search/dashboard-search.reducer';
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';
import {activeLinksList, ActiveSearchLink, activeSearchLink} from '~/features/dashboard-search/dashboard-search.consts';
import {Component, OnDestroy, OnInit} from '@angular/core';
import {Router} from '@angular/router';
export abstract class DashboardSearchComponentBase {
abstract store;
abstract router;
@Component({
selector: 'sm-dashboard-search-base',
template: `<sm-search-results-page
*ngIf="activeSearch$ | async"
(projectSelected)="projectCardClicked($event)"
(experimentSelected)="taskSelected($event)"
(modelSelected)="modelSelected($event)"
(pipelineSelected)="pipelineSelected($event)"
(activeLinkChanged)="activeLinkChanged($event)"
(openDatasetSelected)="openDatasetCardClicked($event)"
(loadMoreClicked)="loadMore()"
[projectsList]="projectsResults$ | async"
[pipelinesList]="pipelinesResults$ | async"
[datasetsList]="datasetsResults$ | async"
[experimentsList]="experimentsResults$ | async"
[modelsList]="modelsResults$ | async"
[activeLink]="activeLink"
[resultsCount]="resultsCount$ | async">
</sm-search-results-page>`,
})
export class DashboardSearchBaseComponent implements OnInit, OnDestroy{
public activeLink = 'projects' as ActiveSearchLink;
private searchSubs;
public searchQuery$: Observable<ICommonSearchState['searchQuery']>;
private allResultsSubscription: Subscription;
public searchQuery$: Observable<SearchState['searchQuery']>;
public activeSearch$: Observable<boolean>;
protected readonly resultsCounter$: Observable<number>;
public modelsResults$: Observable<Array<Model>>;
public projectsResults$: Observable<Array<Project>>;
public experimentsResults$: Observable<any>;
public searchTerm$: Observable<ICommonSearchState['searchQuery']>;
public searchTerm$: Observable<SearchState['searchQuery']>;
public pipelinesResults$: Observable<Project[]>;
public datasetsResults$: Observable<Project[]>;
private scrollIds: Map<ActiveSearchLink, string>;
public resultsCount$: Observable<Map<ActiveSearchLink, number>>;
constructor(store: Store<any>){
constructor(public store: Store<any>, public router: Router){
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.datasetsResults$ = store.select(selectDatasetsResults);
this.projectsResults$ = store.select(selectProjectsResults);
this.experimentsResults$ = store.select(selectExperimentsResults);
this.searchTerm$ = store.select(selectSearchTerm);
this.resultsCount$ = store.select(selectResultsCount);
this.syncAppSearch();
}
public ngOnInit(): void {
this.allResultsSubscription = this.resultsCount$.pipe(
filter(resultsCount => !!resultsCount),
).subscribe((resultsCount) => {
return this.setFirstActiveLink(resultsCount);
});
}
ngOnDestroy(): void {
this.store.dispatch(searchClear());
this.searchTermChanged('');
this.stopSyncSearch();
this.allResultsSubscription.unsubscribe();
}
stopSyncSearch() {
this.store.dispatch(new ResetSearch());
this.store.dispatch(resetSearch());
this.searchSubs.unsubscribe();
}
syncAppSearch() {
this.store.dispatch(new InitSearch('Search for all'));
this.store.dispatch(initSearch({payload: 'Search for all'}));
this.searchSubs = this.searchQuery$
.pipe(skip(1))
.subscribe(query => this.searchTermChanged(query?.query, query?.regExp));
this.searchSubs.add(this.store.select(selectSearchScrollIds).subscribe(scrollIds => this.scrollIds = scrollIds));
}
public modelSelected(model: Model) {
@@ -64,10 +104,10 @@ export abstract class DashboardSearchComponentBase {
public searchTermChanged(term: string, regExp?: boolean) {
if (term && term.length > 0) {
this.store.dispatch(searchStart({query:term, regExp, force: term.length < 3}));
this.store.dispatch(searchStart({query:term, regExp, force: term.length < 3, activeLink: this.activeLink}));
} else {
this.activeLink = 'projects';
this.store.dispatch(new SearchDeactivate());
this.activeLink = activeSearchLink.projects;
this.store.dispatch(searchDeactivate());
}
}
@@ -75,11 +115,17 @@ 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 openDatasetCardClicked(project: Project) {
this.router.navigateByUrl(`datasets/simple/${project.id}/experiments`);
this.store.dispatch(setSelectedProjectId({projectId: project.id, example: isExample(project)}));
}
public taskSelected(task: IRecentTask | ITask) {
// TODO ADD task.id to route
const projectId = task.project ? task.project.id : '*';
@@ -89,14 +135,26 @@ export abstract class DashboardSearchComponentBase {
public activeLinkChanged(activeLink) {
this.activeLink = activeLink;
if (!this.scrollIds?.[activeLink]) {
this.store.dispatch(getCurrentPageResults({activeLink}));
}
}
setFirstActiveLink(allResults, tabsIndexes) {
if (!(allResults[tabsIndexes.indexOf(this.activeLink)].length > 0)) {
const firstTabIndex = allResults.findIndex(list => list.length > 0);
setFirstActiveLink(resultsCount) {
if (resultsCount[this.activeLink] > 0) {
this.activeLinkChanged(this.activeLink);
} else {
const firstTabIndex = activeLinksList.findIndex(activeLink => resultsCount[activeLink.name] > 0);
if (firstTabIndex > -1) {
this.activeLink = tabsIndexes[firstTabIndex];
this.activeLinkChanged(activeLinksList[firstTabIndex].name);
} else {
this.store.dispatch(clearSearchResults());
}
}
}
loadMore() {
this.store.dispatch(getCurrentPageResults({activeLink: this.activeLink}));
}
}

View File

@@ -0,0 +1,18 @@
<div [id]="step.stepId" class="step-container pointer"
[class.selected]="selected">
<div class="step-part step-title" [class]="step?.data?.status">
<div
class="title"
smShowTooltipIfEllipsis
[smTooltip]="step?.data?.name ?? step.name + step?.data?.version ? ' v' + step.data.version : ''"
matTooltipPosition="above"
>{{step?.data?.name ?? step.name}}<ng-container *ngIf="step?.data?.version"> v{{step.data.version}}</ng-container></div>
<i class="al-icon sm-md al-ico-console" (click)="openConsole.emit()"></i>
</div>
<div class="step-part step-footer" [class]="step?.data?.status">
<div *ngIf="step?.data?.job_size">{{step.data.job_size | filesize: fileSizeConfigStorage}}</div>
<div class="d-flex-center" *ngIf="step?.data?.last_update">
<i class="al-icon al-ico-upload sm mr-1"></i>{{step.data.last_update * 1000 | timeAgo}}
</div>
</div>
</div>

View File

@@ -0,0 +1,10 @@
:host {
flex: 0 0 220px;
.step-footer {
font-size: 12px;
}
.title {
max-width: 162px; // use for ellipsis
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DatasetVersionStepComponent } from './dataset-version-step.component';
describe('DatasetVersionStepComponent', () => {
let component: DatasetVersionStepComponent;
let fixture: ComponentFixture<DatasetVersionStepComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DatasetVersionStepComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DatasetVersionStepComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,14 @@
import { Component} from '@angular/core';
import {
PipelineControllerStepComponent
} from '@common/pipelines-controller/pipeline-controller-step/pipeline-controller-step.component';
import { fileSizeConfigStorage } from '@common/shared/pipes/filesize.pipe';
@Component({
selector: 'sm-dataset-version-step',
templateUrl: './dataset-version-step.component.html',
styleUrls: ['../../pipelines-controller/pipeline-controller-step/pipeline-controller-step.component.scss', './dataset-version-step.component.scss']
})
export class DatasetVersionStepComponent extends PipelineControllerStepComponent{
fileSizeConfigStorage = fileSizeConfigStorage;
}

View File

@@ -0,0 +1,64 @@
<sm-card class="project-card"
(click)="projectClicked()"
[isExample]="!['All Experiments'].includes(project.name) && !isRootProject && (!project.company || ! project.company.id)"
[isFolder]="false"
[oneTabMode]="true"
[subFolderTitle]="project?.sub_projects?.length + ' sub projects'"
>
<div header-content [class.rename-state]="editName.active">
<div class="d-flex justify-content-between align-items-center card-name">
<sm-inline-edit #editName
class="title edit-name dark"
[originalText]="project.name | shortProjectName"
[editable]="true"
pattern="^[^/]+$"
[inlineDisabled]="true"
(textChanged)="prepareProjectNameForChange($event)"
(inlineActiveStateChanged)="projectNameEditActiveChanged($event)"
>
<span class="project-name"
matTooltipPosition="above"
[smTooltip]="project.name"
>{{project.name | shortProjectName}}</span>
</sm-inline-edit>
<sm-pipeline-card-menu
class="menu-wrapper"
*ngIf="!hideMenu"
[project]="project"
[allTags]="allTags"
(run)="run.emit()"
(rename)="editName.inlineActivated()"
(addTag)="addTag.emit($event)"
(delete)="delete.emit()"
></sm-pipeline-card-menu>
</div>
<div *ngIf="project.last_update; else: noRun" class="last-run">Updated {{project.last_update | timeAgo}}</div>
<ng-template #noRun><div class="last-run"></div></ng-template>
</div>
<div class="d-flex justify-content-around w-100">
<sm-circle-counter
[counter]="project.stats?.active?.total_tasks"
label="VERSIONS"
></sm-circle-counter>
<sm-circle-counter
[counter]="[
{value: project?.dataset_stats?.file_count | NA, label: 'FILES'},
{value: project?.dataset_stats?.total_size | filesize : fileSizeConfigStorage | NA, label: 'SIZE'}
]"
label="LATEST VERSION"
></sm-circle-counter>
</div>
<div footer class="footer-tags">
<sm-tag-list
*ngIf="!hideMenu; else: ReadOnlyTags"
class="w-100"
[tags]="project.tags"
(remove)="removeTag.emit($event)"
smClickStopPropagation
></sm-tag-list>
<ng-template #ReadOnlyTags>
<sm-tag-list [tags]="project.tags"></sm-tag-list>
</ng-template>
</div>
</sm-card>

View File

@@ -0,0 +1,40 @@
@import "../../shared/ui-components/styles/variables";
:host {
cursor: pointer;
.card-name {
height: 32px;
}
.project-name {
max-width: 280px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
color: $blue-200;
}
.last-run {
margin-bottom: 6px;
font-size: 12px;
color: $blue-300;
}
.title {
font-weight: 500;
}
.footer-tags {
display: flex;
min-height: 38px;
background-color: $blue-600;
padding: 0 16px;
align-items: center;
flex-wrap: wrap;
}
.rename-state .last-run {
visibility: hidden;
height: 18px;
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleDatasetCardComponent } from './simple-dataset-card.component';
describe('SimpleDatasetCardComponent', () => {
let component: SimpleDatasetCardComponent;
let fixture: ComponentFixture<SimpleDatasetCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SimpleDatasetCardComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SimpleDatasetCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import {Component} from '@angular/core';
import {PipelineCardComponent} from '@common/pipelines/pipeline-card/pipeline-card.component';
import { fileSizeConfigStorage } from '@common/shared/pipes/filesize.pipe';
@Component({
selector: 'sm-simple-dataset-card',
templateUrl: './simple-dataset-card.component.html',
styleUrls: ['./simple-dataset-card.component.scss']
})
export class SimpleDatasetCardComponent extends PipelineCardComponent{
fileSizeConfigStorage = {...fileSizeConfigStorage, spacer: '', round: 1};
}

View File

@@ -0,0 +1,37 @@
<mat-menu #menu="matMenu" [hasBackdrop]="false" class="line-item" xPosition="before">
<div
class="action-tooltip"
#menuHesitate="hesitate"
[delay]="1000"
[action]="'leave'"
(smHesitate)="menu.closed.emit();"
>
<p class="command">{{command}}</p>
<div class="w-100 d-flex flex-row-reverse">
<div
class="d-flex-center copy-button pointer"
ngxClipboard
[cbContent]="command"
(cbOnSuccess)="$event.event.stopPropagation(); copied()"
>Copy command</div><i class="al-icon al-ico-success sm mr-1" [class.visible]="copySuccess"></i>
</div>
</div>
</mat-menu>
<span
*ngIf="command"
class="d-flex-center download-button"
[delay]="1000" [action]="'leave'"
(smHesitate)="menuHesitate.hesitateStatus && menu.closed.emit()"
>
<i class="al-icon al-ico-download pointer line-item"
#idElement
[matMenuTriggerFor]="menu"
(click)="openMenu(); menuHesitate.hesitateStatus = true"
></i>
</span>
<sm-table
[columns]="columns"
[tableData]="tableData"
[selectionMode]="null"
[scrollable]="true"
></sm-table>

View File

@@ -0,0 +1,62 @@
@import 'variables.scss';
:host {
display: block;
position: relative;
.download-button {
position: absolute;
right: 24px;
top: 12px;
z-index: 21;
color: $blue-300;
}
::ng-deep sm-table {
th:nth-child(1) {padding-left: 24px !important;}
td:nth-child(1) {padding-left: 24px !important;}
}
}
.action-tooltip {
max-width: 400px;
background: $purple;
padding: 8px;
border-radius: 4px;
box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.16);
font-size: 11px;
div, p {
color: $white;
&.command {
background-color: $black;
padding: 6px 12px;
border-radius: 4px;
font-family: monospace;
}
}
div.copy-button {
color: $light-periwinkle-two;
}
.copy-button {
&:hover {
color: $white;
text-decoration: underline;
}
}
.al-ico-success {
background-color: $pipeline-queued;
border-radius: 50%;
opacity: 0;
transition: opacity 200ms;
&.visible {
opacity: 1;
}
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleDatasetVersionContentComponent } from './simple-dataset-version-content.component';
describe('SimpleDatasetVersionContentComponent', () => {
let component: SimpleDatasetVersionContentComponent;
let fixture: ComponentFixture<SimpleDatasetVersionContentComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SimpleDatasetVersionContentComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SimpleDatasetVersionContentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,49 @@
import {ChangeDetectionStrategy, Component, Input, ViewChild} from '@angular/core';
import {MatMenuTrigger} from '@angular/material/menu';
import {ISmCol} from '@common/shared/ui-components/data/table/table.consts';
import {fileSizeConfigStorage, FileSizePipe} from '@common/shared/pipes/filesize.pipe';
@Component({
selector: 'sm-simple-dataset-version-content',
templateUrl: './simple-dataset-version-content.component.html',
styleUrls: ['./simple-dataset-version-content.component.scss']
})
export class SimpleDatasetVersionContentComponent {
public columns: ISmCol[];
public tableData: string[][];
public command: string;
private ngFile = new FileSizePipe();
@ViewChild(MatMenuTrigger) trigger: MatMenuTrigger;
copySuccess: boolean;
@Input() set id(id: string) {
this.command = `clearml-data get --id ${id}`;
this.copySuccess = false;
}
@Input() set data(csv: string) {
const lines = csv?.split('\n') ?? [];
const header = lines.splice(0, 1)[0] ?? '';
this.columns = header.split(/, ?/).map((caption, index) => ({
id: `${index}`,
header: caption,
style: {width: index === 1 ? '5px' : '300px'}
}));
const tableData = lines.map(line => line.split(/, ?/));
if (Number(tableData[0]?.[1]) && this.columns[1]?.header?.includes('ize')) {
tableData.forEach( line => line[1] = this.ngFile.transform(parseInt(line[1], 10) || 0, fileSizeConfigStorage) as string);
}
this.tableData = tableData;
}
openMenu() {
this.trigger.openMenu();
}
copied() {
this.copySuccess = true;
window.setTimeout(() => this.copySuccess = false, 3000);
}
}

View File

@@ -0,0 +1,72 @@
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<div class="expand-header">VERSION INFO</div>
</mat-expansion-panel-header>
<div class="panel-body" *ngIf="entity">
<div class="header">
<div class="name ellipsis">{{entity.name}}<ng-container *ngIf="entity?.runtime?.version"> v{{entity.runtime.version}}</ng-container></div>
<span class="status" [class]="entity.status">{{entity.status | replaceViaMapPipe:convertStatusMap | replaceViaMapPipe:convertStatusMapBase}}</span>
</div>
<div class="section">
<div class="param">
<div class="key">ID</div>
<div class="value d-flex align-item-center justify-content-between" [smTooltip]="entity.id" smShowTooltipIfEllipsis>
<span>{{entity.id?.slice(0, 8)}}</span>
<i
class="pointer al-icon al-ico-copy-to-clipboard sm"
ngxClipboard
[cbContent]="entity?.id"
(cbOnSuccess)="copyToClipboard()"
></i>
</div>
</div>
<div class="param continue">
<div class="key">Size</div>
<div class="value" [smTooltip]="entity?.runtime?.ds_total_size + ' (original)'" smShowTooltipIfEllipsis>{{$any(entity?.runtime?.ds_total_size) | filesize : fileSizeConfigStorage || '-'}}<span class="comment">(original)</span></div>
</div>
<div class="param">
<div class="key"></div>
<div class="value" [smTooltip]="entity?.runtime?.ds_total_size_compressed + ' (compressed)'" smShowTooltipIfEllipsis>{{$any(entity?.runtime?.ds_total_size_compressed) | filesize : fileSizeConfigStorage || '-'}}<span class="comment">(compressed)</span></div>
</div>
<div class="param">
<div class="key">File count</div>
<div class="value" [smTooltip]="entity?.runtime?.ds_file_count" smShowTooltipIfEllipsis>{{entity?.runtime?.ds_file_count ?? '-'}}</div>
</div>
<div class="param">
<div class="key">Link count</div>
<div class="value" [smTooltip]="entity?.runtime?.ds_link_count" smShowTooltipIfEllipsis>{{entity?.runtime?.ds_link_count ?? '-'}}</div>
</div>
</div>
<div class="section">
<div class="header">FILES CHANGED</div>
<div class="param">
<div class="key">Added</div>
<div class="value" [smTooltip]="entity?.runtime?.ds_change_add " smShowTooltipIfEllipsis>{{entity?.runtime?.ds_change_add ?? '-'}}</div>
</div>
<div class="param">
<div class="key">Modified</div>
<div class="value" [smTooltip]="entity?.runtime?.ds_change_modify " smShowTooltipIfEllipsis>{{entity?.runtime?.ds_change_modify ?? '-'}}</div>
</div>
<div class="param">
<div class="key">Removed</div>
<div class="value" [smTooltip]="entity?.runtime?.ds_change_remove " smShowTooltipIfEllipsis>{{entity?.runtime?.ds_change_remove ?? '-'}}</div>
</div>
<div class="param">
<div class="key">Size</div>
<div class="value"
*ngIf="entity?.runtime?.ds_change_size"
[smTooltip]="$any(entity?.runtime?.ds_change_size) | filesize: fileSizeConfigStorage"
smShowTooltipIfEllipsis
>{{$any(entity?.runtime?.ds_change_size) | filesize: fileSizeConfigStorage}}</div>
</div>
</div>
</div>
<footer *ngIf="entity?.id">
<a
class="arr-link"
target="_blank"
[href]="'/projects/' + project + '/experiments/' + entity?.id + '/output/execution'">
Task information<i class="al-icon al-ico-link-arrow sm"></i>
</a>
</footer>
</mat-expansion-panel>

View File

@@ -0,0 +1,35 @@
@import "variables";
:host {
.header {
width: 100%;
margin-top: 12px;
padding-bottom: 18px;
color: $blue-100;
border-bottom: solid 1px $dark-border;
}
.al-ico-copy-to-clipboard {
opacity: 0;
transition: opacity 0.3s;
}
.value:hover {
.al-ico-copy-to-clipboard {
opacity: 1;
}
}
.section {
.param {
&.continue {
border-bottom: unset;
}
.comment {
color: $blue-300;
margin-left: 4px;
}
}
}
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleDatasetVersionDetailsComponent } from './simple-dataset-version-details.component';
describe('SimpleDatasetVersionDetailsComponent', () => {
let component: SimpleDatasetVersionDetailsComponent;
let fixture: ComponentFixture<SimpleDatasetVersionDetailsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SimpleDatasetVersionDetailsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SimpleDatasetVersionDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,17 @@
import {Component} from '@angular/core';
import {PipelineInfoComponent} from '@common/pipelines-controller/pipeline-details/pipeline-info.component';
import { fileSizeConfigStorage } from '@common/shared/pipes/filesize.pipe';
import {DATASETS_STATUS_LABEL, EXPERIMENTS_STATUS_LABELS} from '~/features/experiments/shared/experiments.const';
@Component({
selector: 'sm-simple-dataset-version-details',
templateUrl: './simple-dataset-version-details.component.html',
styleUrls: ['./simple-dataset-version-details.component.scss', '../../pipelines-controller/pipeline-details/pipeline-info.component.scss']
})
export class SimpleDatasetVersionDetailsComponent extends PipelineInfoComponent {
public fileSizeConfigStorage = fileSizeConfigStorage;
public convertStatusMap = DATASETS_STATUS_LABEL;
public convertStatusMapBase = EXPERIMENTS_STATUS_LABELS;
}

View File

@@ -0,0 +1,82 @@
<sm-simple-dataset-version-details [entity]="selected$ | async" [project]="projectId$ | async" [step]="selectedEntity"></sm-simple-dataset-version-details>
<div class="console-button">
<button class="btn btn-cml-primary d-flex align-items-center" (click)="toggleDetails()">
<i class="al-icon al-ico-console sm mr-3"></i>DETAILS
</button>
</div>
<div
#diagramContainer
class="pipeline-container"
[class.extend]="showLog"
(click)="selectStep()"
>
<ng-container *ngIf="dagModel$ | async as dagModel">
<div *ngFor="let row of dagModel | reverse" class="level" [style.width.px]="chartWidth">
<sm-dataset-version-step
#taskEl
*ngFor="let step of row | uniqueBy: 'stepId'; trackBy: trackByStepId" [step]="step"
[selected]="selectedEntity && selectedEntity.id === step?.id"
(click)="$event.stopPropagation(); !taskEl.selected && selectStep(step)"
(openConsole)="openLog()"
></sm-dataset-version-step>
</div>
<svg class="arrows"
*ngIf="chartWidth"
[attr.viewBox]="'0 0 ' + chartWidth + ' ' + (50 + 132 * dagModel?.length)"
[style.width.px]="chartWidth"
[style.height.px]="50 + 132 * dagModel?.length"
>
<g
*ngFor="let arrow of arrows; trackBy: trackArrows"
[class.selected]="arrow.selected"
>
<path [attr.d]="arrow.path" fill="none" stroke-width="2"></path>
<polygon
points="0,-6 12,0, 0,6"
[attr.transform]="arrow.headTransform"
/>
</g>
</svg>
</ng-container>
</div>
<div class="results-panel" [class.extend]="showLog" [class.maximized]="maximizeResults">
<ng-container *ngIf="showLog">
<div class="header toggle">
<div class="log-name">
<i class="al-icon al-ico-console mr-2"></i>
<span *ngIf="(selected$ | async) as selected">
{{selected?.name}}<ng-container *ngIf="selected?.runtime?.version"> v{{selected.runtime.version}}</ng-container>
</span>
</div>
<sm-button-toggle
[value]="detailsPanelMode"
[options]="[
{label: 'CONTENT', value: statusOption.content},
{label: 'PREVIEW', value: statusOption.preview},
{label: 'CONSOLE', value: statusOption.log}
]"
(valueChanged)="detailsPanelMode = $event"
></sm-button-toggle>
<div class="close">
<i class="al-icon pointer" [class]="maximizeResults ? 'al-ico-min-panel' : 'al-ico-max-panel'" (click)="toggleResultSize()"></i>
<i class="al-icon al-ico-dialog-x pointer ml-4" (click)="openLog(false)"></i>
</div>
</div>
<div [ngSwitch]="detailsPanelMode" class="content">
<sm-experiment-output-log
*ngSwitchCase="statusOption.log"
[experiment]="selected$ | async"
[isDarkTheme]="true"
[showHeader]="false"
></sm-experiment-output-log>
<sm-simple-dataset-version-preview *ngSwitchCase="statusOption.preview" [selected]="(selected$ | async)">
</sm-simple-dataset-version-preview>
<sm-simple-dataset-version-content
*ngSwitchCase="statusOption.content"
class="h-100"
[id]="selectedEntity?.data?.job_id"
[data]="(selected$ | async)?.configuration?.['Dataset Content']?.value"
></sm-simple-dataset-version-content>
</div>
</ng-container>
</div>

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