Added pipeline parameters component and integrated with pipeline dialog.

This commit is contained in:
Shubham Takode 2024-02-15 23:27:28 +05:30
parent 6f37b4ee0d
commit ca102ecd2c
11 changed files with 546 additions and 6 deletions

View File

@ -1,6 +1,7 @@
import {ProjectsGetAllResponseSingleSubProjects} from '~/business-logic/model/projects/projectsGetAllResponseSingleSubProjects';
import {Stats} from '~/business-logic/model/projects/stats';
import {ProjectsGetAllResponseSingleDatasetStats} from '~/business-logic/model/projects/projectsGetAllResponseSingleDatasetStats';
import { PipelinesParameter } from './pipelinesParameter';
/**
* pipelines
@ -69,5 +70,5 @@ export interface Pipeline {
own_models?: number;
hidden?: boolean;
parameters?: Array<object>
parameters?: Array<PipelinesParameter>
}

View File

@ -0,0 +1,21 @@
export interface PipelinesParameter {
id?: string;
/**
* Name of the parameter. The combination of section and name should be unique
*/
name?: string;
/**
* Value of the parameter
*/
value?: string;
/**
* Type of the parameter. Optional
*/
type?: string;
/**
* The parameter description. Optional
*/
description?: string;
default?:string;
}

View File

@ -52,6 +52,34 @@
<textarea class="pipeline-description" name="description" matInput [(ngModel)]="pipeline.description"
#description="ngModel"></textarea>
</mat-form-field>
<div class="parameters">
<div class="search">
<mat-label>Parameters</mat-label>
<sm-search search-button
#search
class="table-search"
[value]="searchedText"
[enableNavigation]="true"
[minimumChars]="1"
[debounceTime]="0"
[expandOnHover]="true"
[searchResultsCount]="searchResultsCount"
[searchCounterIndex]="pipelineParamsForm.matchIndex"
(valueChanged)="searchTable($event)"
></sm-search>
</div>
<sm-pipeline-parameters
#pipelineParamsForm
class="form-section"
[section]=""
[formData]="pipeline?.parameters"
(formDataChanged)="onFormValuesChanged($event)"
(searchCounterChanged)="searchCounterChanged($event)"
(resetSearch)="search.clear(false)"
(scrollToResultCounterReset)="scrollIndexCounterReset()"
></sm-pipeline-parameters>
</div>
<div class="w-100 create-pipeline-button">
<button class="btn btn-dark-fill center" data-id="Create Pipeline" [disabled]="pipelineForm.invalid"
(click)="send()">CREATE PIPELINE

View File

@ -1,7 +1,37 @@
@import "variables";
:host {
.create-report-button {
padding: 32px 12px 0;
}
.parameters {
display: block;
position: relative;
height: 100%;
margin-bottom: 50px;
sm-pipeline-parameters {
display: block;
height: 124px;
}
div.search {
display: flex;
flex-direction: row;
margin-bottom: 8px;
// padding-right: 4px;
justify-content: space-between;
align-items: center;
sm-search.table-search {
border-radius: 4px;
color: $blue-280 !important;
background-color: $blue-600 !important;
}
}
}
mat-form-field {
width: 100%;
@ -9,4 +39,4 @@
min-height: 68px;
}
}
}
}

View File

