mirror of
https://github.com/clearml/clearml-web
synced 2025-03-13 07:08:17 +00:00
corrected mistake in drawer component
This commit is contained in:
parent
8905347cce
commit
579b6a34ab
@ -1,57 +0,0 @@
|
||||
<div class="console-button">
|
||||
<button class="btn btn-cml-primary d-flex align-items-center" (click)="openDetailsDrawer()">
|
||||
<i class="al-icon al-ico-console sm me-3"></i>DETAILS
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-drawer #drawer mode="over" position="end" class="drawer-container" [ngClass]="{'maximized': isMaximized, 'closing': isDrawerClosing }">
|
||||
<div class="header">
|
||||
<div class="left-section">
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="center-section">
|
||||
<button class="toggle-button" [class.active]="currentTab === 'parameter'" (click)="toggleTab('parameter')">PARAMETERS</button>
|
||||
<button class="toggle-button" [class.active]="currentTab === 'code'" (click)="toggleTab('code')">CODE</button>
|
||||
</div>
|
||||
<div class="right-section">
|
||||
<button type="button" class="btn btn-icon g-btn" (click)="toggleMaximize()">
|
||||
<i tabindex="1002" class="al-icon pointer" [class]="isMaximized ? 'al-ico-min-panel' : 'al-ico-max-panel'"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-icon g-btn" (click)="close()">
|
||||
<i class="al-icon al-ico-dialog-x sm-md"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabs-content">
|
||||
<div *ngIf="currentTab === 'parameter' && pipelineData?.parameters?.length > 0; else noParameterContent">
|
||||
<div *ngFor="let parameter of pipelineData.parameters">
|
||||
<div class="parameter-data"><p>Name: {{ parameter.name }}</p>
|
||||
<div class="parameter-input">Value:
|
||||
<input class="full-width-textarea" type="text"
|
||||
[ngModel]="parameter.value"
|
||||
[placeholder]="!parameter.value ? 'Enter value' : ''"
|
||||
(ngModelChange)="onInputChange($event, parameter)"
|
||||
(blur)="updateParameterValue(parameter)">
|
||||
</div></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #noParameterContent>
|
||||
<div *ngIf="currentTab === 'parameter' && !pipelineParameters" class="no-data-container">
|
||||
<i class="icon i-pipeline-no-code mx-auto xxl no-data-icon"></i>
|
||||
<span class="no-data-text">NO PARAMETER</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div *ngIf="currentTab === 'code' && pipelineCode; else noCodeContent">
|
||||
{{ pipelineCode }}
|
||||
</div>
|
||||
<ng-template #noCodeContent>
|
||||
<div *ngIf="currentTab === 'code' && !pipelineCode" class="no-data-container">
|
||||
<i class="icon i-pipeline-no-code mx-auto xxl no-data-icon"></i>
|
||||
<span class="no-data-text">NO CODE</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</mat-drawer>
|
||||
|
@ -1,128 +0,0 @@
|
||||
// // :host {
|
||||
// // display: block;
|
||||
// // width: 100%;
|
||||
@import "variables";
|
||||
mat-drawer{
|
||||
display: grid;
|
||||
}
|
||||
.drawer-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
transition: bottom 0.35s, visibility 0.35s, height ease-in-out 0.5s;
|
||||
height: 60vh; /* Default height */
|
||||
width: calc(100% - 64px); /* Assuming full width */
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.drawer-container.maximized {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
transition: bottom 0.35s, visibility 0.35s, height ease-in-out 0.5s;
|
||||
height: 100vh; /* Default height */
|
||||
width: calc(100% - 64px); /* Assuming full width */
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
}
|
||||
.drawer-container.closing {
|
||||
height: 0;
|
||||
transition: bottom 0.35s, visibility 0.35s, ease-in-out 0.5s; /* Apply transition when closing */
|
||||
}
|
||||
.drawer {
|
||||
width: 100vw; // Make drawer cover the entire viewport width
|
||||
bottom: 0; // Position drawer at the bottom of the screen
|
||||
}
|
||||
.console-button {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: 24px;
|
||||
z-index: 2;
|
||||
}
|
||||
.dialog-container {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 40%; /* Adjust as needed */
|
||||
max-height: 100%;
|
||||
background-color: #141722;
|
||||
transition: bottom 0.35s, visibility 0.35s, height ease-in-out 0.5s; /* Add transition for smooth animation */
|
||||
z-index: -1 !important;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #202432;
|
||||
background-color: #202432;
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
padding: 10px;
|
||||
background-color: #141722;
|
||||
color:#9fabc9;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.full-width-textarea {
|
||||
width: 100%;
|
||||
background-color: #384161;
|
||||
}
|
||||
.parameter-data{
|
||||
background-color: #202432;
|
||||
padding: 5px;
|
||||
}
|
||||
.parameter-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.toggle-button {
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background-color: #2c3246;
|
||||
padding: 15px 25px;
|
||||
transition: background-color 0.3s;
|
||||
color: white;
|
||||
margin-right: -3px;
|
||||
}
|
||||
.toggle-button:last-child {
|
||||
margin-right: 0; /* Remove margin for the last button */
|
||||
}
|
||||
.toggle-button:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.toggle-button:last-child {
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
.toggle-button.active {
|
||||
background-color: #384161; /* Adjust the background color for active state */
|
||||
}
|
||||
|
||||
.no-data-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.no-data-icon {
|
||||
font-size: 6rem;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.no-data-text {
|
||||
margin-top: 15px;
|
||||
font-size: 1.5rem;
|
||||
color: #aaa;
|
||||
}
|
||||
// /* Add more styles as needed */
|
||||
// // }
|
@ -1,71 +0,0 @@
|
||||
import { Component, OnInit, ViewChild, Input } from '@angular/core';
|
||||
import { MatDrawer } from '@angular/material/sidenav';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import {
|
||||
updatePipeline,
|
||||
} from "../pipelines.actions";
|
||||
@Component({
|
||||
selector: 'sm-pipeline-details-drawer',
|
||||
templateUrl: './pipeline-details-drawer.component.html',
|
||||
styleUrls: ['./pipeline-details-drawer.component.scss']
|
||||
})
|
||||
export class PipelineDetailsDrawerComponent implements OnInit {
|
||||
@ViewChild('drawer') drawer: MatDrawer;
|
||||
@Input() pipelineId: string;
|
||||
@Input() pipelineData: any;
|
||||
currentTab: 'parameter' | 'code' = 'parameter'; // Default tab
|
||||
isMaximized: boolean = false;
|
||||
isDrawerClosing: boolean = false;
|
||||
pipelineParameters: string = '';
|
||||
pipelineCode: string = '';
|
||||
parameterValue!: string;
|
||||
|
||||
constructor(private store: Store) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pipelineParameters = this.pipelineData?.parameters;
|
||||
this.pipelineCode = '';
|
||||
}
|
||||
|
||||
openDetailsDrawer(): void {
|
||||
this.drawer.open();
|
||||
}
|
||||
|
||||
toggleMaximize(): void {
|
||||
this.isMaximized = !this.isMaximized;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.isDrawerClosing = true;
|
||||
this.drawer.close();
|
||||
this.isMaximized = false;
|
||||
setTimeout(() => {
|
||||
this.isDrawerClosing = false;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
toggleTab(tab: 'parameter' | 'code'): void {
|
||||
this.currentTab = tab;
|
||||
}
|
||||
|
||||
onInputChange(newValue: any, parameter: any): void {
|
||||
// Update the parameter value when the input changes
|
||||
this.parameterValue = newValue;
|
||||
}
|
||||
updateParameterValue(parameter: any): void {
|
||||
// Perform deep clone to avoid mutating original object
|
||||
const updatedPipelineData = cloneDeep(this.pipelineData);
|
||||
|
||||
// Find the index of the parameter in the parameters array
|
||||
const index = updatedPipelineData.parameters.findIndex(p => p.id === parameter.id);
|
||||
|
||||
// Update the parameter value if found
|
||||
if (index !== -1) {
|
||||
updatedPipelineData.parameters[index].value = this.parameterValue;
|
||||
}
|
||||
// Dispatch action to update pipeline data in the store
|
||||
this.store.dispatch(updatePipeline({ changes: updatedPipelineData }));
|
||||
}
|
||||
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
(deleteStep)="stepDelete($event)"
|
||||
[ioOptions]="selectedStepInputOutputOptions"
|
||||
(stepParamsChanged)="selectedStepParamsChanged($event)"/>
|
||||
<sm-pipeline-details-drawer [pipelineId]="pipelineId" [pipelineData]="selectedPipeline"></sm-pipeline-details-drawer>
|
||||
<sm-pipeline-details-drawer [pipelineData]="selectedPipeline" (pipelineParamsChanged)="pipelineParamsChanged($event)"></sm-pipeline-details-drawer>
|
||||
<sm-flow-editor *ngIf="selectedPipeline?.flow_display?.nodes?.length" [pipelineData]="selectedPipeline"
|
||||
(nodesChangedInReactFlow)="nodesChangedInReactFlow($event)"
|
||||
(nodeClicked)="nodeSelected($event)"
|
||||
|
@ -9,13 +9,21 @@
|
||||
transition: height 0.3s;
|
||||
background-color: #0D0E15;
|
||||
|
||||
sm-flow-editor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.console-button {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: 24px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
sm-pipeline-step-info {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
runPipeline,
|
||||
updatePipelineStep,
|
||||
deletePipelineStep,
|
||||
setSelectedPipeline,
|
||||
} from "../pipelines.actions";
|
||||
import { selectRouterParams } from "@common/core/reducers/router-reducer";
|
||||
import { Observable, Subscription, combineLatest, map } from "rxjs";
|
||||
@ -335,6 +336,13 @@ export class EditPipelinePageComponent implements OnInit, OnDestroy {
|
||||
//this.store.dispatch(setSelectedPipeline({data: cloneDeep(pipelineState)}))
|
||||
}
|
||||
|
||||
public pipelineParamsChanged(changedParams) {
|
||||
const pipelineState = cloneDeep(this.selectedPipeline);
|
||||
pipelineState.flow_display.nodes = this.reactFlowState.nodes;
|
||||
pipelineState.flow_display.edges = this.reactFlowState.edges;
|
||||
pipelineState.parameters = cloneDeep(changedParams);
|
||||
this.store.dispatch(setSelectedPipeline({data: pipelineState}))
|
||||
}
|
||||
/**
|
||||
* @function getIncomingNodeIds
|
||||
* @description It is used to get the incoming node ids.
|
||||
|
@ -0,0 +1,108 @@
|
||||
<div class="console-button">
|
||||
<button class="btn btn-cml-primary d-flex align-items-center" (click)="selectStep(); openLog()" data-id="consoleDetailsButton">
|
||||
<i class="al-icon al-ico-console sm me-3"></i>DETAILS
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
#diagramContainer
|
||||
class="pipeline-container"
|
||||
tabindex="1"
|
||||
[class.extend]="showLog"
|
||||
(click)="selectStep()"
|
||||
(keyup)="selectStep()"
|
||||
data-id="pipelineContainer"
|
||||
>
|
||||
<ng-container *ngIf="dagModel$ | async as dagModel">
|
||||
<!-- <div *ngFor="let row of dagModel" class="level" [style.width.px]="chartWidth">
|
||||
<sm-pipeline-controller-step
|
||||
#taskEl
|
||||
*ngFor="let step of row; trackBy: trackByStepId" [step]="step"
|
||||
[selected]="selectedEntity && selectedEntity.id === step?.id"
|
||||
(click)="$event.stopPropagation(); !taskEl.selected && selectStep(step)"
|
||||
(openConsole)="openLog()"
|
||||
></sm-pipeline-controller-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" data-id="resultsPanel">
|
||||
<ng-container *ngIf="showLog">
|
||||
<div class="header toggle" >
|
||||
<div class="log-name">
|
||||
<i class="al-icon al-ico-console me-2"></i>
|
||||
<span>{{(selected$ | async)?.name}}</span>
|
||||
</div>
|
||||
<sm-button-toggle
|
||||
[value]="detailsPanelMode"
|
||||
[options]="[
|
||||
{label: 'PARAMETERS', value: statusOption.parameters},
|
||||
{label: 'CODE', value: statusOption.code}
|
||||
]"
|
||||
(valueChanged)="detailsPanelMode = $event"
|
||||
></sm-button-toggle>
|
||||
<div class="close">
|
||||
<i tabindex="1002" class="al-icon pointer" [class]="maximizeResults ? 'al-ico-min-panel' : 'al-ico-max-panel'" (click)="toggleResultSize()" (keyup)="toggleResultSize()" data-id="maxResultPanel"></i>
|
||||
<i tabindex="1003" class="al-icon al-ico-dialog-x pointer ms-4" (click)="openLog(false)" (keyup)="openLog(false)" data-id="closeResultPanel"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div [ngSwitch]="detailsPanelMode" class="content">
|
||||
<div *ngSwitchCase="statusOption.parameters" >
|
||||
<ng-container *ngFor="
|
||||
let param of pipelineData?.parameters;
|
||||
let index = index
|
||||
">
|
||||
<div class="param">
|
||||
<div class="key" [smTooltip]="param.name" smShowTooltipIfEllipsis>
|
||||
{{ param.name }}
|
||||
</div>
|
||||
<div class="value">
|
||||
<input [(ngModel)]="param['value']" placeholder="Enter parameter value"
|
||||
|
||||
(ngModelChange)="paramsChanged()" name="parameterKey-{{ param.id }}-{{ index }}" matInput required
|
||||
#optsInput="ngModel" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
<p class=" ms-3 mt-4 mb-4" style="font-size: smaller;">
|
||||
Note: Make sure to hit the <i>save</i> button to save pipeline parameters.
|
||||
</p>
|
||||
</div>
|
||||
<!-- <sm-simple-dataset-version-preview
|
||||
*ngSwitchCase="statusOption.preview" [selected]="(selected$ | async)"
|
||||
></sm-simple-dataset-version-preview> -->
|
||||
<!-- <sm-experiment-output-log
|
||||
*ngSwitchCase="statusOption.log"
|
||||
[experiment]="selected$ | async"
|
||||
[isDarkTheme]="true"
|
||||
[showHeader]="false"
|
||||
></sm-experiment-output-log> -->
|
||||
<ng-container *ngSwitchCase="statusOption.code" >
|
||||
<div *ngIf="stepDiff; else empty" class="ace" #aceEditor></div>
|
||||
<ng-template #empty>
|
||||
<div class="no-code" data-id="noCode">
|
||||
<i class="icon no-output-icon i-no-code-dark"></i>
|
||||
<h4>NO CODE</h4>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
@ -0,0 +1,221 @@
|
||||
@import "variables";
|
||||
|
||||
$log-height: 300px;
|
||||
$log-header-height: 48px;
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: $blue-950;
|
||||
.results-panel {
|
||||
&.maximized {
|
||||
height: 100%;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sm-pipeline-info {
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
.console-button {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: 24px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.results-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: $log-height + $log-header-height;
|
||||
background-color: $blue-900;
|
||||
|
||||
position: absolute;
|
||||
bottom: -($log-height + $log-header-height);
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
|
||||
visibility: hidden;
|
||||
transition: bottom 0.35s, visibility 0.35s, height ease-in-out 0.5s;
|
||||
|
||||
&.extend {
|
||||
visibility: visible;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: $log-header-height;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 24px;
|
||||
padding: 0 24px;
|
||||
background-color: $blue-700;
|
||||
border-bottom: 1px solid $dark-border;
|
||||
color: $blue-100;
|
||||
box-shadow: 0 -2px 6px rgb(0 0 0 / 40%);
|
||||
|
||||
&.toggle {
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
height: $log-height;
|
||||
overflow: auto;
|
||||
.param {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
border-bottom: solid 1px $dark-border;
|
||||
padding: 14px;
|
||||
|
||||
.key {
|
||||
flex: 0.5 1 0;
|
||||
color: $blue-300;
|
||||
margin-right: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1 1 0;
|
||||
color: $blue-100;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep sm-experiment-output-log {
|
||||
.no-output {
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
left: 50%;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
display: block;
|
||||
text-align: center;
|
||||
h4 {
|
||||
color: $blue-250;
|
||||
}
|
||||
.no-output-icon {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
color: $blue-250;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ace {
|
||||
height: 100%;
|
||||
|
||||
&.ace-monokai {
|
||||
background-color: $blue-900;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.level {
|
||||
min-width: 100%;
|
||||
margin: 50px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 74px;
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-container {
|
||||
height: calc(100% + 14px);
|
||||
width: calc(100% + 14px);
|
||||
position: relative;
|
||||
overflow: scroll;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&.extend {
|
||||
padding-bottom: $log-height;
|
||||
}
|
||||
|
||||
.arrows {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
pointer-events: none;
|
||||
stroke: $blue-400;
|
||||
fill: $blue-400;
|
||||
|
||||
.selected {
|
||||
stroke: $blue-200;
|
||||
fill: $blue-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sm-pipeline-controller-step {
|
||||
margin: 0 50px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.log-name {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
> span {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
|
||||
.al-icon {
|
||||
color: $blue-300;
|
||||
&:hover {
|
||||
color: $blue-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-code {
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
left: 50%;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
text-align: center;
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
.icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
@ -0,0 +1,462 @@
|
||||
import {
|
||||
AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef, EventEmitter, HostListener, inject,
|
||||
Input,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import {DagModelItem} from '@ngneat/dag';
|
||||
import {fromEvent, Observable, Subscription} from 'rxjs';
|
||||
import {debounceTime, distinctUntilChanged, filter, map, mergeMap, takeUntil, tap, throttleTime} from 'rxjs/operators';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {selectSelectedExperiment} from '~/features/experiments/reducers';
|
||||
import {selectRouterParams} from '../../../core/reducers/router-reducer';
|
||||
import * as commonInfoActions from '../../../experiments/actions/common-experiments-info.actions';
|
||||
import {
|
||||
getSelectedPipelineStep,
|
||||
setSelectedPipelineStep
|
||||
} from '../../../experiments/actions/common-experiments-info.actions';
|
||||
import {selectPipelineSelectedStepWithFallback} from '../../../experiments/reducers';
|
||||
import {IExperimentInfo} from '~/features/experiments/shared/experiment-info.model';
|
||||
import {getBoxToBoxArrow} from 'curved-arrows';
|
||||
import {Ace, edit} from 'ace-builds';
|
||||
import {TaskStatusEnum} from '~/business-logic/model/tasks/taskStatusEnum';
|
||||
import {selectScaleFactor} from '@common/core/reducers/view.reducer';
|
||||
import {DagManagerUnsortedService} from '@common/shared/services/dag-manager-unsorted.service';
|
||||
import {TaskTypeEnum} from '~/business-logic/model/tasks/taskTypeEnum';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
export enum StepStatusEnum {
|
||||
queued = 'queued',
|
||||
pending = 'pending',
|
||||
skipped = 'skipped',
|
||||
cached = 'cached',
|
||||
executed = 'executed',
|
||||
running = 'running',
|
||||
failed = 'failed',
|
||||
aborted = 'aborted',
|
||||
completed = 'completed'
|
||||
}
|
||||
|
||||
export interface TreeStep {
|
||||
base_task_id: string,
|
||||
queue: string,
|
||||
parents: string[],
|
||||
executed: string;
|
||||
status: StepStatusEnum;
|
||||
job_type: TaskTypeEnum;
|
||||
job_started: number;
|
||||
job_ended: number;
|
||||
job_code_section: string;
|
||||
job_id: string;
|
||||
}
|
||||
|
||||
export interface PipelineItem extends DagModelItem {
|
||||
name: string;
|
||||
id: string;
|
||||
data: TreeStep;
|
||||
}
|
||||
|
||||
export interface Arrow {
|
||||
path: string;
|
||||
headTransform: string;
|
||||
selected: boolean;
|
||||
targetId: string;
|
||||
}
|
||||
|
||||
export enum StatusOption {
|
||||
code = 'Code',
|
||||
parameters = 'Parameters',
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'sm-pipeline-details-drawer',
|
||||
templateUrl: './pipeline-details-drawer.component.html',
|
||||
styleUrls: ['./pipeline-details-drawer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [DagManagerUnsortedService]
|
||||
})
|
||||
|
||||
export class PipelineDetailsDrawerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChildren('taskEl', {read: ElementRef}) tasksElements!: QueryList<ElementRef>;
|
||||
@ViewChild('start', {read: ElementRef}) startingElement: ElementRef;
|
||||
@ViewChild('end', {read: ElementRef}) endingElement: ElementRef;
|
||||
@ViewChild('diagramContainer') diagramContainer: ElementRef<HTMLDivElement>;
|
||||
@ViewChildren('aceEditor') private aceEditorElements: QueryList<ElementRef>;
|
||||
//@Input() pipelineData: any;
|
||||
@Output() pipelineParamsChanged = new EventEmitter<unknown>();
|
||||
private _pipelineData;
|
||||
|
||||
@Input() set pipelineData(pipelineData) {
|
||||
this._pipelineData = pipelineData ? cloneDeep(pipelineData) : null;
|
||||
}
|
||||
get pipelineData() {
|
||||
return this._pipelineData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private aceEditor: Ace.Editor;
|
||||
private sub = new Subscription();
|
||||
private dragging: boolean;
|
||||
public chartWidth = 0;
|
||||
public arrows: Arrow[];
|
||||
public diagramRect: DOMRect;
|
||||
|
||||
public dagModel$: Observable<PipelineItem[][]>;
|
||||
public selected$: Observable<IExperimentInfo>;
|
||||
public selectedEntity: PipelineItem;
|
||||
public projectId$: Observable<string>;
|
||||
protected pipelineController: PipelineItem[];
|
||||
private skipAutoCenter: boolean;
|
||||
private scale: number;
|
||||
showLog: boolean;
|
||||
public infoData: IExperimentInfo;
|
||||
public stepDiff: string;
|
||||
statusOption = StatusOption;
|
||||
detailsPanelMode = StatusOption.parameters;
|
||||
defaultDetailsMode = StatusOption.parameters;
|
||||
|
||||
trackArrows = (index: number, arrow: Arrow) => arrow.path;
|
||||
trackByStepId = (index: number, step) => step.stepId;
|
||||
public maximizeResults: boolean;
|
||||
protected _dagManager: DagManagerUnsortedService<PipelineItem>;
|
||||
protected store: Store;
|
||||
protected cdr: ChangeDetectorRef;
|
||||
protected zone: NgZone;
|
||||
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
onKeyDown(e: KeyboardEvent) {
|
||||
if ((e.key == 'Escape')) {
|
||||
this.showLog = false;
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._dagManager = inject(DagManagerUnsortedService<PipelineItem>);
|
||||
this.store = inject(Store);
|
||||
this.cdr = inject(ChangeDetectorRef);
|
||||
this.zone = inject(NgZone);
|
||||
|
||||
this.sub.add(this.store.select(selectRouterParams)
|
||||
.pipe(
|
||||
debounceTime(150),
|
||||
map(params => this.getEntityId(params)),
|
||||
filter(id => !!id),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe(id => {
|
||||
this.skipAutoCenter = false;
|
||||
this.store.dispatch(commonInfoActions.resetExperimentInfo());
|
||||
this.store.dispatch(commonInfoActions.getExperimentInfo({id}));
|
||||
this.store.dispatch(setSelectedPipelineStep({step: null}));
|
||||
})
|
||||
);
|
||||
|
||||
this.sub.add(this.store.select(selectScaleFactor).subscribe(factor => this.scale = 100 / factor));
|
||||
|
||||
this.selected$ = this.store.select(selectPipelineSelectedStepWithFallback);
|
||||
|
||||
this.projectId$ = this.store.select(selectRouterParams)
|
||||
.pipe(map(params => params?.projectId));
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sub.add(this.store.select(selectSelectedExperiment)
|
||||
.subscribe(task => {
|
||||
if (task?.id !== this.infoData?.id) {
|
||||
this.selectedEntity = null;
|
||||
this.stepDiff = null;
|
||||
this.detailsPanelMode = this.getPanelMode();
|
||||
}
|
||||
this.infoData = task;
|
||||
const width = this.diagramContainer?.nativeElement.getBoundingClientRect().width;
|
||||
this.chartWidth = Math.max(Number.isNaN(width) ? 0 : width, 4000);
|
||||
this.dagModel$ = this._dagManager.dagModel$
|
||||
.pipe(filter(model => model?.length > 0), tap(model =>
|
||||
model.forEach(row => this.chartWidth = Math.max(this.chartWidth, row.length * 300))));
|
||||
const pipelineObject = this.getTreeObject(task);
|
||||
this.pipelineController = this.convertPipelineToDagModel(pipelineObject);
|
||||
this.resetUninitializedRunningFields();
|
||||
this._dagManager.setNewItemsArrayAsDagModel(this.pipelineController);
|
||||
window.setTimeout(() => {
|
||||
if (!this.skipAutoCenter) {
|
||||
const element = this.diagramContainer.nativeElement;
|
||||
element.scroll({left: (element.scrollWidth - element.getBoundingClientRect().width) / 2});
|
||||
}
|
||||
this.removeLines();
|
||||
this.drawLines();
|
||||
this.cdr.markForCheck();
|
||||
}, 0);
|
||||
|
||||
if(this.selectedEntity) {
|
||||
this.selectStep(this.selectedEntity);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
paramsChanged() {
|
||||
setTimeout(() => {
|
||||
this.pipelineParamsChanged.emit(this.pipelineData.parameters);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
protected resetUninitializedRunningFields() {
|
||||
if (!this.infoData?.runtime?._pipeline_hash) {
|
||||
this.pipelineController.forEach(step => {
|
||||
step.data.job_started = null;
|
||||
step.data.job_ended = null;
|
||||
step.data.status = StepStatusEnum.pending;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getEntityId(params) {
|
||||
return params?.controllerId;
|
||||
}
|
||||
|
||||
convertPipelineToDagModel(pipeline): PipelineItem[] {
|
||||
let pipelineObj;
|
||||
try {
|
||||
pipelineObj = JSON.parse(pipeline);
|
||||
pipelineObj = Object.entries(pipelineObj).reduce((acc, [key, val]: [string, TreeStep]) => {
|
||||
acc[key] = {...val, parents: val.parents.map(parent => `${parent}`)};
|
||||
return acc;
|
||||
}, {});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const pipelineKeysArr = Object.keys(pipelineObj).reverse();
|
||||
// sort nodes to prevent @ngneat/dag from crashing
|
||||
const sortedNodes = [];
|
||||
let i = 0;
|
||||
while (pipelineKeysArr.length > 0 || i > pipelineKeysArr.length) {
|
||||
const node = pipelineObj[pipelineKeysArr[i]];
|
||||
const parents = node?.parents ?? [];
|
||||
if (parents.every(parent => sortedNodes.includes(parent))) {
|
||||
sortedNodes.push(pipelineKeysArr[i]);
|
||||
pipelineKeysArr.splice(i, 1);
|
||||
i = 0;
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return sortedNodes.map((key, index) => ({
|
||||
id: key,
|
||||
stepId: index + 1,
|
||||
parentIds: [0].concat(pipelineObj[key].parents.map(parent => sortedNodes.indexOf(parent) + 1)),
|
||||
name: key,
|
||||
branchPath: 1,
|
||||
data: {
|
||||
...pipelineObj[key],
|
||||
...(
|
||||
this.infoData.status === TaskStatusEnum.Stopped &&
|
||||
[TaskStatusEnum.Queued, TaskStatusEnum.InProgress, 'running'].includes(pipelineObj[key].status) &&
|
||||
{status: 'aborted'}
|
||||
)
|
||||
}
|
||||
} as PipelineItem));
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.sub.add(this.aceEditorElements.changes.subscribe(() => this.initAceEditor()));
|
||||
|
||||
fromEvent(this.diagramContainer.nativeElement, 'wheel')
|
||||
.pipe(throttleTime(1000))
|
||||
.subscribe(() => {
|
||||
this.skipAutoCenter = true;
|
||||
});
|
||||
|
||||
const mousedown$ = fromEvent(this.diagramContainer.nativeElement, 'mousedown');
|
||||
const mouseup$ = fromEvent(document, 'mouseup');
|
||||
const mousemove$ = fromEvent(this.diagramContainer.nativeElement, 'mousemove');
|
||||
|
||||
this.sub.add(mousedown$
|
||||
.pipe(
|
||||
mergeMap(() => mousemove$.pipe(
|
||||
tap(() => {
|
||||
this.dragging = true;
|
||||
this.skipAutoCenter = true;
|
||||
}),
|
||||
map((e: MouseEvent) => {
|
||||
e.preventDefault(); // prevent selecting text while dragging
|
||||
return {
|
||||
x: e.movementX,
|
||||
y: e.movementY
|
||||
};
|
||||
}),
|
||||
takeUntil(mouseup$)
|
||||
))
|
||||
)
|
||||
.subscribe(({x, y}: { x: number; y: number }) => {
|
||||
this.diagramContainer.nativeElement.scrollTo({
|
||||
top: this.diagramContainer.nativeElement.scrollTop - y,
|
||||
left: this.diagramContainer.nativeElement.scrollLeft - x,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
removeLines() {
|
||||
this.arrows = [];
|
||||
}
|
||||
|
||||
drawLines() {
|
||||
const pipeLineItems = this._dagManager.getSingleDimensionalArrayFromModel();
|
||||
this.arrows = [];
|
||||
this.diagramRect = this.diagramContainer.nativeElement.getBoundingClientRect();
|
||||
pipeLineItems.forEach((pipeLineItem) => {
|
||||
pipeLineItem.parentIds.forEach((parentId: number) => {
|
||||
if (parentId > 0) {
|
||||
const parent: ElementRef<HTMLDivElement> = this.tasksElements.find(
|
||||
(b: ElementRef) => parentId === +b.nativeElement.children[0].id
|
||||
);
|
||||
const self: ElementRef<HTMLDivElement> = this.tasksElements.find(
|
||||
(b: ElementRef) => +b.nativeElement.children[0].id === pipeLineItem.stepId
|
||||
);
|
||||
if (parent?.nativeElement && self?.nativeElement) {
|
||||
const fromRect = parent.nativeElement.getBoundingClientRect();
|
||||
const toRect = self.nativeElement.getBoundingClientRect();
|
||||
const [sx, sy, c1x, c1y, c2x, c2y, ex, ey, ae] = getBoxToBoxArrow(
|
||||
parent.nativeElement.offsetLeft, parent.nativeElement.offsetTop, fromRect.width / this.scale, fromRect.height / this.scale,
|
||||
self.nativeElement.offsetLeft, self.nativeElement.offsetTop, toRect.width / this.scale, toRect.height / this.scale,
|
||||
{padStart: 0, padEnd: 7}
|
||||
);
|
||||
this.arrows.push({
|
||||
path: `M${sx} ${sy} C${c1x} ${c1y}, ${c2x} ${c2y}, ${ex} ${ey}`,
|
||||
headTransform: `translate(${ex},${ey}) rotate(${ae})`,
|
||||
selected: false,
|
||||
targetId: pipeLineItem.id
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(commonInfoActions.setExperiment({experiment: null}));
|
||||
this.store.dispatch(setSelectedPipelineStep({step: null}));
|
||||
this.removeLines();
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
|
||||
selectStep(step?: PipelineItem) {
|
||||
if (step) {
|
||||
try {
|
||||
this.stepDiff = JSON.parse(this.infoData.configuration[step.data?.job_code_section || step.id]?.value)?.script?.diff;
|
||||
this.aceEditor?.getSession().setValue(this.stepDiff);
|
||||
} catch (e) {
|
||||
this.stepDiff = null;
|
||||
if (this.defaultDetailsMode === StatusOption.code) {
|
||||
this.detailsPanelMode = this.defaultDetailsMode;
|
||||
}
|
||||
}
|
||||
const id = this.infoData.runtime._pipeline_hash ? (step?.data?.job_id) : null;
|
||||
if (id) {
|
||||
this.store.dispatch(getSelectedPipelineStep({id}));
|
||||
} else {
|
||||
this.store.dispatch(setSelectedPipelineStep({step: {
|
||||
id,
|
||||
type: step.data.job_type,
|
||||
status: step.data.status as unknown as TaskStatusEnum,
|
||||
name: step.name
|
||||
}}));
|
||||
this.showLog = false;
|
||||
}
|
||||
this.selectedEntity = step;
|
||||
this.highlightArrows();
|
||||
} else if (!this.dragging) {
|
||||
this.stepDiff = null;
|
||||
this.detailsPanelMode = this.defaultDetailsMode;
|
||||
this.store.dispatch(setSelectedPipelineStep({step: null}));
|
||||
this.selectedEntity = null;
|
||||
this.highlightArrows();
|
||||
} else {
|
||||
this.dragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
openLog(show = true) {
|
||||
this.showLog = show;
|
||||
this.initAceEditor();
|
||||
}
|
||||
|
||||
private initAceEditor() {
|
||||
const aceEditorElement = this.aceEditorElements.first;
|
||||
if (!aceEditorElement) {
|
||||
this.aceEditor = null;
|
||||
return;
|
||||
}
|
||||
this.zone.runOutsideAngular(() => {
|
||||
const aceEditor = edit(aceEditorElement.nativeElement) as Ace.Editor;
|
||||
this.aceEditor = aceEditor;
|
||||
aceEditor.setOptions({
|
||||
readOnly: true,
|
||||
showLineNumbers: false,
|
||||
showGutter: false,
|
||||
fontFamily: 'SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace',
|
||||
fontSize: 13,
|
||||
highlightActiveLine: false,
|
||||
highlightSelectedWord: false,
|
||||
showPrintMargin: false,
|
||||
});
|
||||
|
||||
aceEditor.renderer.setScrollMargin(12, 12, 12, 12);
|
||||
aceEditor.renderer.setPadding(16);
|
||||
(aceEditor.renderer.container.querySelector('.ace_cursor') as HTMLElement).style.color = 'white';
|
||||
|
||||
aceEditor.container.style.lineHeight = '1.6';
|
||||
aceEditor.renderer.updateFontSize();
|
||||
|
||||
aceEditor.session.setMode('ace/mode/python');
|
||||
aceEditor.setTheme('ace/theme/monokai');
|
||||
aceEditor.getSession().setValue(this.stepDiff);
|
||||
|
||||
this.aceEditor = aceEditor;
|
||||
});
|
||||
}
|
||||
toggleResultSize() {
|
||||
// this.maximizeResults = ! this.maximizeResults;
|
||||
// if (this.detailsPanelMode === StatusOption.content) {
|
||||
// this.detailsPanelMode = null;
|
||||
// window.setTimeout(() => {
|
||||
// this.detailsPanelMode = StatusOption.content;
|
||||
// this.cdr.markForCheck();
|
||||
// }, 450);
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected highlightArrows() {
|
||||
this.arrows = this.arrows
|
||||
?.map(arrow => ({...arrow, selected: arrow.targetId === this.selectedEntity?.id}))
|
||||
.sort((a, b) => a.selected && !b.selected ? 1 : -1);
|
||||
}
|
||||
|
||||
protected getTreeObject(task) {
|
||||
return task?.configuration?.Pipeline?.value;
|
||||
}
|
||||
|
||||
protected getPanelMode() {
|
||||
return this.defaultDetailsMode;
|
||||
}
|
||||
}
|
@ -76,7 +76,7 @@ import { FileSizePipe } from "@common/shared/pipes/filesize.pipe";
|
||||
import { RegexPipe } from "@common/shared/pipes/filter-regex.pipe";
|
||||
import { FilterMonitorMetricPipe } from "@common/shared/pipes/filter-monitor-metric.pipe";
|
||||
import { PipelineStepParametersEditDialogComponent } from "./edit-pipeline-page/pipeline-step-parameters-edit-dialog/pipeline-step-parameters-edit-dialog.component";
|
||||
import { PipelineDetailsDrawerComponent } from './details-dialog/pipeline-details-drawer.component';
|
||||
import { PipelineDetailsDrawerComponent } from './edit-pipeline-page/pipeline-details-drawer/pipeline-details-drawer.component';
|
||||
|
||||
export const pipelinesSyncedKeys = ["projects.showPipelineExamples"];
|
||||
const pipelinesSyncedKeys2 = ["orderBy", "sortOrder"];
|
||||
|
Loading…
Reference in New Issue
Block a user