@ -1,5 +1,6 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
@ -16,6 +17,9 @@ import {rootProjectsPageSize} from '@common/constants';
import {
IOption
} from '@common/shared/ui-components/inputs/select-autocomplete-with-chips/select-autocomplete-with-chips.component';
import { PipelinesParameter } from '~/business-logic/model/pipelines/pipelinesParameter';
import { cloneDeep } from 'lodash-es';
import { PipelineParametersComponent } from '@common/pipelines/pipeline-parameters/pipeline-parameters.component';
@Component({
@ -37,16 +41,29 @@ export class CreateNewPipelineFormComponent implements OnChanges, OnDestroy {
public pipelinesNames: Array<string>;
public projectsNames: Array<string>;
public pipeline: { name: string; description: string; project: { label: string; value: string }, parameters: Array<object>, tags: Array<string> } = {
public pipeline: { name: string; description: string; project: { label: string; value: string }, parameters: Array<PipelinesParameter>, tags: Array<string> } = {
name: null,
description: '',
project: null,
parameters: [],
parameters: [{
name: "Paramter1",
value: ""
}, {
name: "Parameter2",
value: ""
}],
tags: [],
};
filterText: string = '';
isAutoCompleteOpen: boolean;
// for parameters
@ViewChild('pipelineParamsForm', {static: false}) pipelineParamsForm: PipelineParametersComponent;
public searchedText: string;
public searchResultsCount: number;
public scrollIndexCounter: number;
public size$: Observable<number>;
@Input() readOnlyProjectsNames: string[];
@Input() defaultProjectId: string;
public loading: boolean;
@ -66,6 +83,24 @@ export class CreateNewPipelineFormComponent implements OnChanges, OnDestroy {
this.projectsNames = this.projectsOptions.map(project => project.label);
}
constructor(/* private store: Store, protected router: Router, */ private cdr: ChangeDetectorRef) {
// this.selectedSectionHyperParams$ = this.store.select(selectExperimentHyperParamsSelectedSectionParams);
// this.editable$ = this.store.select(selectIsExperimentEditable);
// this.selectedSection$ = this.store.select(selectExperimentHyperParamsSelectedSectionFromRoute);
// this.isInDev$ = this.store.select(selectIsSelectedExperimentInDev);
// this.saving$ = this.store.select(selectIsExperimentSaving);
// this.backdropActive$ = this.store.select(selectBackdropActive);
// this.routerConfig$ = this.store.select(selectRouterConfig);
// this.selectedExperiment$ = this.store.select(selectSelectedExperiment);
// this.size$ = this.store.select(selectSplitSize);
// this.store.dispatch(setExperimentFormErrors({errors: null}));
// this.selectedSectionSubscription = this.selectedSection$.subscribe(section => {
// this.selectedSection = section;
// this.propSection = section === 'properties';
// });
}
get projects() {
return this._projects;
}
@ -116,6 +151,9 @@ export class CreateNewPipelineFormComponent implements OnChanges, OnDestroy {
}
send() {
if (this.pipelineParamsForm.formData.length > 0) {
this.pipeline.parameters = cloneDeep(this.pipelineParamsForm.formData);
}
this.pipelineCreated.emit(this.pipeline);
}
@ -134,5 +172,35 @@ export class CreateNewPipelineFormComponent implements OnChanges, OnDestroy {
isFocused(locationRef: HTMLInputElement) {
return document.activeElement === locationRef;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
searchTable(value: string) {
// const searchBackward = value === null;
// if (this.searchedText !== value && !searchBackward) {
// this.searchedText = value;
// this.scrollIndexCounter = -1;
// this.searchResultsCount = 0;
// // this.executionParamsForm.resetIndex();
// this.cdr.detectChanges();
// }
// // this.executionParamsForm.jumpToNextResult(!searchBackward);
}
searchCounterChanged(count: number) {
this.searchResultsCount = count;
this.cdr.detectChanges();
}
scrollIndexCounterReset() {
this.scrollIndexCounter = -1;
this.cdr.detectChanges();
}
onFormValuesChanged(event: { field: string; value: any }) {
// eslint-disable-next-line no-console
console.log(event);
// this.store.dispatch(updateExperimentAtPath({path: ('hyperparams.' + event.field), value: event.value}));
}
}

View File

@ -1,4 +1,4 @@
:host{
width: 640px;
width: 690px;
display: block;
}

View File

@ -0,0 +1,115 @@
<form [class.editable]="editable" #hyperParameters="ngForm">
<cdk-virtual-scroll-viewport #formContainer class="form-container" itemSize="58" minBufferPx="280" >
<ng-container *ngIf="editable">
<div *ngFor="let parameter of formData; let index= index" class="w-100 d-flex">
<mat-form-field
[hintLabel]="parameterKey.invalid && parameterKey?.errors?.required? '*Required': ''"
appearance="outline"
class="strength"
>
<input
#parameterKey="ngModel"
#row="matInput"
[(ngModel)]="parameter['name']"
(keydown.enter)="nextRow($event, index)"
placeholder="Parameter"
name="parameterKey-{{parameter.name}}-{{index}}"
matInput
smUniqueNameValidator
[class.highlight-text]="(searchedText?.length > 0) && parameter['name'].includes(searchedText)"
[class.current-match]="searchIndexList[matchIndex]?.index===index && searchIndexList[matchIndex]?.col==='name'"
[existingNames]="formNames(parameter.name)"
required/>
<mat-error *ngIf="parameterKey.invalid && parameterKey?.errors?.required">
Required
</mat-error>
<mat-error *ngIf="parameterKey.invalid && parameterKey?.errors?.smNotAllowedStringsValidator">
.(dot) $(dollar) and space are not allowed in parameter key.
</mat-error>
<mat-error *ngIf="!parameterKey?.errors?.required && parameterKey.invalid && parameterKey?.errors?.uniqueName">
key already exists
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline"
class="strength">
<input
#parameterValue="ngModel"
[(ngModel)]="parameter['value']"
(keydown.enter)="nextRow($event, index)"
name="parameterValue-{{parameter.name}}-{{index}}"
placeholder="Value"
[class.highlight-text]="(searchedText?.length > 0) && parameter['value'].includes(searchedText)"
[class.current-match]="searchIndexList[matchIndex]?.index===index && searchIndexList[matchIndex]?.col==='value'"
matInput/>
</mat-form-field>
<button class="remove-button btn" (click)="removeRow(index)">
<i class="al-icon al-ico-trash al-color blue-400 sm-md pointer flashing-icon"></i>
</button>
</div>
</ng-container>
</cdk-virtual-scroll-viewport>
<button *ngIf="editable" class="btn btn-dark-fill add-parameter" (click)="addRow()"><i class="fas fa-plus" data-id="AddParamterButton"></i> ADD PARAMETER</button>
</form>
<!-- <div class="table-container" *ngIf="!editable">
<sm-table
[columns]="cols"
columnResizeMode="fit"
[tableData]="formData"
[enableTableSearch]="true"
[globalFilterFields]="['name', 'value','description']"
[noHeader]="true"
[simple]="true"
[scrollable]="true"
[virtualScrollOptions]="{ trackBy: trackByIndex, appendOnly: true, delay: 0, orientation: 'vertical', items: formData, itemSize: 32}"
[virtualScroll]="true"
[rowHeight]="32"
(rowClicked)="rowActivated($event)"
>
<ng-template pTemplate="body"
let-col
let-i="rowIndex"
let-row="rowData">
<ng-container [ngSwitch]="col.id">
<ng-container *ngSwitchCase="'description'">
<span *ngIf="row.type!=='legacy' && (row.description || row.type)"
class="allow-multi-space pointer"
customClass="hyper-parameters-tooltip parameter-tooltip"
smTooltip="{{(row.type ? ('Type: '+ row.type+'\n') : '') + (row?.description|| '')}}"
matTooltipPosition="before"
><i class="al-icon al-ico-description"></i></span>
</ng-container>
<span
*ngSwitchCase="'name'"
class="ellipsis name"
smShowTooltipIfEllipsis
matTooltipPosition="before"
[matTooltipShowDelay]="250"
[smTooltip]="row.name"
[smSearchText]="searchedText"
highlightClass="highlight-text"
[class.current-match]="searchIndexList[matchIndex]?.index===i && searchIndexList[matchIndex]?.col==='name'"
>{{row.name}}</span>
<ng-container *ngSwitchCase="'value'">
<a *ngIf="row.section === 'Datasets' && row.value.length === 32; else: noLink"
(click)="$event.preventDefault(); navigateToDataset(row.value)"
href="" target="_blank"
>{{row.value}}</a>
<ng-template #noLink>
<span
class="ellipsis"
smShowTooltipIfEllipsis
matTooltipPosition="before"
[matTooltipShowDelay]="250"
[smTooltip]="row.value"
[smSearchText]="searchedText"
highlightClass="highlight-text"
[class.current-match]="searchIndexList[matchIndex]?.index===i && searchIndexList[matchIndex]?.col==='value'"
>{{row.value}}</span>
</ng-template>
</ng-container>
</ng-container>
</ng-template>
</sm-table>
</div> -->

View File

@ -0,0 +1,80 @@
@import "variables";
@import "mixins/link";
:host {
@include link();
form.editable, .table-container {
height: 100%;
padding-bottom: 6px;
}
form.editable {
padding-bottom: 6px;
}
.form-container {
height: 100%;
overflow: auto;
input.highlight-text {
background: $neon-yellow;
&.current-match {
background: #f5d655;
}
}
}
.param-row {
margin-bottom: 12px;
}
mat-form-field {
flex: 1;
padding-right: 12px;
}
::ng-deep tr:hover {
cursor: auto !important;
transition: background-color 0.3s;
}
.separator {
width: 100%;
//border-bottom: solid 1px #dee1e9;
}
.remove-button {
margin-bottom: 22px;
padding: 0;
}
.param-row {
font-weight: 500;
& > div {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.current-match .highlight-text{
background-color: #f5d655;
}
.name {
font-weight: 500;
}
.allow-multi-space {
white-space: pre;
}
.add-parameter {
border: 1px solid $grey-purple;
background-color: #ffffff;
color: $grey-purple;
transition: all ease-in-out 0.15s, border ease-in-out 0.15s;
}
}

View File

@ -0,0 +1,193 @@
import {
AfterViewInit,
Component,
EventEmitter,
Input, OnChanges,
OnDestroy,
Output,
QueryList, SimpleChanges,
ViewChild,
ViewChildren
} from '@angular/core';
import {IExperimentInfoFormComponent} from '~/features/experiments/shared/experiment-info.model';
import {cloneDeep} from 'lodash-es';
import {v4 as uuidV4} from 'uuid';
import {NgForm} from '@angular/forms';
import {ParamsItem} from '~/business-logic/model/tasks/paramsItem';
import {ISmCol} from '@common/shared/ui-components/data/table/table.consts';
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
import {Subscription} from 'rxjs';
// import {TableComponent} from '@common/shared/ui-components/data/table/table.component';
import {isEqual} from 'lodash-es';
import {MatInput} from '@angular/material/input';
import {Store} from '@ngrx/store';
import { navigateToDataset } from '@common/experiments/actions/common-experiments-info.actions';
import {trackByIndex} from '@common/shared/utils/forms-track-by';
import { PipelinesParameter } from '~/business-logic/model/pipelines/pipelinesParameter';
@Component({
selector: 'sm-pipeline-parameters',
templateUrl: './pipeline-parameters.component.html',
styleUrls: ['./pipeline-parameters.component.scss']
})
export class PipelineParametersComponent implements IExperimentInfoFormComponent, OnDestroy, AfterViewInit, OnChanges {
private _formData = [] as PipelinesParameter[];
private formContainersSub: Subscription;
private _editable: boolean = true;
private formContainer: CdkVirtualScrollViewport;
private clickedRow: number;
public search = '';
public searchIndexList: {index: number; col: string}[] = [];
public matchIndex = -1;
public cols = [
{id: 'name', style: {width: '300px'}},
{id: 'value', style: {width: '300px'}},
{id: 'description', style: {width: '48px'}}
] as ISmCol[];
@ViewChild('hyperParameters') hyperParameters: NgForm;
// @ViewChild(TableComponent) executionParametersTable: TableComponent<PipelinesParameter>;
@ViewChildren('formContainer') formContainers: QueryList<CdkVirtualScrollViewport>;
@ViewChildren('row') rows: QueryList<MatInput>;
@ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport;
@Output() formDataChanged = new EventEmitter<{ field: string; value: ParamsItem[] }>();
@Output() searchCounterChanged = new EventEmitter<number>();
@Output() resetSearch = new EventEmitter();
@Output() scrollToResultCounterReset = new EventEmitter();
@Input() section;
private _originalData: PipelineParametersComponent['formData'];
// @Input() set size(size: number) {
// this.executionParametersTable?.resize();
// }
@Input() set formData(formData) {
this._originalData = formData;
this._formData = cloneDeep(formData).map((row: ParamsItem) => ({...row, id: uuidV4()}));
// console.log(this._formData);
}
get formData() {
return this._formData;
}
// @Input() set editable(editable: boolean) {
// this._editable = editable;
// this.executionParametersTable?.resize();
// editable && window.setTimeout(() => this.rows.first?.focus());
// }
get editable() {
return this._editable;
}
@Input() searchedText: string;
constructor(private store: Store) {}
ngAfterViewInit() {
this.formContainersSub = this.formContainers.changes
.subscribe((list: QueryList<CdkVirtualScrollViewport>) => {
this.formContainer = list.first;
if (this.formContainer && this.clickedRow !== null) {
this.formContainer.scrollToIndex(this.clickedRow, 'smooth');
this.clickedRow = null;
}
});
//this.executionParametersTable.resize();
}
formNames(name) {
return this.formData.filter(parameter => parameter.name !== name).map(parameter => parameter.name);
}
addRow() {
this.formData.push({
id: uuidV4(),
name: '',
value: '',
description: '',
type: ''
});
window.setTimeout(() => {
const height = this.viewPort.elementRef.nativeElement.scrollHeight;
this.viewPort.scrollToIndex(height, 'smooth');
}, 50);
}
removeRow(index) {
this.formData.splice(index, 1);
}
rowActivated({data}: { data: PipelinesParameter; e: MouseEvent }) {
this.clickedRow = this.formData.findIndex(row => row.name === data.name);
}
ngOnDestroy(): void {
this.formContainersSub?.unsubscribe();
}
resetIndex() {
this.matchIndex = -1;
}
jumpToNextResult(forward: boolean) {
this.matchIndex = forward ? this.matchIndex + 1 : this.matchIndex - 1;
if (this.editable) {
this.viewPort.scrollToIndex(this.searchIndexList[this.matchIndex]?.index, 'smooth');
} else {
// this.executionParametersTable.scrollToIndex(this.searchIndexList[this.matchIndex]?.index);
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes?.formData && (!changes.formData?.firstChange) && !isEqual(changes.formData.currentValue, changes.formData.previousValue)) {
this.matchIndex = -1;
this.searchIndexList = [];
this.resetSearch.emit();
}
if (changes?.searchedText) {
let searchResultsCounter = 0;
const searchedIndexList = [];
if (changes?.searchedText?.currentValue) {
this.formData.forEach((parameter, index) => {
if (parameter?.name.includes(changes.searchedText.currentValue)) {
searchResultsCounter++;
searchedIndexList.push({index, col: 'name'});
}
if (parameter?.value.includes(changes.searchedText.currentValue)) {
searchResultsCounter++;
searchedIndexList.push({index, col: 'value'});
}
});
}
this.searchCounterChanged.emit(searchResultsCounter);
this.scrollToResultCounterReset.emit();
this.searchIndexList = searchedIndexList;
}
}
cancel() {
this._formData = cloneDeep(this._originalData).map((row: ParamsItem) => ({...row, id: uuidV4()}));
}
nextRow(event: Event, index: number) {
event.stopPropagation();
event.preventDefault();
if (this.formData.length === index + 1) {
this.addRow();
}
window.setTimeout(()=> this.rows.get(index + 1)?.focus());
}
navigateToDataset(datasetId: string) {
this.store.dispatch(navigateToDataset({datasetId}));
}
protected readonly trackByIndex = trackByIndex;
}

View File

@ -138,7 +138,7 @@ if __name__ == '__main__':
this.dialog.open(PipelineDialogComponent, {
data: {defaultProjectId: this.projectId},
panelClass: 'light-theme',
width: '640px'
width: '690px'
})
.afterClosed()
.subscribe(pipeline => {

View File

@ -53,6 +53,8 @@ import { createUserPrefFeatureReducer } from "@common/core/meta-reducers/user-pr
import { PIPELINES_PREFIX } from "./pipelines.actions";
import { PipelineAddStepDialogComponent } from "./pipeline-add-step-dialog/pipeline-add-step-dialog.component";
import { PipelineAddStepFormComponent } from "./pipeline-add-step-dialog/pipeline-add-step-form/pipeline-add-step-form.component";
import {SortPipe} from '@common/shared/pipes/sort.pipe';
import { PipelineParametersComponent } from "./pipeline-parameters/pipeline-parameters.component";
export const pipelinesSyncedKeys = ["projects.showPipelineExamples"];
const pipelinesSyncedKeys2 = ['orderBy', 'sortOrder'];
@ -103,6 +105,7 @@ const getInitState = (userPreferences: UserPreferences) => ({
PipelineAddStepFormComponent,
EditPipelinePageComponent,
EditPipelineHeaderComponent,
PipelineParametersComponent,
],
imports: [
CommonModule,
@ -145,6 +148,7 @@ const getInitState = (userPreferences: UserPreferences) => ({
ExperimentSharedModule,
RefreshButtonComponent,
LabeledFormFieldDirective,
SortPipe,
],
exports: [PipelinesPageComponent, EditPipelinePageComponent],
providers: [