mirror of
https://github.com/clearml/clearml-web
synced 2025-06-26 18:27:02 +00:00
@@ -1,9 +1,19 @@
|
||||
import {Component, ElementRef, EventEmitter, HostListener, Input, OnDestroy, Output, ViewChild} from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {MatMenuTrigger} from '@angular/material/menu';
|
||||
import {TagsMenuComponent} from '../../ui-components/tags/tags-menu/tags-menu.component';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {deactivateEdit, activateEdit} from 'app/webapp-common/experiments/actions/common-experiments-info.actions';
|
||||
import {activateModelEdit, cancelModelEdit} from 'app/webapp-common/models/actions/models-info.actions';
|
||||
import {deactivateEdit, activateEdit} from '@common/experiments/actions/common-experiments-info.actions';
|
||||
import {activateModelEdit, cancelModelEdit} from '@common/models/actions/models-info.actions';
|
||||
import {CountAvailableAndIsDisableSelectedFiltered} from '@common/shared/entity-page/items.utils';
|
||||
import {MenuItems} from '../../entity-page/items.utils';
|
||||
import {Subscription} from 'rxjs';
|
||||
@@ -37,11 +47,14 @@ export class BaseContextMenuComponent implements OnDestroy{
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected store: Store,
|
||||
protected eRef: ElementRef
|
||||
) {
|
||||
this.sub.add(store.select(selectSelectedProjectId)
|
||||
protected store: Store;
|
||||
protected eRef: ElementRef;
|
||||
|
||||
constructor() {
|
||||
this.store = inject(Store);
|
||||
this.eRef = inject(ElementRef);
|
||||
|
||||
this.sub.add(this.store.select(selectSelectedProjectId)
|
||||
.subscribe(id => {
|
||||
this.projectId = id;
|
||||
this.allProjects = id === '*';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div [hidden]="!loading" [class.d-flex]="loading" class="overlay justify-content-center align-items-center">
|
||||
<div class="loading">
|
||||
<mat-spinner strokeWidth="2" diameter="24"></mat-spinner>
|
||||
<mat-spinner strokeWidth="2" diameter="24" color="accent"></mat-spinner>
|
||||
<div class="text">Loading</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -11,7 +11,5 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-100">
|
||||
<div (window:resize)="onResize()" #chart class="chart"></div>
|
||||
<div #legend></div>
|
||||
</div>
|
||||
<div (window:resize)="onResize()" #chart class="chart"></div>
|
||||
<div #legend></div>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
display: block;
|
||||
position: relative;
|
||||
background-color: $black;
|
||||
background-color: var(--line-chart-background-color, $black);
|
||||
|
||||
.chart {
|
||||
height: calc(100% - 40px);
|
||||
height: calc(100% - 46px);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
bottom: 0;
|
||||
left: 0 ;
|
||||
right: 0;
|
||||
background-color: $black;
|
||||
background-color: var(--line-chart-background-color, $black);
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -84,3 +84,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .legend-circle.circle-hidden {
|
||||
fill-opacity: 0.4;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,19 @@ import {
|
||||
Input,
|
||||
ChangeDetectionStrategy,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
ChangeDetectorRef,
|
||||
AfterViewInit,
|
||||
NgZone,
|
||||
NgZone, ElementRef, OnDestroy,
|
||||
} from '@angular/core';
|
||||
import {line, tooltip, legend} from 'britecharts';
|
||||
import {selectScaleFactor} from '@common/core/reducers/view.reducer';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {line, tooltip, legend, LegendModule} from 'britecharts';
|
||||
import {select, Selection} from 'd3-selection';
|
||||
import 'd3-transition';
|
||||
import {LineChartModule} from 'britecharts/src/typings/charts/line-chart';
|
||||
import {TimeSeriesChartAPI} from 'britecharts/src/typings/common/base';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
interface Topic {
|
||||
topicName: string;
|
||||
@@ -18,11 +23,18 @@ interface Topic {
|
||||
dates: { value: number; date: string }[];
|
||||
}
|
||||
|
||||
export interface LineChartData {
|
||||
dataByTopic?: Topic[];
|
||||
export interface LineChartFlatData {
|
||||
topicName: string;
|
||||
name: string;
|
||||
originalDate: number;
|
||||
date: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const COLOR_SCHEME = ['#a4a1fb', '#ff8a15'];
|
||||
export interface LineChartData {
|
||||
dataByTopic?: Topic[];
|
||||
data: LineChartFlatData[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector : 'sm-line-chart',
|
||||
@@ -30,22 +42,26 @@ const COLOR_SCHEME = ['#a4a1fb', '#ff8a15'];
|
||||
styleUrls : ['./line-chart.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LineChartComponent implements AfterViewInit {
|
||||
export class LineChartComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
private lineMargin = {top: 30, bottom: 50, left: 80, right: 10};
|
||||
private lineChartContainer: Selection<SVGElement, LineChartData, HTMLElement, any>;
|
||||
private lineChart;
|
||||
private lineChartContainer: Selection<Element, LineChartData, Element, LineChartFlatData>;
|
||||
private lineChart: LineChartModule;
|
||||
private lineTooltipContainer;
|
||||
private lineTooltip;
|
||||
private legendContainer: Selection<SVGElement, LineChartData, HTMLElement, any>;
|
||||
private legendChart;
|
||||
private legendContainer: Selection<Element, LineChartData, Element, {id: number; name: string}>;
|
||||
private legendChart: LegendModule;
|
||||
private legendWidth: number;
|
||||
public loading = false;
|
||||
private _yLabel: string;
|
||||
public chartData: LineChartData;
|
||||
private initDone = false;
|
||||
private hidden = {} as {[id: string]: boolean}
|
||||
private hiddenIndexes = {} as {[id: number]: boolean}
|
||||
|
||||
@Input() yTickFormatter: (number) => number;
|
||||
private sub = new Subscription();
|
||||
private scale: number;
|
||||
|
||||
@Input() set yLabel(label: string) {
|
||||
this._yLabel = label;
|
||||
@@ -70,7 +86,7 @@ export class LineChartComponent implements AfterViewInit {
|
||||
@Input() tooltipVerticalOffset = -100;
|
||||
|
||||
@Input() set data(data: LineChartData) {
|
||||
if (data && (data.dataByTopic.length === 0 || data.dataByTopic.every(topic => topic.dates.length === 0))) {
|
||||
if (data && !(data?.data?.length > 0) && (data.dataByTopic?.length === 0 || data.dataByTopic?.every(topic => topic.dates.length === 0))) {
|
||||
this.chartData = undefined;
|
||||
return;
|
||||
}
|
||||
@@ -83,37 +99,52 @@ export class LineChartComponent implements AfterViewInit {
|
||||
this.updateLegend();
|
||||
}
|
||||
}
|
||||
@Input() colorScheme = ['#a4a1fb', '#ff8a15'];
|
||||
|
||||
@ViewChild('chart', {read: ViewContainerRef, static: true}) chartRef: ViewContainerRef;
|
||||
@ViewChild('legend', {read: ViewContainerRef, static: true}) legendRef: ViewContainerRef;
|
||||
@ViewChild('chart') chartRef: ElementRef<HTMLDivElement>;
|
||||
@ViewChild('legend') legendRef: ElementRef<HTMLDivElement>;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef, private readonly zone: NgZone) {
|
||||
constructor(private cdr: ChangeDetectorRef, private readonly zone: NgZone, private store: Store) {
|
||||
this.sub.add(this.store.select(selectScaleFactor)
|
||||
.pipe(map(factor => 100 / factor))
|
||||
.subscribe(scale => this.scale = scale)
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.legendChart = legend();
|
||||
this.legendContainer = select(this.legendRef.element.nativeElement);
|
||||
this.lineChart = line();
|
||||
this.lineTooltip = tooltip();
|
||||
this.lineChartContainer = select(this.chartRef.element.nativeElement);
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.legendChart = legend();
|
||||
this.legendContainer = select(this.legendRef.nativeElement);
|
||||
this.lineChart = line();
|
||||
this.lineTooltip = tooltip();
|
||||
this.lineChartContainer = select(this.chartRef.nativeElement);
|
||||
});
|
||||
if (!this.loading) {
|
||||
setTimeout(this.initLineChart.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
initLegend() {
|
||||
const rect = this.legendContainer.node().getBoundingClientRect();
|
||||
const {width} = rect;
|
||||
this.legendWidth = width;
|
||||
this.zone.runOutsideAngular(() => {
|
||||
const rect = this.legendContainer.node().getBoundingClientRect();
|
||||
const {width} = rect;
|
||||
this.legendWidth = width;
|
||||
|
||||
if (rect) {
|
||||
this.legendChart
|
||||
.width(this.legendWidth)
|
||||
.height(40)
|
||||
.colorSchema(COLOR_SCHEME)
|
||||
.numberFormat('l')
|
||||
.isHorizontal(true);
|
||||
}
|
||||
if (rect) {
|
||||
this.legendChart
|
||||
.width(this.legendWidth)
|
||||
.height(40)
|
||||
.colorSchema(this.colorScheme)
|
||||
.numberFormat('l')
|
||||
.isHorizontal(true)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleSeries({id, name}: {id: number; name: string}) {
|
||||
this.hidden[name] = !this.hidden[name];
|
||||
this.hiddenIndexes[id] = !this.hiddenIndexes[id];
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
updateLegend() {
|
||||
@@ -122,10 +153,23 @@ export class LineChartComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
this.zone.runOutsideAngular(() => {
|
||||
const data = this.chartData.dataByTopic.map(topic => ({id: topic.topic, name: topic.topicName}));
|
||||
this.legendContainer.datum(data).call(this.legendChart);
|
||||
const data = this.chartData.dataByTopic ?
|
||||
this.chartData.dataByTopic.map(topic => ({id: topic.topic, name: topic.topicName})) :
|
||||
this.chartData.data.reduce((acc, d) => {
|
||||
!acc.find(entry => entry.name === d.topicName) && acc.push({name: d.topicName, id: acc.length});
|
||||
return acc;
|
||||
}, [])
|
||||
|
||||
this.legendContainer.datum(data).call(this.legendChart, []);
|
||||
|
||||
this.legendContainer.select('defs').remove();
|
||||
this.legendContainer.selectAll('.legend-entry')
|
||||
.style('cursor', 'pointer')
|
||||
.on('click', (event, data: {id: number; name: string}) => this.toggleSeries(data));
|
||||
|
||||
this.legendContainer.selectAll('.legend-circle')
|
||||
.classed('circle-hidden', (data: {name: string}) => this.hidden[data.name])
|
||||
|
||||
this.legendContainer.select('svg').append('defs').append('svg:clipPath')
|
||||
.attr('id', 'legend-label-clip')
|
||||
.append('svg:rect')
|
||||
@@ -137,30 +181,30 @@ export class LineChartComponent implements AfterViewInit {
|
||||
this.legendContainer.selectAll('.legend-entry-name')
|
||||
.attr('clip-path', 'url(#legend-label-clip)')
|
||||
.append('title')
|
||||
.text((d: any[]) => d['name']);
|
||||
.text((d) => d['name']);
|
||||
});
|
||||
}
|
||||
|
||||
initLineChart() {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
const containerWidth: number = this.lineChartContainer.node() ?
|
||||
this.lineChartContainer.node().getBoundingClientRect().width : 10;
|
||||
this.lineChartContainer.node().getBoundingClientRect().width / this.scale : 10;
|
||||
|
||||
this.lineChart
|
||||
.tooltipThreshold(600)
|
||||
.height(270)
|
||||
.height(this.chartRef.nativeElement.getBoundingClientRect().height)
|
||||
.margin(this.lineMargin)
|
||||
.lineCurve('monotoneX')
|
||||
.isAnimated(true)
|
||||
.grid('horizontal')
|
||||
.width(containerWidth)
|
||||
.colorSchema(COLOR_SCHEME)
|
||||
.lineGradient(['#a4a1fb', '#a4a1fb'])
|
||||
.colorSchema(this.colorScheme)
|
||||
.lineGradient([this.colorScheme[0], this.colorScheme[0]])
|
||||
.xAxisLabel('')
|
||||
.xTicks(8)
|
||||
.xTicks(2)
|
||||
.yAxisLabelPadding(50)
|
||||
.yAxisLabel(this._yLabel)
|
||||
.xAxisFormat(this.lineChart.axisTimeCombinations.CUSTOM)
|
||||
.xAxisFormat('custom' as unknown as TimeSeriesChartAPI<number>['axisTimeCombinations'])
|
||||
.on('customMouseOver', this.lineTooltip.show)
|
||||
.on('customMouseMove', this.lineTooltip.update)
|
||||
.on('customMouseOut', this.lineTooltip.hide);
|
||||
@@ -174,7 +218,7 @@ export class LineChartComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
// Tooltip Setup and start
|
||||
const tooltipElm = this.chartRef.element.nativeElement;
|
||||
const tooltipElm = this.chartRef.nativeElement;
|
||||
this.lineTooltipContainer = select(tooltipElm).select('.metadata-group .vertical-marker-container');
|
||||
this.lineTooltip
|
||||
.title('')
|
||||
@@ -199,7 +243,16 @@ export class LineChartComponent implements AfterViewInit {
|
||||
updateChart() {
|
||||
if (this.chartData) {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.lineChartContainer.datum(this.chartData).call(this.lineChart);
|
||||
const firstColorNotHidden = this.colorScheme.find( (color, i) => !this.hiddenIndexes[i]);
|
||||
this.lineChartContainer.selectAll('#one-line-gradient-1 stop').attr('stop-color', firstColorNotHidden);
|
||||
const chartData = {
|
||||
...this.chartData,
|
||||
data: this.chartData?.data?.filter(d => !this.hidden?.[d.topicName]),
|
||||
dataByTopic: this.chartData?.dataByTopic?.filter(t => !this.hidden?.[t.topicName])
|
||||
} as LineChartData;
|
||||
this.lineChartContainer.selectAll('.chart-group .topic').remove();
|
||||
this.lineChartContainer.datum(chartData).call(this.lineChart, undefined);
|
||||
this.lineChartContainer.selectAll('.chart-group .topic path').attr('stroke', (base: Topic)=> this.colorScheme[base.topic - 1]);
|
||||
if (this.yTickFormatter) {
|
||||
this.lineChartContainer.selectAll('.y.axis .tick text').text(this.yTickFormatter);
|
||||
}
|
||||
@@ -207,13 +260,13 @@ export class LineChartComponent implements AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
getXAxisFormatter(data) {
|
||||
getXAxisFormatter(data: LineChartData) {
|
||||
if (!data) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
data = data.dataByTopic[0].dates;
|
||||
const firstDate = new Date(data[0].date);
|
||||
const lastDate = new Date(data[data.length - 1].date);
|
||||
const dates = data.dataByTopic?.[0].dates.map(d => d.date) ?? data.data.map(d => d.originalDate);
|
||||
const firstDate = new Date(dates[0]);
|
||||
const lastDate = new Date(dates.at(-1));
|
||||
const dateTimeSpan = lastDate.getTime() - firstDate.getTime();
|
||||
|
||||
if (dateTimeSpan > 3 * 24 * 60 * 60 * 1000) { // more then 3 days
|
||||
@@ -236,4 +289,8 @@ export class LineChartComponent implements AfterViewInit {
|
||||
this.updateChart();
|
||||
this.updateLegend();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div [hidden]="!loading" [class.d-flex]="loading" class="overlay justify-content-center align-items-center">
|
||||
<div class="loading">
|
||||
<mat-spinner strokeWidth="2" diameter="24"></mat-spinner>
|
||||
<mat-spinner strokeWidth="2" diameter="24" color="accent"></mat-spinner>
|
||||
<div class="text">Loading</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
(itemClicked)="selectedTableColsChanged.emit(col)">
|
||||
</sm-menu-item>
|
||||
</div>
|
||||
<div class="loader" [ngClass]="{'d-none': !isLoading}">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
<div class="p-4 pe-none" [ngClass]="{'d-none': !isLoading}">
|
||||
<mat-spinner class="m-auto" [diameter]="32" [strokeWidth]="4" color="accent"></mat-spinner>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="auto-refresh p-2">
|
||||
<mat-slide-toggle
|
||||
(change)="onAutoRefreshChange($event)"
|
||||
[disabled]="!allowAutorefresh"
|
||||
[checked]="(allowAutorefresh === false) ? false : autoRefreshValue">Auto Refresh
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<div class="p2">
|
||||
<button class="btn btn-secondary" smTooltip="Refresh" [disabled]="disabled"
|
||||
(click)="onRefreshLogClicked.emit({isAutoRefresh: false})">
|
||||
<i class="fas fa-sync"></i>
|
||||
</button>
|
||||
<button *ngIf="showSettings" class="btn" smTooltip="Settings" (click)="toggleSettings.emit()">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +0,0 @@
|
||||
:host{
|
||||
&{
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ExperimentRefreshComponent } from './experiment-refresh.component';
|
||||
import {StoreModule} from '@ngrx/store';
|
||||
|
||||
describe('ExperimentRefreshComponent', () => {
|
||||
let component: ExperimentRefreshComponent;
|
||||
let fixture: ComponentFixture<ExperimentRefreshComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ExperimentRefreshComponent ],
|
||||
imports: [StoreModule.forRoot({})]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ExperimentRefreshComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
|
||||
import {filter} from 'rxjs/operators';
|
||||
import {Subscription, interval} from 'rxjs';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {setCompareAutoRefresh} from '@common/core/actions/layout.actions';
|
||||
import {selectCompareAutoRefresh} from '@common/core/reducers/view.reducer';
|
||||
|
||||
@Component({
|
||||
selector : 'sm-experiment-refresh',
|
||||
templateUrl: './experiment-refresh.component.html',
|
||||
styleUrls : ['./experiment-refresh.component.scss']
|
||||
})
|
||||
export class ExperimentRefreshComponent implements OnInit, OnDestroy {
|
||||
|
||||
public autoRefreshValue = false;
|
||||
private autoRefreshSubscription: Subscription;
|
||||
private autoRefreshValueSub: Subscription;
|
||||
readonly refreshInterval: number = 30000;
|
||||
@Input() disabled = false;
|
||||
@Input() showSettings = false;
|
||||
@Input() allowAutorefresh: boolean = true;
|
||||
@Output() onRefreshLogClicked = new EventEmitter<{isAutoRefresh: boolean}>();
|
||||
@Output() toggleSettings = new EventEmitter();
|
||||
|
||||
constructor(private store: Store) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.autoRefreshValueSub = this.store.select(selectCompareAutoRefresh)
|
||||
.subscribe(val => this.autoRefreshValue = val);
|
||||
|
||||
this.autoRefreshSubscription = interval(this.refreshInterval)
|
||||
.pipe(
|
||||
filter(() => this.autoRefreshValue && this.allowAutorefresh)
|
||||
)
|
||||
.subscribe(() => this.onRefreshLogClicked.emit({isAutoRefresh: true}));
|
||||
}
|
||||
|
||||
onAutoRefreshChange(event) {
|
||||
this.store.dispatch(setCompareAutoRefresh({autoRefresh: event.checked}));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.autoRefreshSubscription.unsubscribe();
|
||||
this.autoRefreshValueSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
:host {
|
||||
height: 16px;
|
||||
user-select: none;
|
||||
display: inline-flex;
|
||||
|
||||
.id-number {
|
||||
color: $blue-250;
|
||||
|
||||
@@ -85,7 +85,7 @@ export class JsonViewerComponent {
|
||||
|
||||
private _search: string = null;
|
||||
public isArray: boolean;
|
||||
public index: number = 0;
|
||||
public index = 0;
|
||||
public stringify = JSON.stringify;
|
||||
// nestedNodeMap = new Map<any, TreeFlatNode>();
|
||||
// flatNodeMap = new Map<TreeFlatNode, Segment>();
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
</mat-menu>
|
||||
<hr/>
|
||||
<button mat-menu-item
|
||||
[disabled]="!((showOnlyUserWork$ | async) || ((tagsFilters$ | async)?.length > 0))"
|
||||
smClickStopPropagation
|
||||
class="user-filter-button"
|
||||
(click)="clearAll()">
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
height="100%"
|
||||
[mode]="editMode ? 'editor' : 'preview'"
|
||||
[options]="options"
|
||||
[postRender]="postRender"
|
||||
[upload]="handleUpload"
|
||||
(onEditorLoaded)="editorReady($event)"
|
||||
(onPreviewDomChanged)="domFixes()"
|
||||
|
||||
@@ -6,10 +6,8 @@ import {
|
||||
Input,
|
||||
Output,
|
||||
Renderer2,
|
||||
SecurityContext,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import {MarkdownEditorComponent as MDComponent, MdEditorOption, UploadResult} from 'ngx-markdown-editor';
|
||||
import {Ace} from 'ace-builds';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
@@ -17,6 +15,8 @@ import {
|
||||
MarkdownCheatSheetDialogComponent
|
||||
} from '@common/shared/components/markdown-editor/markdown-cheat-sheet-dialog/markdown-cheat-sheet-dialog.component';
|
||||
import {getBaseName} from '@common/shared/utils/shared-utils';
|
||||
import * as marked from 'marked';
|
||||
import * as DOMPurify from 'dompurify';
|
||||
|
||||
const BREAK_POINT = 990;
|
||||
|
||||
@@ -35,9 +35,12 @@ export class MarkdownEditorComponent {
|
||||
public editorVisible: boolean;
|
||||
private _editMode: boolean;
|
||||
public options = {
|
||||
markedjsOpt: {
|
||||
sanitizer: this.sanitizer.sanitize.bind(SecurityContext.NONE)
|
||||
},
|
||||
// markedjsOpt: {
|
||||
// sanitizer: (dirty: string): string => {
|
||||
// debugger
|
||||
// return DOMPurify.sanitize(dirty, { ADD_TAGS: ["iframe"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling'] })
|
||||
// }
|
||||
// },
|
||||
enablePreviewContentClick: true,
|
||||
fontAwesomeVersion: '6',
|
||||
showPreviewPanel: true,
|
||||
@@ -48,6 +51,11 @@ export class MarkdownEditorComponent {
|
||||
public ace: Ace.Editor;
|
||||
public isExpand: boolean = false;
|
||||
public duplicateNames: boolean;
|
||||
public postRender = (dirty: string): string => {
|
||||
return DOMPurify.sanitize(dirty, { ADD_TAGS: ["iframe"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling'] })
|
||||
};
|
||||
|
||||
|
||||
trackByUrl = (index: number, resource) => resource.url;
|
||||
|
||||
set editMode(editMode: boolean) {
|
||||
@@ -67,11 +75,10 @@ export class MarkdownEditorComponent {
|
||||
@Input() resources = [] as {unused: boolean; url: string}[];
|
||||
@Output() saveInfo = new EventEmitter<string>();
|
||||
@Output() editModeChanged = new EventEmitter();
|
||||
@Output() dirtyChanged = new EventEmitter<boolean>();
|
||||
@Output() deleteResource = new EventEmitter<string>();
|
||||
@Output() imageMenuOpened = new EventEmitter<string>();
|
||||
@ViewChild(MDComponent) editorComponent: MDComponent;
|
||||
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
updateEditorVisibility() {
|
||||
if (!this.ready) {
|
||||
@@ -97,10 +104,10 @@ export class MarkdownEditorComponent {
|
||||
|
||||
constructor(
|
||||
private renderer: Renderer2,
|
||||
protected sanitizer: DomSanitizer,
|
||||
protected cdr: ChangeDetectorRef,
|
||||
protected dialog: MatDialog,
|
||||
) {
|
||||
window['marked'] = marked.marked;
|
||||
this.editMode = false;
|
||||
}
|
||||
|
||||
@@ -146,7 +153,9 @@ export class MarkdownEditorComponent {
|
||||
}
|
||||
|
||||
checkDirty() {
|
||||
this.isDirty = this.originalInfo !== this.data;
|
||||
const isDirty = this.originalInfo !== this.data;
|
||||
isDirty !== this.isDirty && this.dirtyChanged.emit(isDirty);
|
||||
this.isDirty = isDirty;
|
||||
this.getDuplicateIframes();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
:host {
|
||||
vertical-align: middle;
|
||||
|
||||
.al-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-container {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<div class="refresh-wrapper d-flex flex-row-reverse align-items-center" [ngClass]="{'allow-autorefresh': allowAutoRefresh}">
|
||||
<div class="refresh-wrapper d-flex flex-row-reverse align-items-center pointer" [ngClass]="{'allow-autorefresh': allowAutoRefresh}">
|
||||
<span
|
||||
*ngIf="allowAutoRefresh && autoRefreshState$ | async; else pause"
|
||||
class="ps-1 al-icon al-ico-auto-refresh-play"
|
||||
class="al-icon al-ico-auto-refresh-play pointer"
|
||||
data-id="Auto Refresh"
|
||||
(click)="refresh.trigger()"
|
||||
><span class="path1"></span><span class="path2"></span></span>
|
||||
<ng-template #pause>
|
||||
<span
|
||||
class="ps-1 al-icon al-ico-auto-refresh-pause"
|
||||
class="al-icon al-ico-auto-refresh-pause"
|
||||
(click)="refresh.trigger()"
|
||||
><span class="path1"></span><span class="path2"></span>
|
||||
</span>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
}
|
||||
|
||||
.refresh-wrapper {
|
||||
width: 28px;
|
||||
padding: 0 5px;
|
||||
width: 24px;
|
||||
padding-left: 5px;
|
||||
overflow: hidden;
|
||||
transition: all 0.5s 0.5s, background 0.5s 0s;
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
@import 'variables';
|
||||
|
||||
nav {
|
||||
--mdc-typography-button-font-size: 12px;
|
||||
--mat-tab-header-label-text-size: 12px;
|
||||
--mat-tab-header-label-text-letter-spacing: 0.03em;
|
||||
--mat-tab-header-inactive-hover-label-text-color: #{$purple};
|
||||
&.mat-mdc-tab-header-pagination-controls-enabled {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -26,4 +26,4 @@
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</div>
|
||||
<span *ngIf="!showSpinner && !lines?.length && !editable" class="no-changes">{{emptyMessage}}</span>
|
||||
<mat-spinner class="mx-auto mt-3" [diameter]="50" *ngIf="showSpinner"></mat-spinner>
|
||||
<mat-spinner *ngIf="showSpinner" class="mx-auto mt-3" [diameter]="32" [strokeWidth]="4" color="accent"></mat-spinner>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
<ng-template #noData>
|
||||
<div class="d-flex h-100 flex-1">
|
||||
<mat-spinner *ngIf="!metadataKeys" class="spinner" diameter="80" strokeWidth="8"></mat-spinner>
|
||||
<mat-spinner *ngIf="!metadataKeys" class="spinner" [diameter]="32" [strokeWidth]="4" color="accent"></mat-spinner>
|
||||
<div *ngIf="metadataKeys && metadataKeys.length === 0" class="empty-state">No data to show</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<sm-menu
|
||||
panelClasses="light-theme"
|
||||
[header]="(showOnlyUserWork$ | async) ? 'My Work': 'Team\`s work'"
|
||||
[header]="(showOnlyUserWork$ | async) ? 'My Work': 'Team\'s Work'"
|
||||
[prefixIconClass]="'me-1 al-icon ' + ((showOnlyUserWork$ | async)? 'al-ico-me': 'al-ico-team')"
|
||||
class="menu" data-id="Team's Work">
|
||||
<sm-menu-item itemLabel="My Work" (itemClicked)="userFilterChanged(true)" iconClass="al-icon al-ico-me" data-id="My Work Option"></sm-menu-item>
|
||||
<sm-menu-item itemLabel="Team`s work" (itemClicked)="userFilterChanged(false)"
|
||||
<sm-menu-item itemLabel="Team's Work" (itemClicked)="userFilterChanged(false)"
|
||||
iconClass="al-icon al-ico-team" data-id="Team's Work Option"></sm-menu-item>
|
||||
</sm-menu>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[class.snippets-mode]="snippetsMode"
|
||||
[style.--cardWidth]="cardWidth + 'px'"
|
||||
[style.--gridGap]="gridGap + 'px'"
|
||||
[style.--padding]="snippetsMode ? padding + 'px' : null"
|
||||
*cdkVirtualFor="let row of itemRows$ | async; let rowIndex = index"
|
||||
>
|
||||
<ng-container *ngFor="let item of row; let itemIndex = index; trackBy: trackByFn">
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
&.snippets-mode {
|
||||
grid-template-columns: repeat(auto-fit, minmax(var(--cardWidth), 1fr));
|
||||
padding: 0 var(--padding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
<div class="image" *ngSwitchCase="'image'" data-id="imageContainer">
|
||||
<img
|
||||
#imageElement
|
||||
class="pointer"
|
||||
data-id="sourceImage"
|
||||
[src]="source"
|
||||
[alt]="source"
|
||||
(error)="isFailed = true"
|
||||
(click)="imageClicked.emit({src: source})"
|
||||
(load)="loadedMedia()"
|
||||
@@ -15,7 +17,7 @@
|
||||
<div *ngIf="!noHoverEffects" class="toolbar top">
|
||||
<div class="clickable-icon d-flex align-items-center justify-content-center pointer"
|
||||
(click)="createEmbedCodeClicked($event)"
|
||||
smTooltip="Copy to Report"
|
||||
smTooltip="Copy embed code"
|
||||
data-id="copyToReportButton">
|
||||
<i class="al-icon al-ico-code sm"></i>
|
||||
</div>
|
||||
@@ -46,7 +48,7 @@
|
||||
<div class="toolbar top">
|
||||
<div *ngIf="!noHoverEffects" class="clickable-icon d-flex align-items-center justify-content-center pointer"
|
||||
(click)="createEmbedCodeClicked($event)"
|
||||
smTooltip="Copy to Report" data-id="CopyToReportButton">
|
||||
smTooltip="Copy embed code" data-id="CopyToReportButton">
|
||||
<i class="al-icon al-ico-code sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,7 +61,7 @@
|
||||
<div *ngIf="!noHoverEffects" class="toolbar">
|
||||
<div class="clickable-icon d-flex align-items-center justify-content-center pointer"
|
||||
(click)="createEmbedCodeClicked($event)"
|
||||
smTooltip="Copy to Report" data-id="CopyToReportButton">
|
||||
smTooltip="Copy embed code" data-id="CopyToReportButton">
|
||||
<i class="al-icon al-ico-code sm"></i>
|
||||
</div>
|
||||
<div class="clickable-icon d-flex align-items-center justify-content-center pointer"
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||
import {Observable} from 'rxjs/internal/Observable';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {Observable} from 'rxjs';
|
||||
import {tap} from 'rxjs/operators';
|
||||
import {IsAudioPipe} from '../../pipes/is-audio.pipe';
|
||||
import {IsVideoPipe} from '../../pipes/is-video.pipe';
|
||||
@@ -10,12 +20,14 @@ import {isHtmlPage} from '@common/shared/utils/is-html-page';
|
||||
import {isTextFileURL} from '@common/shared/utils/is-text-file';
|
||||
import {getSignedUrlOrOrigin$} from '@common/core/reducers/common-auth-reducer';
|
||||
|
||||
// import {Event} from '@common/debug-images/debug-images-types';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-debug-image-snippet',
|
||||
templateUrl: './debug-image-snippet.component.html',
|
||||
styleUrls: ['./debug-image-snippet.component.scss']
|
||||
})
|
||||
export class DebugImageSnippetComponent {
|
||||
export class DebugImageSnippetComponent implements OnDestroy{
|
||||
public type: 'image' | 'player' | 'html';
|
||||
public source$: Observable<string>;
|
||||
private _frame: any;
|
||||
@@ -49,6 +61,7 @@ export class DebugImageSnippetComponent {
|
||||
@Output() imageClicked = new EventEmitter<{src: string}>();
|
||||
@Output() createEmbedCode = new EventEmitter();
|
||||
@ViewChild('video') video: ElementRef<HTMLVideoElement>;
|
||||
@ViewChildren('imageElement') imageElements: QueryList<ElementRef<HTMLImageElement>>
|
||||
|
||||
isFailed = false;
|
||||
isLoading = true;
|
||||
@@ -74,10 +87,6 @@ export class DebugImageSnippetComponent {
|
||||
));
|
||||
}
|
||||
|
||||
log($event: ErrorEvent) {
|
||||
console.log($event);
|
||||
}
|
||||
|
||||
iframeLoaded(event) {
|
||||
if (event.target.src) {
|
||||
this.isLoading = false;
|
||||
@@ -87,4 +96,11 @@ export class DebugImageSnippetComponent {
|
||||
createEmbedCodeClicked($event: MouseEvent) {
|
||||
this.createEmbedCode.emit({x: $event.clientX, y: $event.clientY});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.imageElements.forEach(imageRef => imageRef.nativeElement.src = '');
|
||||
if (this.video?.nativeElement) {
|
||||
this.video.nativeElement.src = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
}
|
||||
|
||||
label {
|
||||
color: $blue-grey;
|
||||
color: $blue-300;
|
||||
padding-right: 4px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ export class ImageViewerComponent extends BaseImageViewerComponent implements On
|
||||
private autoRefreshState$: Observable<boolean>;
|
||||
private isAppVisible$: Observable<boolean>;
|
||||
private autoRefreshSub: Subscription;
|
||||
private beginningOfTime: boolean = false;
|
||||
private endOfTime: boolean = false;
|
||||
private beginningOfTime = false;
|
||||
private endOfTime = false;
|
||||
private begOfTimeSub: Subscription;
|
||||
private endOfTimeSub: Subscription;
|
||||
change$: BehaviorSubject<number>;
|
||||
@@ -55,7 +55,7 @@ export class ImageViewerComponent extends BaseImageViewerComponent implements On
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: {
|
||||
@Inject(MAT_DIALOG_DATA) public override data: {
|
||||
index: number;
|
||||
isAllMetrics: boolean;
|
||||
snippetsMetaData: Array<{task: string; metric: string; variant: string; iter: number}>;
|
||||
@@ -63,9 +63,9 @@ export class ImageViewerComponent extends BaseImageViewerComponent implements On
|
||||
withoutNavigation: boolean;
|
||||
embedFunction: () => null;
|
||||
},
|
||||
public dialogRef: MatDialogRef<ImageViewerComponent>,
|
||||
public changeDetector: ChangeDetectorRef,
|
||||
public store: Store<any>
|
||||
public override dialogRef: MatDialogRef<ImageViewerComponent>,
|
||||
public override changeDetector: ChangeDetectorRef,
|
||||
public override store: Store<any>
|
||||
) {
|
||||
super(data, dialogRef, changeDetector, store);
|
||||
if(data.url) {
|
||||
@@ -171,7 +171,7 @@ export class ImageViewerComponent extends BaseImageViewerComponent implements On
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
override ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.begOfTimeSub = this.beginningOfTime$.subscribe(beg => {
|
||||
this.beginningOfTime = beg;
|
||||
@@ -187,7 +187,7 @@ export class ImageViewerComponent extends BaseImageViewerComponent implements On
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
override ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
this.store.dispatch(setDebugImageViewerScrollId({scrollId: null}));
|
||||
this.begOfTimeSub.unsubscribe();
|
||||
@@ -196,10 +196,6 @@ export class ImageViewerComponent extends BaseImageViewerComponent implements On
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
|
||||
showImage() {
|
||||
this.imageLoaded = true;
|
||||
}
|
||||
|
||||
embedCodeClick($event: MouseEvent) {
|
||||
$event && this.embedFunction(($event?.currentTarget as HTMLElement).getBoundingClientRect(), this.currentDebugImage?.metric, this.currentDebugImage?.variant);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {AfterViewInit, Component, inject, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {ActionCreator, Store} from '@ngrx/store';
|
||||
import {combineLatest, Observable, of, Subject, Subscription, switchMap} from 'rxjs';
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ import {SplitComponent} from 'angular-split';
|
||||
import {selectRouterParams} from '../../core/reducers/router-reducer';
|
||||
import {ITableExperiment} from '../../experiments/shared/common-experiment-model.model';
|
||||
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
|
||||
import {IFooterState} from './footer-items/footer-items.models';
|
||||
import {IFooterState, ItemFooterModel} from './footer-items/footer-items.models';
|
||||
import {
|
||||
CountAvailableAndIsDisableSelectedFiltered,
|
||||
selectionAllHasExample,
|
||||
@@ -46,7 +46,8 @@ import {selectNeverShowPopups} from '../../core/reducers/view.reducer';
|
||||
import {isReadOnly} from '@common/shared/utils/is-read-only';
|
||||
import {setCustomMetrics} from '@common/models/actions/models-view.actions';
|
||||
import * as experimentsActions from '@common/experiments/actions/common-experiments-view.actions';
|
||||
import {setParents} from '@common/experiments/actions/common-experiments-view.actions';
|
||||
import {hyperParamSelectedExperiments, hyperParamSelectedInfoExperiments, setHyperParamsFiltersPage, setParents} from '@common/experiments/actions/common-experiments-view.actions';
|
||||
import {IExperimentInfo} from '~/features/experiments/shared/experiment-info.model';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-base-entity-page',
|
||||
@@ -54,19 +55,19 @@ import {setParents} from '@common/experiments/actions/common-experiments-view.ac
|
||||
})
|
||||
export abstract class BaseEntityPageComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
public selectedProject$: Observable<Project>;
|
||||
protected setSplitSizeAction: any;
|
||||
protected setSplitSizeAction: ActionCreator<string, any>;
|
||||
protected addTag: ActionCreator<string, any>;
|
||||
protected abstract setTableModeAction: ActionCreator<string, any>;
|
||||
public shouldOpenDetails = false;
|
||||
protected sub = new Subscription();
|
||||
public selectedExperiments: ITableExperiment[];
|
||||
public selectedExperiments: IExperimentInfo[];
|
||||
public projectId: string;
|
||||
public isExampleProject: boolean;
|
||||
public selectSplitSize$?: Observable<number>;
|
||||
public infoDisabled: boolean;
|
||||
public splitInitialSize: number;
|
||||
public minimizedView: boolean;
|
||||
public footerItems = [];
|
||||
public footerItems = [] as ItemFooterModel[];
|
||||
public footerState$: Observable<IFooterState<any>>;
|
||||
public tableModeAwareness$: Observable<boolean>;
|
||||
private tableModeAwareness: boolean;
|
||||
@@ -78,7 +79,7 @@ export abstract class BaseEntityPageComponent implements OnInit, AfterViewInit,
|
||||
@ViewChild('split') split: SplitComponent;
|
||||
protected abstract inEditMode$: Observable<boolean>;
|
||||
public selectedProject: Project;
|
||||
private currentSelection: any[];
|
||||
private currentSelection: {id: string}[];
|
||||
public showAllSelectedIsActive$: Observable<boolean>;
|
||||
private allProjects: boolean;
|
||||
|
||||
@@ -97,13 +98,19 @@ export abstract class BaseEntityPageComponent implements OnInit, AfterViewInit,
|
||||
return this.route.parent.snapshot.params.projectId;
|
||||
}
|
||||
|
||||
protected constructor(
|
||||
protected store: Store,
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected dialog: MatDialog,
|
||||
protected refresh: RefreshService,
|
||||
) {
|
||||
protected store: Store;
|
||||
protected route: ActivatedRoute;
|
||||
protected router: Router;
|
||||
protected dialog: MatDialog;
|
||||
protected refresh: RefreshService;
|
||||
|
||||
protected constructor() {
|
||||
this.store = inject( Store);
|
||||
this.route = inject(ActivatedRoute);
|
||||
this.router = inject(Router);
|
||||
this.dialog = inject(MatDialog);
|
||||
this.refresh = inject(RefreshService);
|
||||
|
||||
this.users$ = this.store.select(selectSelectedProjectUsers);
|
||||
this.sub.add(this.store.select(selectSelectedProject).pipe(filter(p => !!p)).subscribe((project: Project) => {
|
||||
this.selectedProject = project;
|
||||
@@ -112,7 +119,7 @@ export abstract class BaseEntityPageComponent implements OnInit, AfterViewInit,
|
||||
}));
|
||||
this.projectsOptions$ = this.store.select(selectTablesFilterProjectsOptions);
|
||||
|
||||
this.tableModeAwareness$ = store.select(selectTableModeAwareness)
|
||||
this.tableModeAwareness$ = this.store.select(selectTableModeAwareness)
|
||||
.pipe(
|
||||
filter(featuresAwareness => featuresAwareness !== null && featuresAwareness !== undefined),
|
||||
tap(aware => this.tableModeAwareness = aware)
|
||||
@@ -202,7 +209,7 @@ export abstract class BaseEntityPageComponent implements OnInit, AfterViewInit,
|
||||
|
||||
createFooterItems(config: {
|
||||
entitiesType: EntityTypeEnum;
|
||||
selected$: Observable<Array<any>>;
|
||||
selected$: Observable<{id: string}[]>;
|
||||
showAllSelectedIsActive$: Observable<boolean>;
|
||||
data$?: Observable<Record<string, CountAvailableAndIsDisableSelectedFiltered>>;
|
||||
tags$?: Observable<string[]>;
|
||||
@@ -220,8 +227,8 @@ export abstract class BaseEntityPageComponent implements OnInit, AfterViewInit,
|
||||
);
|
||||
}
|
||||
|
||||
createFooterState<T = any>(
|
||||
selected$: Observable<Array<any>>,
|
||||
createFooterState<T extends {id: string}>(
|
||||
selected$: Observable<T[]>,
|
||||
data$?: Observable<Record<string, CountAvailableAndIsDisableSelectedFiltered>>,
|
||||
showAllSelectedIsActive$?: Observable<boolean>,
|
||||
companyTags$?: Observable<string[]>,
|
||||
@@ -305,14 +312,6 @@ export abstract class BaseEntityPageComponent implements OnInit, AfterViewInit,
|
||||
});
|
||||
}
|
||||
|
||||
updateUrl(queryParams: Params) {
|
||||
return this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParamsHandling: 'merge',
|
||||
queryParams
|
||||
});
|
||||
}
|
||||
|
||||
filterSearchChanged({colId, value}: { colId: string; value: { value: string; loadMore?: boolean } }) {
|
||||
switch (colId) {
|
||||
case 'project.name':
|
||||
@@ -325,7 +324,7 @@ export abstract class BaseEntityPageComponent implements OnInit, AfterViewInit,
|
||||
} else {
|
||||
this.store.dispatch(setTablesFilterProjectsOptions({
|
||||
projects: this.selectedProject ? [this.selectedProject,
|
||||
...this.selectedProject?.sub_projects] : [], scrollId: null
|
||||
...(this.selectedProject?.sub_projects ?? [])] : [], scrollId: null
|
||||
}));
|
||||
}
|
||||
break;
|
||||
@@ -338,9 +337,15 @@ export abstract class BaseEntityPageComponent implements OnInit, AfterViewInit,
|
||||
this.store.dispatch(experimentsActions.getParents({searchValue: value.value}));
|
||||
}
|
||||
}
|
||||
if (colId.startsWith('hyperparams.')) {
|
||||
if (!value.loadMore) {
|
||||
this.store.dispatch(hyperParamSelectedInfoExperiments({col: {id: colId}, loadMore: false, values: null}));
|
||||
this.store.dispatch(setHyperParamsFiltersPage({page: 0}));
|
||||
}
|
||||
this.store.dispatch(hyperParamSelectedExperiments({col: {id: colId, getter: `${colId}.value`}, searchValue: value.value}));
|
||||
}
|
||||
}
|
||||
|
||||
public setupBreadcrumbsOptions() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import {ConfirmDialogComponent} from '@common/shared/ui-components/overlay/confi
|
||||
import {ConfirmDialogConfig} from '@common/shared/ui-components/overlay/confirm-dialog/confirm-dialog.model';
|
||||
import {ErrorService} from '@common/shared/services/error.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {Task} from '~/business-logic/model/tasks/task';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteDialogEffectsBase {
|
||||
@@ -141,6 +142,7 @@ export class DeleteDialogEffectsBase {
|
||||
case EntityTypeEnum.project:
|
||||
return selectProjectForDelete;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pauseAutorefresh(entityType: EntityTypeEnum): Action[] {
|
||||
|
||||
@@ -140,7 +140,7 @@ export class CommonDeleteDialogComponent implements OnInit, OnDestroy {
|
||||
this.isOpenEntities = !this.isOpenEntities;
|
||||
}
|
||||
|
||||
getMessageByEntity(entityType: EntityTypeEnum, stats?: CommonReadyForDeletion): string {
|
||||
getMessageByEntity(entityType: EntityTypeEnum, stats?: CommonReadyForDeletion) {
|
||||
switch (entityType as any) {
|
||||
case EntityTypeEnum.controller:
|
||||
case EntityTypeEnum.experiment:
|
||||
@@ -151,10 +151,12 @@ export class CommonDeleteDialogComponent implements OnInit, OnDestroy {
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const entitiesBreakDown = getDeleteProjectPopupStatsBreakdown(stats, 'total', 'experiment');
|
||||
return entitiesBreakDown.trim().length > 0 ? `${entitiesBreakDown} will be deleted, including their artifacts. This may take a few minutes.` : '';
|
||||
case EntityTypeEnum.simpleDataset:
|
||||
case EntityTypeEnum.simpleDataset: {
|
||||
const entitiesBreakDown2 = getDeleteProjectPopupStatsBreakdown(stats, 'total', `version`);
|
||||
const single = Object.values(stats).reduce((a, b) => a + (b.total || 0), 0) == 1;
|
||||
return entitiesBreakDown2.trim().length > 0 ? `${entitiesBreakDown2} will be deleted and ${single ? 'its' : 'their'} data. This may take a few minutes.` : '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,5 @@ export class EntityFooterComponent extends BaseContextMenuComponent {
|
||||
|
||||
icons = ICONS;
|
||||
trackBy = trackByIndex;
|
||||
constructor(store: Store, eRef: ElementRef) {
|
||||
super(store, eRef);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import {IconNames, ICONS} from '../../../constants';
|
||||
import {MenuItems, selectionDisabledAbortAllChildren} from '../items.utils';
|
||||
|
||||
export class AbortAllChildrenFooterItem extends ItemFooterModel {
|
||||
id = MenuItems.abortAllChildren;
|
||||
emit = true;
|
||||
icon = ICONS.STOPPED_ALL as Partial<IconNames>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MenuItems.abortAllChildren;
|
||||
this.emit = true;
|
||||
this.icon = ICONS.STOPPED_ALL as Partial<IconNames>;
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
|
||||
@@ -4,12 +4,12 @@ import {MenuItems, selectionDisabledAbort} from '../items.utils';
|
||||
import {EntityTypeEnum} from '~/shared/constants/non-common-consts';
|
||||
|
||||
export class AbortFooterItem extends ItemFooterModel {
|
||||
id = MenuItems.abort;
|
||||
emit = true;
|
||||
icon = ICONS.STOPPED as Partial<IconNames>;
|
||||
|
||||
constructor(public entitiesType: EntityTypeEnum) {
|
||||
super();
|
||||
this.id = MenuItems.abort;
|
||||
this.emit = true;
|
||||
this.icon = ICONS.STOPPED as Partial<IconNames>;
|
||||
}
|
||||
getItemState(state: IFooterState<any>) {
|
||||
const {available, disable} = selectionDisabledAbort(state.selected);
|
||||
|
||||
@@ -4,10 +4,10 @@ import {ItemFooterModel, IFooterState} from './footer-items.models';
|
||||
import {MenuItems} from '../items.utils';
|
||||
|
||||
export class ArchiveFooterItem extends ItemFooterModel {
|
||||
id = MenuItems.archive;
|
||||
|
||||
constructor(public entitiesType: EntityTypeEnum) {
|
||||
super();
|
||||
this.id = MenuItems.archive;
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
|
||||
@@ -4,15 +4,15 @@ import {MenuItems} from '../items.utils';
|
||||
import { ItemFooterModel} from './footer-items.models';
|
||||
export const compareLimitations = 100;
|
||||
export class CompareFooterItem extends ItemFooterModel {
|
||||
id = MenuItems.compare;
|
||||
icon = ICONS.COMPARE as Partial<IconNames>;
|
||||
class = 'compare';
|
||||
title = 'COMPARE';
|
||||
emit = true;
|
||||
disableDescription = `${compareLimitations} or fewer ${this.entitiesType}s can be compared`;
|
||||
|
||||
constructor(public entitiesType: EntityTypeEnum) {
|
||||
super();
|
||||
this.id = MenuItems.compare;
|
||||
this.icon = ICONS.COMPARE as Partial<IconNames>;
|
||||
this.class = 'compare';
|
||||
this.title = 'COMPARE';
|
||||
this.emit = true;
|
||||
this.disableDescription = `${compareLimitations} or fewer ${this.entitiesType}s can be compared`;
|
||||
}
|
||||
getItemState(state): any {
|
||||
return {
|
||||
|
||||
@@ -3,13 +3,13 @@ import {IconNames, ICONS} from '../../../constants';
|
||||
import {MenuItems} from '../items.utils';
|
||||
|
||||
export class DeleteFooterItem extends ItemFooterModel {
|
||||
id = MenuItems.delete;
|
||||
emit = true;
|
||||
icon = ICONS.REMOVE as Partial<IconNames>;
|
||||
disableDescription = 'Delete';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MenuItems.delete;
|
||||
this.emit = true;
|
||||
this.icon = ICONS.REMOVE as Partial<IconNames>;
|
||||
this.disableDescription = 'Delete';
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
|
||||
@@ -3,12 +3,12 @@ import {IconNames, ICONS} from '../../../constants';
|
||||
import {MenuItems} from '../items.utils';
|
||||
|
||||
export class DequeueFooterItem extends ItemFooterModel {
|
||||
id = MenuItems.dequeue;
|
||||
emit = true;
|
||||
icon = ICONS.DEQUEUE as Partial<IconNames>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MenuItems.dequeue;
|
||||
this.emit = true;
|
||||
this.icon = ICONS.DEQUEUE as Partial<IconNames>;
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
|
||||
@@ -7,7 +7,7 @@ export class DividerFooterItem extends GenericFooterItem {
|
||||
super({className: 'divider'});
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
override getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import {IconNames, ICONS} from '../../../constants';
|
||||
import {MenuItems} from '../items.utils';
|
||||
|
||||
export class EnqueueFooterItem extends ItemFooterModel {
|
||||
id = MenuItems.enqueue;
|
||||
emit = true;
|
||||
icon = ICONS.ENQUEUE as Partial<IconNames>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MenuItems.enqueue;
|
||||
this.emit = true;
|
||||
this.icon = ICONS.ENQUEUE as Partial<IconNames>;
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
|
||||
@@ -2,7 +2,6 @@ import {IFooterState, ItemFooterModel} from './footer-items.models';
|
||||
import {IconNames} from '../../../constants';
|
||||
|
||||
export class GenericFooterItem extends ItemFooterModel {
|
||||
emit = true;
|
||||
constructor({icon = null,
|
||||
title = '',
|
||||
disable = false,
|
||||
@@ -15,6 +14,7 @@ export class GenericFooterItem extends ItemFooterModel {
|
||||
this.disable = disable;
|
||||
this.disableDescription = disableDescription;
|
||||
this.class = className;
|
||||
this.emit = true;
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
|
||||
@@ -2,13 +2,13 @@ import {ItemFooterModel, IFooterState} from './footer-items.models';
|
||||
import {IconNames, ICONS} from '../../../constants';
|
||||
|
||||
export class HasReadOnlyFooterItem extends ItemFooterModel {
|
||||
emit = true;
|
||||
icon = ICONS.ALERT as Partial<IconNames>;
|
||||
description = `Selected read-only items cannot be modified`;
|
||||
wrapperClass = 'has-example-item';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.emit = true;
|
||||
this.icon = ICONS.ALERT as Partial<IconNames>;
|
||||
this.description = `Selected read-only items cannot be modified`;
|
||||
this.wrapperClass = 'has-example-item';
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
|
||||
@@ -3,12 +3,12 @@ import {IconNames, ICONS} from '../../../constants';
|
||||
import {MenuItems} from '../items.utils';
|
||||
|
||||
export class MoveToFooterItem extends ItemFooterModel {
|
||||
id = MenuItems.moveTo;
|
||||
emit = true;
|
||||
icon = ICONS.MOVE_TO as Partial<IconNames>;
|
||||
disableDescription = 'Move To';
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MenuItems.moveTo;
|
||||
this.emit = true;
|
||||
this.icon = ICONS.MOVE_TO as Partial<IconNames>;
|
||||
this.disableDescription = 'Move To';
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
|
||||
@@ -4,13 +4,13 @@ import {MenuItems} from '../items.utils';
|
||||
import {EntityTypeEnum} from '../../../../shared/constants/non-common-consts';
|
||||
|
||||
export class PublishFooterItem extends ItemFooterModel {
|
||||
id = MenuItems.publish;
|
||||
emit = true;
|
||||
icon = ICONS.PUBLISHED as Partial<IconNames>;
|
||||
|
||||
constructor(private entityType: EntityTypeEnum) {
|
||||
super();
|
||||
this.disableDescription = entityType === EntityTypeEnum.experiment ? this.disableDescription : ``;
|
||||
this.id = MenuItems.publish;
|
||||
this.emit = true;
|
||||
this.icon = ICONS.PUBLISHED as Partial<IconNames>;
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
|
||||
@@ -5,12 +5,12 @@ import {MenuItems} from '../items.utils';
|
||||
import {EntityTypeEnum} from '../../../../shared/constants/non-common-consts';
|
||||
|
||||
export class ResetFooterItem<T extends {status: TaskStatusEnum}> extends ItemFooterModel {
|
||||
id = MenuItems.reset;
|
||||
emit = true;
|
||||
icon = ICONS.RESET as Partial<IconNames>;
|
||||
|
||||
constructor(public entitiesType: EntityTypeEnum) {
|
||||
super();
|
||||
this.id = MenuItems.reset;
|
||||
this.emit = true;
|
||||
this.icon = ICONS.RESET as Partial<IconNames>;
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): { icon?: IconNames; title?: string; description?: string; disable?: boolean; disableDescription?: string; emit?: boolean; emitValue?: boolean; preventCurrentItem?: boolean; class?: string; wrapperClass?: string } {
|
||||
|
||||
@@ -4,15 +4,15 @@ import {MenuItems, selectionTags} from '../items.utils';
|
||||
|
||||
export class SelectedTagsFooterItem extends ItemFooterModel {
|
||||
|
||||
id = MenuItems.tags;
|
||||
isTag = true;
|
||||
disableDescription = 'Tags';
|
||||
|
||||
constructor(
|
||||
public entitiesType: EntityTypeEnum,
|
||||
) {
|
||||
super();
|
||||
this.id = MenuItems.tags;
|
||||
this.isTag = true;
|
||||
this.disableDescription = 'Tags';
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>): {
|
||||
icon?: any; title?: string; description?: string; disable?: boolean; disableDescription?: string;
|
||||
emit?: boolean; emitValue?: any; preventCurrentItem?: boolean; class?: string; wrapperClass?: string; tags: string[];
|
||||
|
||||
@@ -3,13 +3,13 @@ import {IFooterState, ItemFooterModel} from './footer-items.models';
|
||||
import {MenuItems} from '../items.utils';
|
||||
|
||||
export class ShowItemsFooterSelected extends ItemFooterModel {
|
||||
id = MenuItems.showAllItems;
|
||||
emit = true;
|
||||
class = 'show-all';
|
||||
|
||||
|
||||
constructor(public entitiesType: EntityTypeEnum) {
|
||||
super();
|
||||
this.id = MenuItems.showAllItems;
|
||||
this.emit = true;
|
||||
this.class = 'show-all';
|
||||
|
||||
}
|
||||
|
||||
getItemState(state: IFooterState<any>) {
|
||||
|
||||
@@ -1,33 +1,55 @@
|
||||
<div *ngIf="noGraphs" class="no-output" >
|
||||
<div *ngIf="noGraphs && !(multipleSingleValueData?.data.length > 0) && !(singleValueData?.length > 0)" class="no-output" >
|
||||
<i class="icon no-output-icon" [ngClass]="isDarkTheme ? 'i-no-plots-dark' : 'i-no-plots'"></i>
|
||||
<h3>NO CHART DATA</h3>
|
||||
<h4>NO CHART DATA</h4>
|
||||
</div>
|
||||
|
||||
<div #allMetrics class="metrics-section">
|
||||
<div class="d-flex align-items-center justify-content-center summary-container" *ngIf="singleValueData?.length>0 && !hiddenList.includes('Summary')">
|
||||
<div class="d-flex align-items-center justify-content-center summary-container" *ngIf="singleValueData?.length>0 && !hiddenList.includes(singleValueChartTitle)">
|
||||
<sm-single-value-summary-table
|
||||
*ngIf="exportForReport; else: noEmbedCode"
|
||||
[data]="singleValueData"
|
||||
[experimentName]="experimentName"
|
||||
class="single-value-summary-table"
|
||||
class="single-value-summary-section"
|
||||
(createEmbedCode)="creatingEmbedCode(null, $event)"
|
||||
></sm-single-value-summary-table>
|
||||
<ng-template #noEmbedCode>
|
||||
<sm-single-value-summary-table
|
||||
[data]="singleValueData"
|
||||
[experimentName]="experimentName"
|
||||
class="single-value-summary-table"
|
||||
class="single-value-summary-section"
|
||||
></sm-single-value-summary-table>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="all-metrics"
|
||||
<div
|
||||
*ngIf="multipleSingleValueData?.data.length > 0"
|
||||
[class.hidden]="hiddenList.includes(singleValueChartTitle)"
|
||||
class="d-flex align-items-center justify-content-center summary-container"
|
||||
>
|
||||
<sm-single-graph
|
||||
#singleValueGraph
|
||||
class="w-100 single-value-summary-section"
|
||||
[graphsNumber]="1"
|
||||
[legendStringLength]="legendStringLength"
|
||||
[chart]="multipleSingleValueData"
|
||||
[id]="'singleValues'"
|
||||
[isCompare]="isCompare"
|
||||
[hoverMode]="hoverMode"
|
||||
[isDarkTheme]="isDarkTheme"
|
||||
[showLoaderOnDraw]="showLoaderOnDraw"
|
||||
[identifier]="generateIdentifier(multipleSingleValueData)"
|
||||
[exportForReport]="exportForReport"
|
||||
(createEmbedCode)="creatingEmbedCode(null, $event)"
|
||||
(hoverModeChanged)="hoverModeChanged.emit($event)"
|
||||
></sm-single-graph>
|
||||
</div>
|
||||
<div [class.all-metrics]="graphList?.length > 0"
|
||||
[class.row]="!isGroupGraphs"
|
||||
>
|
||||
<div *ngFor="let metric of (graphList); trackBy: trackByFn" class="metric-group-container less-padding"
|
||||
#metricGroup
|
||||
[class.two-in-a-row]="!isGroupGraphs && graphList.length > 1 && isWidthBigEnough()"
|
||||
[style.height.percent]=""
|
||||
[class.hidden]="hiddenList.includes(metric)"
|
||||
[class.hidden]="hiddenList?.includes(metric)"
|
||||
>
|
||||
<div [id]="experimentGraphidPrefix + metric" class="graph-id">
|
||||
<div>
|
||||
@@ -66,7 +88,7 @@
|
||||
[xAxisType]="xAxisType"
|
||||
[height]="height"
|
||||
[width]="width"
|
||||
[moveLegendToTitle]="groupBy === groupByCharts.none"
|
||||
[moveLegendToTitle]="checkIfLegendToTitle(chartItem)"
|
||||
[identifier]="generateIdentifier(chartItem)"
|
||||
[exportForReport]="exportForReport"
|
||||
(createEmbedCode)="creatingEmbedCode(chartItem, $event)"
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
border-bottom: 1px solid #efefef;
|
||||
}
|
||||
|
||||
.single-value-summary-table {
|
||||
margin: 16px 0;
|
||||
.single-value-summary-section {
|
||||
margin: 16px 0 8px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
@@ -285,6 +285,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.no-output h4 {
|
||||
color: $blue-100;
|
||||
}
|
||||
.no-output-icon {
|
||||
height: 100px;
|
||||
width: 150px;
|
||||
|
||||
@@ -18,7 +18,7 @@ import {SingleGraphComponent} from '../single-graph/single-graph.component';
|
||||
import {
|
||||
ChartHoverModeEnum,
|
||||
EXPERIMENT_GRAPH_ID_PREFIX,
|
||||
SINGLE_GRAPH_ID_PREFIX
|
||||
SINGLE_GRAPH_ID_PREFIX, singleValueChartTitle
|
||||
} from '../../experiments/shared/common-experiments.const';
|
||||
import {ScalarKeyEnum} from '~/business-logic/model/events/scalarKeyEnum';
|
||||
import {AdminService} from '~/shared/services/admin.service';
|
||||
@@ -39,6 +39,8 @@ import {v4} from 'uuid';
|
||||
import {selectGraphsPerRow} from '@common/experiments/reducers';
|
||||
import {setGraphsPerRow} from '@common/experiments/actions/common-experiment-output.actions';
|
||||
import {SmoothTypeEnum} from '@common/shared/single-graph/single-graph.utils';
|
||||
import {maxInArray} from '@common/shared/utils/helpers.util';
|
||||
import {Title} from "@angular/platform-browser";
|
||||
|
||||
@Component({
|
||||
selector: 'sm-experiment-graphs',
|
||||
@@ -48,18 +50,6 @@ import {SmoothTypeEnum} from '@common/shared/single-graph/single-graph.utils';
|
||||
})
|
||||
|
||||
export class ExperimentGraphsComponent implements OnDestroy {
|
||||
|
||||
groupByOptions = [
|
||||
{
|
||||
name: 'Metric',
|
||||
value: groupByCharts.metric
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: groupByCharts.none
|
||||
}
|
||||
];
|
||||
public groupByCharts = groupByCharts;
|
||||
readonly experimentGraphidPrefix = EXPERIMENT_GRAPH_ID_PREFIX;
|
||||
readonly singleGraphidPrefix = SINGLE_GRAPH_ID_PREFIX;
|
||||
public graphList: Array<any> = [];
|
||||
@@ -84,6 +74,7 @@ export class ExperimentGraphsComponent implements OnDestroy {
|
||||
private _hiddenList: string[];
|
||||
private maxUserHeight: number;
|
||||
private maxUserWidth: number;
|
||||
protected readonly singleValueChartTitle = singleValueChartTitle;
|
||||
|
||||
@HostListener('window:resize')
|
||||
onResize() {
|
||||
@@ -113,6 +104,7 @@ export class ExperimentGraphsComponent implements OnDestroy {
|
||||
@Input() hoverMode: ChartHoverModeEnum;
|
||||
@Input() disableResize: boolean = false;
|
||||
@Input() singleValueData: Array<EventsGetTaskSingleValueMetricsResponseValues>;
|
||||
@Input() multipleSingleValueData: ExtFrame;
|
||||
@Input() experimentName: string;
|
||||
|
||||
|
||||
@@ -135,6 +127,7 @@ export class ExperimentGraphsComponent implements OnDestroy {
|
||||
|
||||
@ViewChildren('metricGroup') allMetricGroups !: QueryList<ElementRef>;
|
||||
@ViewChildren('singleGraphContainer') singleGraphs !: QueryList<ElementRef>;
|
||||
@ViewChildren('singleValueGraph') singleValueGraph !: QueryList<SingleGraphComponent>;
|
||||
@ViewChildren(SingleGraphComponent) allGraphs !: QueryList<SingleGraphComponent>;
|
||||
|
||||
constructor(
|
||||
@@ -190,8 +183,8 @@ export class ExperimentGraphsComponent implements OnDestroy {
|
||||
this.graphsData = this.addId(this.sortGraphsData(graphGroups));
|
||||
|
||||
this.graphsPerRow = (this.disableResize || this.allGroupsSingleGraphs()) ? 1 : this.graphsPerRow;
|
||||
this.maxUserHeight = Math.max(...Object.values(this.graphsData).flat().map((chart: ExtFrame) => chart.layout?.height || 0));
|
||||
this.maxUserWidth = Math.max(...Object.values(this.graphsData).flat().map((chart: ExtFrame) => chart.layout?.width || 0));
|
||||
this.maxUserHeight = maxInArray(Object.values(this.graphsData).flat().map((chart: ExtFrame) => chart.layout?.height || 0));
|
||||
this.maxUserWidth = maxInArray(Object.values(this.graphsData).flat().map((chart: ExtFrame) => chart.layout?.width || 0));
|
||||
if (this.maxUserHeight) {
|
||||
this.height = this.maxUserHeight;
|
||||
}
|
||||
@@ -344,7 +337,8 @@ export class ExperimentGraphsComponent implements OnDestroy {
|
||||
if ($event.edges.right) {
|
||||
const containerWidth = this.el.nativeElement.clientWidth;
|
||||
const userWidth = $event.rectangle.width;
|
||||
this.store.dispatch(setGraphsPerRow({graphsPerRow: this.calcGraphPerRow(userWidth, containerWidth)}));
|
||||
this.graphsPerRow = this.calcGraphPerRow(userWidth, containerWidth)
|
||||
this.store.dispatch(setGraphsPerRow({graphsPerRow: this.graphsPerRow}));
|
||||
|
||||
if (!this.isGroupGraphs) {
|
||||
this.allMetricGroups.forEach(metricGroup => {
|
||||
@@ -405,7 +399,7 @@ export class ExperimentGraphsComponent implements OnDestroy {
|
||||
const element = this.allMetrics.nativeElement.getElementsByClassName('graph-id')[EXPERIMENT_GRAPH_ID_PREFIX + id] as HTMLDivElement;
|
||||
if (element) {
|
||||
this.allMetrics.nativeElement.scrollTo({top: element.offsetTop, behavior: 'smooth'});
|
||||
} else if (this.allMetrics.nativeElement.getElementsByTagName('sm-single-value-summary-table')[0]){
|
||||
} else if (this.allMetrics.nativeElement.getElementsByClassName('single-value-summary-section')[0]){
|
||||
this.allMetrics.nativeElement.scrollTo({top: 0, behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
@@ -431,7 +425,7 @@ export class ExperimentGraphsComponent implements OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.groupBy === groupByCharts.none) {
|
||||
if (!this.isCompare && this.groupBy === groupByCharts.none) {
|
||||
// split scalars by variants
|
||||
this.createEmbedCode.emit({
|
||||
metrics: [chartItem.data[0].originalMetric ?? chartItem.metric.substring(0, chartItem.metric.lastIndexOf('/'))?.trim()],
|
||||
@@ -441,6 +435,7 @@ export class ExperimentGraphsComponent implements OnDestroy {
|
||||
});
|
||||
} else {
|
||||
this.createEmbedCode.emit({
|
||||
originalObject: chartItem.task,
|
||||
metrics: [chartItem.metric],
|
||||
variants: chartItem.variants ?? [chartItem.variant],
|
||||
...((xaxis || this.xAxisType) && {xaxis: xaxis ?? this.xAxisType}),
|
||||
@@ -449,4 +444,8 @@ export class ExperimentGraphsComponent implements OnDestroy {
|
||||
domRect});
|
||||
}
|
||||
}
|
||||
|
||||
checkIfLegendToTitle(chartItem: ExtFrame) {
|
||||
return this.isGroupGraphs && chartItem.data?.length === 1 && (!chartItem.data[0].name || chartItem.data[0].name === chartItem.layout?.title) && ['scatter', 'scattergl', 'bar', 'scatter3d'].includes(chartItem?.data[0]?.type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
[step]="smoothType === smoothTypeEnum.exponential ? 0.05 : 1"
|
||||
[(ngModel)]="smoothWeight"
|
||||
(ngModelChange)="trimToLimits($event)"
|
||||
(blur)="smoothWeight === null && trimToLimits(-1)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="smooth-type no-bottom">
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
|
||||
mat-form-field {
|
||||
width: 150px;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.axis-type-field {
|
||||
@@ -150,3 +152,5 @@ mat-slider {
|
||||
margin-top: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -70,6 +70,10 @@ export class GraphSettingsBarComponent {
|
||||
if (value === 0) {
|
||||
return;
|
||||
}
|
||||
if (value === null) {
|
||||
this.changeWeight.emit(this.smoothWeight);
|
||||
return;
|
||||
}
|
||||
if (value > (this.smoothType === smoothTypeEnum.exponential ? 0.999 : 100) || value < (this.smoothType === smoothTypeEnum.exponential ? 0 : 1)) {
|
||||
this.smoothWeight = null;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
@import "src/app/webapp-common/shared/ui-components/styles/variables";
|
||||
@import "variables";
|
||||
:host{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.middle {
|
||||
height: 14px;
|
||||
margin-top: 2px
|
||||
}
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import {capitalize} from 'lodash-es';
|
||||
|
||||
@Pipe({
|
||||
name: 'camelToTitle',
|
||||
@@ -14,11 +15,6 @@ export class CamelToTitlePipe implements PipeTransform {
|
||||
|
||||
const words = value.match(/[A-Za-z][a-z]*/g);
|
||||
|
||||
return words.map(this.capitalize).join(' ');
|
||||
return words.map(capitalize).join(' ');
|
||||
}
|
||||
|
||||
capitalize(word) {
|
||||
return word.charAt(0).toUpperCase() + word.substring(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
74
src/app/webapp-common/shared/pipes/duration-formater.pipe.ts
Normal file
74
src/app/webapp-common/shared/pipes/duration-formater.pipe.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'durationFormater',
|
||||
pure: true
|
||||
})
|
||||
export class DurationFormaterPipe implements PipeTransform {
|
||||
|
||||
transform(value: any, arg1: any, arg2: any): any {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
let days: any;
|
||||
let seconds: any;
|
||||
let minutes: any;
|
||||
let hours: any;
|
||||
|
||||
if (arg1 === 'ms' && arg2 === 'hhmmss') {
|
||||
seconds = Math.floor((value / 1000) % 60);
|
||||
minutes = Math.floor(((value / (1000 * 60)) % 60));
|
||||
hours = Math.floor((value / (1000 * 60 * 60)));
|
||||
return this.format(arg2, seconds, minutes, hours, days);
|
||||
|
||||
} else if (arg1 === 's' && arg2 === 'hhmmss') {
|
||||
seconds = Math.floor((value % 60));
|
||||
minutes = Math.floor(((value / 60) % 60));
|
||||
hours = Math.floor(((value / 60) / 60));
|
||||
return this.format(arg2, seconds, minutes, hours, days);
|
||||
|
||||
} else if (arg1 === 'ms' && (arg2 === 'ddhhmmss' || arg2 === 'ddhhmmssLong')) {
|
||||
seconds = Math.floor(((value / 1000) % 60));
|
||||
minutes = Math.floor((value / (1000 * 60) % 60));
|
||||
hours = Math.floor((value / (1000 * 60 * 60) % 24));
|
||||
days = Math.floor((value / (1000 * 60 * 60 * 24)));
|
||||
return this.format(arg2, seconds, minutes, hours, days);
|
||||
|
||||
} else if (arg1 === 's' && (arg2 === 'ddhhmmss' || arg2 === 'ddhhmmssLong')) {
|
||||
seconds = Math.floor(value % 60);
|
||||
minutes = Math.floor(((value / 60) % 60));
|
||||
hours = Math.floor(((value / 60) / 60) % 24);
|
||||
days = Math.floor((((value / 60) / 60) / 24));
|
||||
return this.format(arg2, seconds, minutes, hours, days);
|
||||
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private format = (arg2, seconds, minutes, hours, days) => {
|
||||
(days < 10) ? days = '0' + days : days;
|
||||
(hours < 10) ? hours = '0' + hours : hours;
|
||||
(minutes < 10) ? minutes = '0' + minutes : minutes;
|
||||
(seconds < 10) ? seconds = '0' + seconds : seconds;
|
||||
|
||||
switch (arg2) {
|
||||
case 'hhmmss':
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
|
||||
case 'ddhhmmss':
|
||||
return `${days === '00' ? '' : days}${days === '00' ? '' : 'd'}
|
||||
${(days === '00' && hours === '00') ? '' : hours}${(days === '00' && hours === '00') ? '' : 'h'}
|
||||
${(days === '00' && hours === '00' && minutes === '00') ? '' : minutes}${(days === '00' && hours === '00' && minutes === '00')?'':'m'}
|
||||
${seconds}s`;
|
||||
|
||||
|
||||
case 'ddhhmmssLong':
|
||||
return `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import { Pipe, PipeTransform } from '@angular/core';
|
||||
})
|
||||
export class InitialsPipe implements PipeTransform {
|
||||
|
||||
transform(value: string): unknown {
|
||||
transform(value: string) {
|
||||
if (value !== null) {
|
||||
return value.split(' ').map(part => part[0]).join('');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export class LabelValuePipe implements PipeTransform {
|
||||
|
||||
transform(value: string[], args?: any): Array<{ label: string; value: string }> {
|
||||
if (!value) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (!value.every(item => typeof item === 'string')) {
|
||||
return value as any;
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'msToSec',
|
||||
pure: true
|
||||
})
|
||||
export class MsToSecPipe implements PipeTransform {
|
||||
|
||||
transform(value: any, args?: any): any {
|
||||
|
||||
if (!value) {
|
||||
return '';
|
||||
} else {
|
||||
if (value < 1000) {
|
||||
return (value + ' ms');
|
||||
}
|
||||
const result = [];
|
||||
let seconds = value / 1000;
|
||||
// 2- Extract hours:
|
||||
// const hours = ('0' + parseInt((seconds / 3600).toString(), 10)).slice(-2); // 3,600 seconds in 1 hour
|
||||
const hours = Math.floor(seconds / 3600) % 100;
|
||||
if (hours) {
|
||||
result.push(hours.toString().padStart(2, '0') + 'h');
|
||||
}
|
||||
|
||||
seconds = seconds % 3600; // seconds remaining after extracting hours
|
||||
// 3- Extract minutes:
|
||||
const minutes = Math.floor(seconds / 60) % 100;
|
||||
if (minutes) {
|
||||
result.push(minutes.toString().padStart(2, '0') + 'm');
|
||||
}
|
||||
// const minutes = ('0' + parseInt((seconds / 60).toString(), 10)).slice(-2); // 60 seconds in 1 minute
|
||||
// 4- Keep only seconds not extracted to minutes:
|
||||
result.push(Math.floor(seconds % 60).toString().padStart(2, '0') + 's');
|
||||
|
||||
return result.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
27
src/app/webapp-common/shared/pipes/sec-to-hours.pipe.ts
Normal file
27
src/app/webapp-common/shared/pipes/sec-to-hours.pipe.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {Pipe, PipeTransform} from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'secToHours',
|
||||
pure: true
|
||||
})
|
||||
export class SecToHoursPipe implements PipeTransform {
|
||||
|
||||
transform(value: any, args?: any): any {
|
||||
|
||||
if (!value) {
|
||||
return '—';
|
||||
} else {
|
||||
let seconds = value;
|
||||
// 2- Extract hours:
|
||||
const hours = parseInt((seconds / 3600).toString(), 10); // 3,600 seconds in 1 hour
|
||||
|
||||
seconds = seconds % 3600; // seconds remaining after extracting hours
|
||||
// 3- Extract minutes:
|
||||
const minutes = ('0' + parseInt((seconds / 60).toString(), 10)).slice(-2); // 60 seconds in 1 minute
|
||||
// 4- Keep only seconds not extracted to minutes:
|
||||
seconds = seconds % 60;
|
||||
return (hours + ':' + minutes + 'h');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import {HighlightSearchTextPipe} from './highlight-search-text.pipe';
|
||||
import {HideHashPipe} from './hide-hash.pipe';
|
||||
import {TimeAgoPipe} from './timeAgo';
|
||||
import {TimeTillNowPipe} from './time-till-now.pipe';
|
||||
import {MsToSecPipe} from './ms-to-sec.pipe';
|
||||
import {HasExampleItemPipe} from './has-example-item.pipe';
|
||||
import {AdvancedFilterPipe} from './advanced-filter.pipe';
|
||||
import {SafePipe} from './safe.pipe';
|
||||
@@ -65,15 +64,17 @@ import {IsStringPipe} from './is-string.pipe';
|
||||
import {CleanProjectPathPipe} from './clean-project-path.pipe';
|
||||
import {BaseNamePipe} from '@common/shared/pipes/base-name.pipe';
|
||||
import {CountLinesPipe} from '@common/shared/pipes/count-lines.pipe';
|
||||
import {SecToHoursPipe} from '@common/shared/pipes/sec-to-hours.pipe';
|
||||
import { DurationFormaterPipe } from './duration-formater.pipe';
|
||||
|
||||
const pipes = [
|
||||
CamelToTitlePipe, FilenameFromPath, FilterPipe, FloorPipe, KeyValuePipe, NAPipe, SortPipe, IsVideoPipe, IsAudioPipe, FilterInternalPipe, UuidPipe,
|
||||
JoinPipe, KeyvalPipe, LabelValuePipe, NoUnderscorePipe, UniqueByPipe, MsToHoursPipe, MsToSecPipe, DurationPipe,
|
||||
JoinPipe, KeyvalPipe, LabelValuePipe, NoUnderscorePipe, UniqueByPipe, MsToHoursPipe, DurationPipe,
|
||||
ToExponentialPipe, HighlightSearchTextPipe, HighlightSearchPipe, HideHashPipe, HideHashTitlePipe, TimeAgoPipe, TimeTillNowPipe, HasExampleItemPipe, safeAngularUrlParameterPipe,
|
||||
AdvancedFilterPipe, SafePipe, SelectOptionValueToLabelPipe, ToPercentagePipe, ReplaceViaMapPipe, FilterByIdPipe, FilterLast, FilterOutPipe, SimpleFilterPipe,
|
||||
BreadcrumbsEllipsisPipe, ShortProjectNamePipe, ProjectLocationPipe, StringIncludedInArrayPipe, ToPropertyArrayPipe, MenuItemTextPipe, InitialsPipe, IdToObjectsArrayPipe, IsEmptyPipe,
|
||||
TemplateInjectorPipe, TestConditionalPipe, GroupHasErrorsPipe, FormgroupHasRequiredFieldPipe, RegexPipe, LabelValueToStringArrayPipe, IsStringPipe,
|
||||
ItemByIdPipe, HideRedactedArgumentsPipe, HasCompletedPipe, CleanProjectPathPipe, BaseNamePipe
|
||||
ItemByIdPipe, HideRedactedArgumentsPipe, HasCompletedPipe, CleanProjectPathPipe, BaseNamePipe, SecToHoursPipe, DurationFormaterPipe
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -102,7 +102,7 @@ export class TimeAgoPipe implements PipeTransform, OnDestroy {
|
||||
newDate.setHours(hours - offset);
|
||||
return newDate;
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
smUniqueNameValidator
|
||||
[existingNames]="[]"
|
||||
[parent]="targetFolder.value"
|
||||
pattern="^[^/]*$"
|
||||
pattern="^[^\/]*$"
|
||||
required minlength="3" >
|
||||
</mat-form-field>
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
>
|
||||
<div [innerHTML]="projectName | highlightSearchText:targetFolder.value"></div>
|
||||
</mat-option>
|
||||
<div *ngIf="!projects" style="line-height: 100px; pointer-events: none;">
|
||||
<mat-spinner class="m-auto" [diameter]="80" [strokeWidth]="8"></mat-spinner>
|
||||
<div *ngIf="!projects" class="p-4 pe-none">
|
||||
<mat-spinner class="m-auto" [diameter]="32" [strokeWidth]="4" color="accent"></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="projects && !noMoreOptions" (smScrollEnd)="!loading && loadMore(targetFolder.value)" class="text-center">Loading more...</div>
|
||||
<mat-option disabled style="height: 0; min-height: 0;"></mat-option> <!-- Empty mat-option, so the autocomplete menu will always pop -->
|
||||
@@ -93,10 +93,8 @@
|
||||
[pattern]="outputDestPattern"
|
||||
placeholder="e.g. s3://bucket. gs://bucket">
|
||||
</mat-form-field>
|
||||
<div class="row">
|
||||
<div class="col-24 create-project-button">
|
||||
<button class="btn btn-dark-fill center" data-id="Create Project" [disabled]="projectForm.invalid" (click)="send()">CREATE PROJECT
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-100 create-project-button">
|
||||
<button class="btn btn-dark-fill center" data-id="Create Project" [disabled]="projectForm.invalid" (click)="send()">CREATE PROJECT
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
:host {
|
||||
form {
|
||||
gap: 12px;
|
||||
}
|
||||
.create-project-button {
|
||||
padding-top: 32px;
|
||||
padding: 32px 12px 0;
|
||||
}
|
||||
|
||||
small.text-danger {
|
||||
//position: absolute !important;
|
||||
//top: -15px;
|
||||
}
|
||||
|
||||
#project-description {
|
||||
max-height: 250px;
|
||||
padding-top: 12px;
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
|
||||
@@ -24,8 +9,10 @@
|
||||
min-height: 68px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
transform: translateY(3px);
|
||||
.creat-new-suffix {
|
||||
display: inline-block;
|
||||
margin-right: 18px;
|
||||
font-size: 14px;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export class ProjectDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
filterSearchChanged($event: {value: string; loadMore?: boolean}) {
|
||||
!$event.loadMore && this.store.dispatch(resetTablesFilterProjectsOptions());
|
||||
this.store.dispatch(getTablesFilterProjectsOptions({searchString: $event.value || '', loadMore: $event.loadMore}));
|
||||
this.store.dispatch(getTablesFilterProjectsOptions({searchString: $event.value || '', loadMore: $event.loadMore, allowPublic: false}));
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<div class="dynamic-subtitle">
|
||||
<div class="line">Move <span class="p-name ellipsis">{{projectName | shortProjectName}}</span> from <span
|
||||
class="p-name">{{projectName | projectLocation}}</span></div>
|
||||
<div class="line">To <span *ngIf="project.parent; else placeH" class="p-name">{{project.parent}}</span>
|
||||
<ng-template #placeH><span class="p-holder">Project…</span></ng-template>
|
||||
</div>
|
||||
<div>Move</div>
|
||||
<div><span class="p-name">{{projectName | shortProjectName}}</span> from <span class="p-name">{{projectName | projectLocation}}</span></div>
|
||||
<div>To</div>
|
||||
<div><span *ngIf="project.parent; else placeH" class="p-name">{{project.parent}}</span> <ng-template #placeH><span class="p-holder">Project…</span></ng-template></div>
|
||||
</div>
|
||||
<form #moveToForm="ngForm" (submit)="send()">
|
||||
<mat-form-field class="w-100"
|
||||
@@ -62,8 +61,8 @@
|
||||
(onSelectionChange)="optionSelected()">
|
||||
<div [innerHTML]="projectName | highlightSearchText:projectInput.value"></div>
|
||||
</mat-option>
|
||||
<div *ngIf="projectsNames === null" style="line-height: 100px; pointer-events: none;">
|
||||
<mat-spinner class="m-auto" [diameter]="80" [strokeWidth]="8"></mat-spinner>
|
||||
<div *ngIf="projectsNames === null" class="p-4 pe-none">
|
||||
<mat-spinner class="m-auto" [diameter]="32" [strokeWidth]="4" color="accent"></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="projects && !noMoreOptions" (smScrollEnd)="!loading && loadMore(projectInput.value)" class="text-center">Loading more...</div>
|
||||
<mat-option disabled style="height: 0; min-height: 0;"></mat-option> <!-- Empty mat-option, so the autocomplete menu will always pop -->
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
:host {
|
||||
.creat-new-suffix {
|
||||
display: inline-block;
|
||||
margin-right: 18px;
|
||||
font-size: 14px;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@@ -25,47 +28,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.create-project-button {
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
.dynamic-subtitle {
|
||||
padding: 9px;
|
||||
margin-bottom: 9px;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 12px 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 4px;
|
||||
background-color: $blue-50;
|
||||
|
||||
.p-name {
|
||||
font-weight: bold;
|
||||
max-width: calc(100% - 38px);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
color: $blue-600;
|
||||
font-weight: 500;
|
||||
word-break: break-word; /* prevent long strings from breaking the layout */
|
||||
}
|
||||
|
||||
.p-holder {
|
||||
opacity: 0.3;
|
||||
font-weight: bold;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
line-height: 1.5;
|
||||
color: $blue-500;
|
||||
opacity: 0.6;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
small.text-danger {
|
||||
//position: absolute !important;
|
||||
//top: -15px;
|
||||
}
|
||||
|
||||
#project-description {
|
||||
max-height: 250px;
|
||||
padding-top: 12px;
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
|
||||
@@ -73,8 +60,4 @@
|
||||
min-height: 68px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
transform: translateY(3px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,9 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-24 create-queue-button">
|
||||
<button class="btn btn-dark-fill center" [disabled]="queueForm.invalid"
|
||||
(click)="send()">{{isEdit ? 'RENAME' : 'CREATE'}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="create-queue-button">
|
||||
<button class="btn btn-dark-fill center" [disabled]="queueForm.invalid || (isEdit && queueForm.pristine)"
|
||||
(click)="send()">{{isEdit ? 'RENAME' : 'CREATE'}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
}
|
||||
|
||||
.create-queue-button {
|
||||
padding-top: 32px;
|
||||
width: 100%;
|
||||
padding: 32px 12px 0;
|
||||
}
|
||||
|
||||
small.text-danger {
|
||||
|
||||
@@ -20,7 +20,7 @@ export class QueueCreateDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(private store: Store, private matDialogRef: MatDialogRef<QueueCreateDialogComponent>, @Inject(MAT_DIALOG_DATA) public data) {
|
||||
if (data) {
|
||||
this.queue = data;
|
||||
this.queue = {...data};
|
||||
this.editMode = true;
|
||||
}
|
||||
this.queues$ = this.store.select(createQueueSelectors.selectQueues);
|
||||
|
||||
@@ -35,7 +35,11 @@ export class BreadcrumbsService implements OnDestroy {
|
||||
this.store.dispatch(setBreadcrumbs({
|
||||
breadcrumbs: [
|
||||
[breadcrumbOptions.featureBreadcrumb],
|
||||
...(projectAncestors?.length > 0 ? [projectAncestors?.filter(ancestor => (!breadcrumbOptions.projectsOptions.filterBaseNameWith || !breadcrumbOptions.projectsOptions.filterBaseNameWith.includes(ancestor.basename)))
|
||||
...([projectAncestors
|
||||
?.filter(ancestor =>
|
||||
!breadcrumbOptions.projectsOptions.filterBaseNameWith ||
|
||||
!breadcrumbOptions.projectsOptions.filterBaseNameWith.includes(ancestor.basename)
|
||||
)
|
||||
.map(ancestor => ({
|
||||
name: ancestor.basename,
|
||||
example: isExample(ancestor),
|
||||
@@ -43,9 +47,10 @@ export class BreadcrumbsService implements OnDestroy {
|
||||
type: CrumbTypeEnum.Project,
|
||||
hidden: ancestor.hidden,
|
||||
collapsable: true
|
||||
}))] : []),
|
||||
}))
|
||||
] ?? []),
|
||||
...(breadcrumbOptions.projectsOptions?.selectedProjectBreadcrumb ? [[breadcrumbOptions.projectsOptions.selectedProjectBreadcrumb]] : []),
|
||||
...(breadcrumbOptions.subFeatureBreadcrumb ? [[breadcrumbOptions.subFeatureBreadcrumb]] : []),
|
||||
...(breadcrumbOptions.subFeatureBreadcrumb && (!breadcrumbOptions.subFeatureBreadcrumb.onlyWithProject || projectAncestors?.length > 0) ? [[breadcrumbOptions.subFeatureBreadcrumb]] : []),
|
||||
]
|
||||
}));
|
||||
})
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {UsersState} from '@common/core/reducers/users-reducer';
|
||||
import {filter, take} from 'rxjs/operators';
|
||||
import {selectColorPreferences} from '../../ui-components/directives/choose-color/choose-color.reducer';
|
||||
import {addUpdateColorPreferences, ColorPreference} from '../../ui-components/directives/choose-color/choose-color.actions';
|
||||
import stc from 'string-to-color';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
export interface ColorCache {[label: string]: number[]}
|
||||
export const DOT_PLACEHOLDER = '--DOT--';
|
||||
|
||||
@@ -37,7 +36,7 @@ export class ColorHashService {
|
||||
if (colorCache) {
|
||||
return colorCache;
|
||||
}
|
||||
const tColor = tinycolor(stc(label));
|
||||
const tColor = new TinyColor(stc(label));
|
||||
const tLum = tColor.getLuminance();
|
||||
if (tLum < 0.3 && lighten) {
|
||||
tColor.lighten(30 - tLum * 100);
|
||||
@@ -76,14 +75,13 @@ export class ColorHashService {
|
||||
setColorForString(str: string, color: number[], savePreference: boolean = true) {
|
||||
if (savePreference) {
|
||||
this.updateColorCache(str, color);
|
||||
const cleanString = str.replace(/\./, DOT_PLACEHOLDER);
|
||||
this.store.dispatch(addUpdateColorPreferences({[cleanString]: color}));
|
||||
this.store.dispatch(addUpdateColorPreferences({[str]: color}));
|
||||
}
|
||||
}
|
||||
|
||||
public hex(hash: string) {
|
||||
const rgb = this.initColor(hash);
|
||||
return tinycolor({r: rgb[0], g: rgb[1], b: rgb[2], a: rgb[3]}).toHexString();
|
||||
return new TinyColor({r: rgb[0], g: rgb[1], b: rgb[2], a: rgb[3]}).toHexString();
|
||||
}
|
||||
|
||||
public getRgbString(str, opacity = -1) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
|
||||
export const rgbList2Hex = (rgbArray: number[]) =>
|
||||
tinycolor({r: rgbArray[0], g: rgbArray[1], b: rgbArray[2], ...(rgbArray.length === 4 && {a: rgbArray[3]})}).toHexString();
|
||||
new TinyColor({r: rgbArray[0], g: rgbArray[1], b: rgbArray[2], ...(rgbArray.length === 4 && {a: rgbArray[3]})}).toHexString();
|
||||
|
||||
export const RGB2HEX = (rgbArray: number[]) => {
|
||||
if (!rgbArray) {
|
||||
@@ -17,16 +17,95 @@ export const rgba2String = (rgba: number[]) => `rgba(${rgba.join(',')})`;
|
||||
|
||||
export const normalizeColorToString = (color) => {
|
||||
if (typeof color === 'string') {
|
||||
return tinycolor(color).toHexString();
|
||||
return new TinyColor(color).toHexString();
|
||||
}
|
||||
if (Array.isArray(color)) {
|
||||
return rgbList2Hex(color);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
|
||||
export const hexToRgb = hex => {
|
||||
const {r, g, b} = tinycolor(hex).toRgb();
|
||||
const {r, g, b} = new TinyColor(hex).toRgb();
|
||||
return [r, g, b];
|
||||
};
|
||||
|
||||
export const invertRgb = (rgb: [number, number, number]) => rgb.map(c => 255 - c);
|
||||
|
||||
export function rgbToHsl(rgbArray): [number, number, number] {
|
||||
let [r, g, b] = [...rgbArray];
|
||||
r /= 255, g /= 255, b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
|
||||
if (max == min) {
|
||||
h = s = 0; // achromatic
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return [h, s, l];
|
||||
}
|
||||
|
||||
export function hslToRgb(hslArray) {
|
||||
// const [h, s, l, a] = [...hslArray]; Skip this line for performance
|
||||
let r, g, b;
|
||||
|
||||
if (hslArray[1] === 0) {
|
||||
r = g = b = hslArray[2]; // achromatic
|
||||
} else {
|
||||
const q = hslArray[2] < 0.5 ? hslArray[2] * (1 + hslArray[1]) : hslArray[2] + hslArray[1] - hslArray[2] * hslArray[1];
|
||||
const p = 2 * hslArray[2] - q;
|
||||
|
||||
r = hue2rgb(p, q, hslArray[0] + 1 / 3);
|
||||
g = hue2rgb(p, q, hslArray[0]);
|
||||
b = hue2rgb(p, q, hslArray[0] - 1 / 3);
|
||||
}
|
||||
|
||||
return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255), hslArray[3] ? hslArray[3] : null];
|
||||
}
|
||||
|
||||
export function hue2rgb(p, q, t) {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
|
||||
export function getLuminanace(r, g, b) {
|
||||
const a = [r, g, b].map(function (v) {
|
||||
v /= 255;
|
||||
return v <= 0.03928
|
||||
? v / 12.92
|
||||
: Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
}
|
||||
|
||||
export function getHslContrast(hsl1, hsl2): number {
|
||||
return getContrast(hslToRgb(hsl1), hslToRgb(hsl2));
|
||||
}
|
||||
|
||||
export function getContrast(rgb1, rgb2) {
|
||||
const l1 = getLuminanace(rgb1[0], rgb1[1], rgb1[2]) + 0.05;
|
||||
const l2 = getLuminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05;
|
||||
return (Math.max(l1, l2) / Math.min(l1, l2));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {Injectable} from '@angular/core';
|
||||
import {environment} from '../../../../environments/environment';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {BehaviorSubject, of, timer, throwError} from 'rxjs';
|
||||
import {catchError, map, retryWhen, mergeMap} from 'rxjs/operators';
|
||||
import {catchError, retryWhen, mergeMap, tap} from 'rxjs/operators';
|
||||
import {Environment} from '../../../../environments/base';
|
||||
import { retryOperation } from '../utils/promie-with-retry';
|
||||
|
||||
@@ -31,10 +31,7 @@ export class ConfigurationService {
|
||||
mergeMap((err, i) => i > 2 ? throwError('Error from retry!') : timer(500))
|
||||
)),
|
||||
catchError(() => of({})),
|
||||
map(env => {
|
||||
ConfigurationService.globalEnvironment = {...ConfigurationService.globalEnvironment, ...env};
|
||||
this.globalEnvironmentObservable.next(ConfigurationService.globalEnvironment);
|
||||
})
|
||||
tap(env => this.setEnv(env))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {DagManagerService, DagModelItem} from '@ngneat/dag';
|
||||
})
|
||||
export class DagManagerUnsortedService<T extends DagModelItem> extends DagManagerService<T> {
|
||||
|
||||
convertArrayToDagModel(itemsArray: Array<T>): Array<Array<T>> {
|
||||
override convertArrayToDagModel(itemsArray: Array<T>): Array<Array<T>> {
|
||||
const result = [];
|
||||
const levels = {};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class ErrorService {
|
||||
57: this.template`Account already exists for this ${'provider'} identity. Use 'Log In' Instead.`,
|
||||
58: this.template`No account exists. Use the provider you signed up with or sign up to create a new account`,
|
||||
62: this.template`Please check your email to continue the signup process`,
|
||||
67: this.template`${'email'} is not registered - please contact your admin`,
|
||||
67: this.template`${'email'} does not have access to ClearML - Ask your admin to whitelist this address`,
|
||||
1205: this.template`This workspace is at its limit for concurrently running instances.`,
|
||||
509: this.template`Can't edit frame's metadata for published version.`
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||
import {catchError, filter, map, mergeMap, retryWhen, switchMap, tap} from 'rxjs/operators';
|
||||
import {catchError, filter, map, retry, switchMap, take, tap} from 'rxjs/operators';
|
||||
import {HTTP} from '~/app.constants';
|
||||
import {UsersGetAllResponse} from '~/business-logic/model/users/usersGetAllResponse';
|
||||
import {AuthCreateUserResponse} from '~/business-logic/model/auth/authCreateUserResponse';
|
||||
import {v1 as uuidV1} from 'uuid';
|
||||
import {EMPTY, Observable, of, throwError, timer} from 'rxjs';
|
||||
import {EMPTY, Observable, of, timer} from 'rxjs';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {ConfirmDialogComponent} from '../ui-components/overlay/confirm-dialog/confirm-dialog.component';
|
||||
import {LoginModeResponse} from '~/business-logic/model/LoginModeResponse';
|
||||
@@ -40,6 +40,13 @@ export class BaseLoginService {
|
||||
private _loginMode: LoginMode;
|
||||
private _guestUser: { enabled: boolean; username: string; password: string };
|
||||
private environment: Environment;
|
||||
protected httpClient: HttpClient;
|
||||
protected loginApi: ApiLoginService;
|
||||
protected dialog: MatDialog;
|
||||
protected configService: ConfigurationService;
|
||||
protected store: Store;
|
||||
protected router: Router;
|
||||
protected userPreferences: UserPreferences;
|
||||
get guestUser() {
|
||||
return clone(this._guestUser);
|
||||
}
|
||||
@@ -48,16 +55,15 @@ export class BaseLoginService {
|
||||
return this._authenticated;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected httpClient: HttpClient,
|
||||
protected loginApi: ApiLoginService,
|
||||
protected dialog: MatDialog,
|
||||
protected configService: ConfigurationService,
|
||||
protected store: Store,
|
||||
protected router: Router,
|
||||
protected userPreferences: UserPreferences
|
||||
) {
|
||||
configService.globalEnvironmentObservable.subscribe(env => {
|
||||
constructor() {
|
||||
this.httpClient = inject(HttpClient);
|
||||
this.loginApi = inject(ApiLoginService);
|
||||
this.dialog = inject(MatDialog);
|
||||
this.configService = inject(ConfigurationService);
|
||||
this.store = inject(Store);
|
||||
this.router = inject(Router);
|
||||
this.userPreferences = inject(UserPreferences);
|
||||
this.configService.globalEnvironmentObservable.subscribe(env => {
|
||||
const firstLogin = !window.localStorage.getItem(USER_PREFERENCES_KEY.firstLogin);
|
||||
this.environment = env;
|
||||
this.signupMode = !!this.environment.communityServer && firstLogin;
|
||||
@@ -72,12 +78,10 @@ export class BaseLoginService {
|
||||
});
|
||||
|
||||
return this.getLoginMode().pipe(
|
||||
retryWhen(errors => errors.pipe(
|
||||
mergeMap((err, i) => i > 2 ? throwError(() => 'Error from retry!') : timer(500))
|
||||
)),
|
||||
catchError(() => {
|
||||
retry({count: 3, delay: (err, count) => timer(500 * count)}),
|
||||
catchError(err => {
|
||||
this.openServerError();
|
||||
return of({});
|
||||
throw err;
|
||||
}),
|
||||
switchMap(mode => mode === loginModes.simple ? this.httpClient.get('credentials.json') : of(fromEnv())),
|
||||
catchError(() => of(fromEnv())),
|
||||
@@ -167,12 +171,11 @@ export class BaseLoginService {
|
||||
.pipe(map((x: any) => x.data.id));
|
||||
}
|
||||
|
||||
autoLogin(name: string, callback: (res) => void) {
|
||||
autoLogin(name: string) {
|
||||
return this.createUser(name)
|
||||
.subscribe(id => this.login(id)
|
||||
.subscribe((res: any) => {
|
||||
callback(res);
|
||||
}));
|
||||
.pipe(
|
||||
switchMap(id => this.login(id)),
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -243,14 +246,6 @@ After the issue is resolved and Trains Server is up and running, reload this pag
|
||||
this._loginMode = undefined;
|
||||
}
|
||||
|
||||
afterLogin(resolve, store) {
|
||||
this.userPreferences.loadPreferences()
|
||||
.subscribe(() => {
|
||||
store.dispatch(fetchCurrentUser());
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
|
||||
loginFlow(resolve, skipInvite = false) {
|
||||
if (location.search.includes('invite') && !skipInvite) {
|
||||
const currentURL = new URL(location.href);
|
||||
@@ -266,10 +261,25 @@ After the issue is resolved and Trains Server is up and running, reload this pag
|
||||
) {
|
||||
if (this.guestUser?.enabled) {
|
||||
this.passwordLogin(this.guestUser.username, this.guestUser.password)
|
||||
.subscribe(() => this.afterLogin.bind(this)(resolve, this.store));
|
||||
.pipe(
|
||||
take(1),
|
||||
switchMap(() => this.userPreferences.loadPreferences())
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.store.dispatch(fetchCurrentUser());
|
||||
resolve(null);
|
||||
});
|
||||
} else if (ConfigurationService.globalEnvironment.autoLogin) {
|
||||
const name = `${(new Date()).getTime().toString()}`;
|
||||
this.autoLogin(name, this.afterLogin.bind(this, resolve, this.store));
|
||||
this.autoLogin(name)
|
||||
.pipe(
|
||||
take(1),
|
||||
switchMap(() => this.userPreferences.loadPreferences())
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.store.dispatch(fetchCurrentUser());
|
||||
resolve(null)
|
||||
});
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
@@ -293,6 +303,7 @@ After the issue is resolved and Trains Server is up and running, reload this pag
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getInviteInfo(inviteId: string): Observable<any> {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {addMessage} from '@common/core/actions/layout.actions';
|
||||
import {LocationStrategy} from '@angular/common';
|
||||
@@ -29,13 +29,16 @@ export interface ReportCodeEmbedConfiguration {
|
||||
export class ReportCodeEmbedBaseService {
|
||||
private workspace: GetCurrentUserResponseUserObjectCompany;
|
||||
private isCommunity: boolean;
|
||||
protected store: Store;
|
||||
protected locationStrategy: LocationStrategy;
|
||||
protected configService: ConfigurationService;
|
||||
protected _clipboardService: ClipboardService;
|
||||
|
||||
constructor(
|
||||
protected store: Store,
|
||||
protected locationStrategy: LocationStrategy,
|
||||
protected configService: ConfigurationService,
|
||||
protected _clipboardService: ClipboardService
|
||||
) {
|
||||
constructor() {
|
||||
this.store = inject(Store);
|
||||
this.locationStrategy = inject(LocationStrategy);
|
||||
this.configService = inject(ConfigurationService);
|
||||
this._clipboardService = inject(ClipboardService);
|
||||
this.isCommunity = this.configService.getStaticEnvironment().communityServer;
|
||||
this.store.select(selectActiveWorkspace).subscribe(workspace => this.workspace = workspace);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import {TableModule} from 'primeng/table';
|
||||
import {SectionHeaderComponent} from './components/section-header/section-header.component';
|
||||
import {LineChartComponent} from './components/charts/line-chart/line-chart.component';
|
||||
import {DonutComponent} from './components/charts/donut/donut.component';
|
||||
import {ExperimentRefreshComponent} from './components/experiment-refresh/experiment-refresh.component';
|
||||
import {CustomColumnsListComponent} from './components/custom-columns-list/custom-columns-list.component';
|
||||
import {BaseContextMenuComponent} from './components/base-context-menu/base-context-menu.component';
|
||||
import {EntityFooterComponent} from './entity-page/entity-footer/entity-footer.component';
|
||||
@@ -56,6 +55,7 @@ import {
|
||||
ExperimentTypeIconLabelComponent
|
||||
} from '@common/shared/experiment-type-icon-label/experiment-type-icon-label.component';
|
||||
import {LabeledFormFieldDirective} from '@common/shared/directive/labeled-form-field.directive';
|
||||
import {EllipsisMiddleDirective} from '@common/shared/ui-components/directives/ellipsis-middle.directive';
|
||||
|
||||
const _declarations = [
|
||||
ExperimentInfoHeaderStatusProgressBarComponent,
|
||||
@@ -68,7 +68,6 @@ const _declarations = [
|
||||
SectionHeaderComponent,
|
||||
LineChartComponent,
|
||||
DonutComponent,
|
||||
ExperimentRefreshComponent,
|
||||
CustomColumnsListComponent,
|
||||
EntityFooterComponent,
|
||||
CheckPermissionDirective,
|
||||
@@ -101,6 +100,7 @@ const _declarations = [
|
||||
LMarkdownEditorModule,
|
||||
SharedPipesModule,
|
||||
LabeledFormFieldDirective,
|
||||
EllipsisMiddleDirective,
|
||||
],
|
||||
declarations: [
|
||||
..._declarations,
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
<div class="modal-container" [class.dark-theme]="darkTheme" #modalContainer>
|
||||
<div class="graph-viewer-header">
|
||||
<div class="chart-title ellipsis"><span *ngIf="chart?.metric && !darkTheme">{{chart?.metric}} - </span>{{chart?.layout?.title || chart?.metric}}</div>
|
||||
<div class="chart-title ellipsis">
|
||||
<span #dot
|
||||
*ngIf="singleGraph && data.moveLegendToTitle && chart"
|
||||
[style.background-color]="singleGraph.chart?.data[0]?.line?.color"
|
||||
[colorButtonRef]="dot"
|
||||
[smChooseColor]="$any(singleGraph.chart?.data[0]?.line?.color)"
|
||||
[stringToColor]="singleGraph.singleColorKey"
|
||||
class="title-color">
|
||||
</span>
|
||||
<span *ngIf="chart?.metric && !darkTheme">{{chart?.metric}} - </span>{{chart?.variants?.length > 1 ? chart.variants.join(', ') : chart?.layout?.title || chart?.metric}}
|
||||
<sm-tag-list [tags]="chart?.tags"></sm-tag-list>
|
||||
</div>
|
||||
<input #fakeInput name="Don't remove - it take the autofocus from slider" style="opacity: 0; height: 0; width: 0">
|
||||
<div *ngIf="!darkTheme" class="viewer-iteration">
|
||||
<div *ngIf="minMaxIterations$ | async as minMaxIterations">
|
||||
@@ -70,6 +81,7 @@
|
||||
[disabled]="smoothType === smoothTypeEnum.any"
|
||||
[(ngModel)]="smoothWeight"
|
||||
(ngModelChange)="changeWeight($any($event))"
|
||||
(blur)="smoothWeight === null && changeWeight(-1)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
@@ -102,7 +114,7 @@
|
||||
[isCompare]="isCompare"
|
||||
[xAxisType]="xAxisType"
|
||||
[yAxisType]="yAxisType"
|
||||
[moveLegendToTitle]="false"
|
||||
[moveLegendToTitle]="data.moveLegendToTitle && !chart.layout.showlegend"
|
||||
[legendConfiguration]="this.data.legendConfiguration || {}"
|
||||
[isDarkTheme]="darkTheme"
|
||||
[graphsNumber]="9999"
|
||||
|
||||
@@ -62,6 +62,14 @@
|
||||
color: $blue-500;
|
||||
}
|
||||
|
||||
.title-color {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -21,6 +21,7 @@ import {getSignedUrl} from '@common/core/actions/common-auth.actions';
|
||||
import {selectSignedUrl} from '@common/core/reducers/common-auth-reducer';
|
||||
import {AxisType} from 'plotly.js';
|
||||
import {SmoothTypeEnum, smoothTypeEnum} from '@common/shared/single-graph/single-graph.utils';
|
||||
import {SingleGraphComponent} from "@common/shared/single-graph/single-graph.component";
|
||||
|
||||
export interface GraphViewerData {
|
||||
chart: ExtFrame;
|
||||
@@ -31,6 +32,7 @@ export interface GraphViewerData {
|
||||
smoothType?: SmoothTypeEnum;
|
||||
darkTheme: boolean;
|
||||
isCompare: boolean;
|
||||
moveLegendToTitle: boolean;
|
||||
embedFunction: (data: {xaxis: ScalarKeyEnum; domRect: DOMRect}) => null;
|
||||
legendConfiguration: Partial<ExtLegend & { noTextWrap: boolean }>;
|
||||
}
|
||||
@@ -41,7 +43,7 @@ export interface GraphViewerData {
|
||||
styleUrls: ['./graph-viewer.component.scss']
|
||||
})
|
||||
export class GraphViewerComponent implements AfterViewInit, OnInit, OnDestroy {
|
||||
@ViewChild('singleGraph') singleGraph;
|
||||
@ViewChild('singleGraph') singleGraph: SingleGraphComponent;
|
||||
@ViewChild('modalContainer') modalContainer;
|
||||
public height;
|
||||
public width;
|
||||
@@ -178,6 +180,10 @@ export class GraphViewerComponent implements AfterViewInit, OnInit, OnDestroy {
|
||||
parsingError && this.store.dispatch(addMessage('warn', `Couldn't read all plots. Please make sure all plots are properly formatted (NaN & Inf aren't supported).`, [], true));
|
||||
Object.values(graphs).forEach((graphss: ExtFrame[]) => {
|
||||
graphss.forEach((graph: ExtFrame) => {
|
||||
graph.data?.forEach((d, i) => d.visible = this.data.chart.data[i]?.visible)
|
||||
// if (this.data.chart?.layout?.showlegend === false) {
|
||||
graph.layout.showlegend = this.data.chart?.layout?.showlegend ?? false;
|
||||
// }
|
||||
if ((graph?.layout?.images?.length ?? 0) > 0) {
|
||||
graph.layout.images.forEach((image: Plotly.Image) => {
|
||||
this.store.dispatch(getSignedUrl({
|
||||
@@ -240,6 +246,7 @@ export class GraphViewerComponent implements AfterViewInit, OnInit, OnDestroy {
|
||||
this.singleGraph.shouldRefresh = true;
|
||||
}
|
||||
this.chart = cloneDeep(chart);
|
||||
console.log(this.chart?.data[0].line?.color);
|
||||
this.cdr.detectChanges();
|
||||
}));
|
||||
this.sub.add(this.iterationChanged$
|
||||
@@ -281,7 +288,7 @@ export class GraphViewerComponent implements AfterViewInit, OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
changeWeight(value: number) {
|
||||
if (value === 0) {
|
||||
if (value === 0 || value === null) {
|
||||
return;
|
||||
}
|
||||
if (value > (this.smoothType === smoothTypeEnum.exponential ? 0.999 : 100) || value < (this.smoothType === smoothTypeEnum.exponential ? 0 : 1)) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Component, Input, OnDestroy} from '@angular/core';
|
||||
import {Config, Frame, Layout, Legend, PlotData} from 'plotly.js';
|
||||
import {Component, inject, Input, OnDestroy} from '@angular/core';
|
||||
import plotly from 'plotly.js';
|
||||
import {selectScaleFactor} from '@common/core/reducers/view.reducer';
|
||||
import {Store} from '@ngrx/store';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
|
||||
export const DARK_THEME_GRAPH_LINES_COLOR = '#39405f';
|
||||
export const DARK_THEME_GRAPH_TICK_COLOR = '#c1cdf3';
|
||||
@@ -12,7 +12,7 @@ export interface VisibleExtFrame extends ExtFrame {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export interface ExtFrame extends Omit<Frame, 'data' | 'layout'> {
|
||||
export interface ExtFrame extends Omit<plotly.Frame, 'data' | 'layout'> {
|
||||
iter: number;
|
||||
metric: string;
|
||||
task: string;
|
||||
@@ -23,22 +23,23 @@ export interface ExtFrame extends Omit<Frame, 'data' | 'layout'> {
|
||||
worker: string;
|
||||
data: ExtData[];
|
||||
layout: Partial<ExtLayout>;
|
||||
config: Partial<Config>;
|
||||
config: Partial<plotly.Config>;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface ExtLegend extends Legend {
|
||||
export interface ExtLegend extends plotly.Legend {
|
||||
valign: 'top' | 'middle' | 'bottom';
|
||||
itemwidth: number;
|
||||
}
|
||||
|
||||
export interface ExtLayout extends Omit<Layout, 'legend'> {
|
||||
export interface ExtLayout extends Omit<plotly.Layout, 'legend'> {
|
||||
type: string;
|
||||
legend: Partial<ExtLegend>;
|
||||
uirevision: number | string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ExtData extends PlotData {
|
||||
export interface ExtData extends plotly.PlotData {
|
||||
task: string;
|
||||
cells: any;
|
||||
header: any;
|
||||
@@ -58,19 +59,21 @@ export abstract class PlotlyGraphBaseComponent implements OnDestroy {
|
||||
protected colorSub: Subscription;
|
||||
public isSmooth = false;
|
||||
public scaleFactor: number;
|
||||
protected store: Store;
|
||||
|
||||
@Input() isCompare: boolean = false;
|
||||
@Input() isCompare= false;
|
||||
|
||||
|
||||
protected constructor(protected store: Store) {
|
||||
this.sub.add(store.select(selectScaleFactor).subscribe(scaleFactor => this.scaleFactor = scaleFactor));
|
||||
protected constructor() {
|
||||
this.store = inject(Store);
|
||||
this.sub.add(this.store.select(selectScaleFactor).subscribe(scaleFactor => this.scaleFactor = scaleFactor));
|
||||
}
|
||||
|
||||
public _reColorTrace(trace: ExtData, newColor: number[]): void {
|
||||
if (Array.isArray(trace.line?.color) || Array.isArray(trace.marker?.color)) {
|
||||
return;
|
||||
}
|
||||
const colorString = tinycolor({r: newColor[0], g: newColor[1], b: newColor[2]})
|
||||
const colorString = new TinyColor({r: newColor[0], g: newColor[1], b: newColor[2]})
|
||||
.lighten((this.isSmooth && !trace.isSmoothed) ? 20 : 0).toRgbString();
|
||||
if (trace.marker) {
|
||||
trace.marker.color = colorString;
|
||||
@@ -104,7 +107,7 @@ export abstract class PlotlyGraphBaseComponent implements OnDestroy {
|
||||
continue;
|
||||
}
|
||||
const name = data[i].name;
|
||||
if (namesHash[name]) {
|
||||
if (namesHash[name] && name !== data[i].legendgroup) {
|
||||
namesHash[name].push(i);
|
||||
} else {
|
||||
namesHash[name] = [i];
|
||||
@@ -118,7 +121,7 @@ export abstract class PlotlyGraphBaseComponent implements OnDestroy {
|
||||
data[key].colorHash = data[key].name;
|
||||
// Warning: "data[key].task" in compare case. taskId in subplots (multiple plots with same name)
|
||||
if (data[key].task || taskId) {
|
||||
data[key].name = `${data[key].name}.${(data[key].task || taskId).substring(0, 7)}`;
|
||||
data[key].name = `${data[key].name}.${(data[key].task || taskId).substring(0, 6)}`;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
|
||||
@@ -3,10 +3,21 @@
|
||||
[class.whitebg]="!isDarkTheme"
|
||||
[class.dark-theme]="isDarkTheme"
|
||||
[class.loading]="loading"
|
||||
[class.whitebg-table]="type === 'table'"
|
||||
[class.move-title]="type !== 'parcoords'"
|
||||
[class.whitebg-table]="type?.[0] === 'table'"
|
||||
[class.move-title]="type?.[0] !== 'parcoords'"
|
||||
(scroll)="repositionModeBar($event.target)"
|
||||
>
|
||||
<div #graphTitle class="graph-title ellipsis" [class.table-title]="type === 'table'" [title]="graphTitle.scrollWidth <= graphTitle.clientWidth ? '' : title" (mouseenter)="changeDetector.detectChanges()">{{title}}</div>
|
||||
<mat-spinner [diameter]="50" *ngIf="loading" class="plot-loader"></mat-spinner>
|
||||
<div class="graph-title" [class.table-title]="type?.[0] === 'table'">
|
||||
<span #dot
|
||||
*ngIf="moveLegendToTitle && chart && title"
|
||||
[style.background-color]="chart?.data[0].line?.color"
|
||||
[colorButtonRef]="dot"
|
||||
[smChooseColor]="$any(chart?.data[0].line?.color)"
|
||||
[stringToColor]="singleColorKey"
|
||||
class="title-color">
|
||||
</span>
|
||||
<div #graphTitle class="ellipsis" [title]="graphTitle.scrollWidth <= graphTitle.clientWidth ? '' : title" (mouseenter)="changeDetector.detectChanges()" data-id="graphTitleName">{{title}}</div>
|
||||
<sm-tag-list *ngIf="title" [tags]="chart.tags?.slice(0,3)"></sm-tag-list>
|
||||
</div>
|
||||
<mat-spinner [diameter]="32" [strokeWidth]="4" color="accent" *ngIf="loading" class="plot-loader"></mat-spinner>
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
|
||||
.graph-title {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
top: 36px;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 0 80px;
|
||||
padding: 0 60px;
|
||||
z-index: 1;
|
||||
font-size: 16px;
|
||||
color: $blue-600;
|
||||
@@ -16,6 +19,19 @@
|
||||
&.table-title {
|
||||
top: 20px
|
||||
}
|
||||
|
||||
.title-color {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex: 0 0 10px;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
sm-tag-list {
|
||||
flex-shrink: 2;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.dark-theme .graph-title {
|
||||
|
||||
@@ -12,26 +12,10 @@ import {
|
||||
} from '@angular/core';
|
||||
import {select} from 'd3-selection';
|
||||
import {cloneDeep, escape} from 'lodash-es';
|
||||
import {Store} from '@ngrx/store';
|
||||
import {
|
||||
AxisType,
|
||||
Config,
|
||||
Data,
|
||||
Datum,
|
||||
Margin,
|
||||
ModeBarButton,
|
||||
ModeBarDefaultButtons,
|
||||
PlotData,
|
||||
PlotlyHTMLElement,
|
||||
PlotMarker,
|
||||
PlotType,
|
||||
Root,
|
||||
deleteTraces,
|
||||
react
|
||||
} from 'plotly.js';
|
||||
import plotly from 'plotly.js';
|
||||
import {Subject} from 'rxjs';
|
||||
import {debounceTime, filter} from 'rxjs/operators';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import {TinyColor} from '@ctrl/tinycolor';
|
||||
import {ScalarKeyEnum} from '~/business-logic/model/events/scalarKeyEnum';
|
||||
import {ColorHashService} from '@common/shared/services/color-hash/color-hash.service';
|
||||
import {wordWrap} from '@common/tasks/tasks.utils';
|
||||
@@ -57,10 +41,10 @@ export type ChartHoverModeEnum = 'x' | 'y' | 'closest' | false | 'x unified' | '
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
public alreadyDrawn: boolean = false;
|
||||
public shouldRefresh: boolean = false;
|
||||
public alreadyDrawn = false;
|
||||
public shouldRefresh = false;
|
||||
public loading: boolean;
|
||||
public type: PlotData['type'] | 'table';
|
||||
public type: Array<plotly.PlotData['type'] | 'table'>;
|
||||
public ratio: number;
|
||||
public title: string;
|
||||
private originalChart: ExtFrame;
|
||||
@@ -74,16 +58,18 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
private _height: number;
|
||||
private _hoverMode: ChartHoverModeEnum;
|
||||
private listeningToHoverMode = false;
|
||||
public singleColorKey: string;
|
||||
private drawGraph$ = new Subject<{ forceRedraw?: boolean; forceSkipReact?: boolean }>();
|
||||
|
||||
private drawGraph$ = new Subject<{forceRedraw?: boolean; forceSkipReact?: boolean}>();
|
||||
@Input() identifier: string;
|
||||
@Input() hideTitle: boolean = false;
|
||||
@Input() hideLegend: boolean = false;
|
||||
@Input() hideTitle = false;
|
||||
@Input() hideLegend = false;
|
||||
@Input() isDarkTheme: boolean;
|
||||
@Input() showLoaderOnDraw = true;
|
||||
@Input() hideMaximize: 'show' | 'hide' | 'disabled' = 'show';
|
||||
@Input() legendConfiguration: Partial<ExtLegend & { noTextWrap: boolean }> = {};
|
||||
@Input() yAxisType: AxisType = 'linear';
|
||||
@Input() yAxisType: plotly.AxisType = 'linear';
|
||||
private previousHoverMode: "x" | "y" | "closest" | false | "x unified" | "y unified";
|
||||
|
||||
@Input() set height(height: number) {
|
||||
this._height = height;
|
||||
@@ -98,7 +84,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
|
||||
@Input() set width(width: number) {
|
||||
if (this.chart) {
|
||||
this.drawGraph$.next({forceRedraw: false, forceSkipReact: false});
|
||||
this.drawGraph$.next({forceRedraw: true, forceSkipReact: false});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,12 +112,12 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
.filter(d => !d.isSmoothed)
|
||||
.map((d, i) => d.visible === 'legendonly' ? i : null)
|
||||
.filter(index => index !== null);
|
||||
if (clean && this.alreadyDrawn && this.type === 'scatter' && chart.data[0]?.x?.length === 1) {
|
||||
(Plotly.deleteTraces as typeof deleteTraces)(this.chartElm, Array.from({length: this.chartElm.data.length ?? 0}, (v, i) => i));
|
||||
if (clean && this.alreadyDrawn && this.type[0] === 'scatter' && chart.data[0]?.x?.length === 1) {
|
||||
(Plotly.deleteTraces as typeof plotly.deleteTraces)(this.chartElm, Array.from({length: this.chartElm.data.length ?? 0}, (v, i) => i));
|
||||
this.modeBar = null;
|
||||
}
|
||||
this._chart = cloneDeep(chart);
|
||||
if (hidden) {
|
||||
if (hidden?.length > 0) {
|
||||
this._chart.data.forEach((d, i) => d.visible = hidden.includes(i) ? 'legendonly': true);
|
||||
}
|
||||
}
|
||||
@@ -143,6 +129,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
|
||||
@Input() set hoverMode(hoverMode) {
|
||||
this._hoverMode = hoverMode;
|
||||
this.previousHoverMode = this.previousHoverMode ?? hoverMode;
|
||||
if (this.chart) {
|
||||
this.drawGraph$.next({forceRedraw: true, forceSkipReact: false});
|
||||
}
|
||||
@@ -180,7 +167,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
@Input() hideDownloadButtons = false;
|
||||
@Input() noMargins = false;
|
||||
@Output() hoverModeChanged = new EventEmitter<ChartHoverModeEnum>();
|
||||
@Output() createEmbedCode = new EventEmitter<{xaxis: ScalarKeyEnum; domRect: DOMRect}>();
|
||||
@Output() createEmbedCode = new EventEmitter<{ xaxis: ScalarKeyEnum; domRect: DOMRect }>();
|
||||
@Output() maximizeClicked = new EventEmitter();
|
||||
@ViewChild('drawHere', {static: true}) plotlyContainer: ElementRef;
|
||||
private chartElm;
|
||||
@@ -192,11 +179,10 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
protected renderer: Renderer2,
|
||||
private colorHash: ColorHashService,
|
||||
public changeDetector: ChangeDetectorRef,
|
||||
protected store: Store,
|
||||
private dialog: MatDialog,
|
||||
private readonly zone: NgZone
|
||||
) {
|
||||
super(store);
|
||||
super();
|
||||
this.initColorSubscription();
|
||||
|
||||
this.sub.add(this.smooth$
|
||||
@@ -213,7 +199,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
.pipe(
|
||||
debounceTime(100),
|
||||
filter(() => !!this.chart)
|
||||
)
|
||||
)
|
||||
.subscribe(({forceRedraw, forceSkipReact}) => {
|
||||
if (this.showLoaderOnDraw) {
|
||||
this.loading = true;
|
||||
@@ -242,7 +228,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
this.loading = false;
|
||||
this.changeDetector.detectChanges();
|
||||
this.updateLegend();
|
||||
if ((data[0] as any)?.mode === 'gauge+number') {
|
||||
if ((data[0] as ExtData)?.mode === 'gauge+number') {
|
||||
this.fixGaugeValuePositionAfterResize();
|
||||
}
|
||||
}));
|
||||
@@ -251,7 +237,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
this.previousHeight = this.height;
|
||||
|
||||
if (!skipReact) {
|
||||
this.zone.runOutsideAngular(() => (Plotly.react as typeof react)(root, data, layout, config).then(() => {
|
||||
this.zone.runOutsideAngular(() => (Plotly.react as typeof plotly.react)(root, data, layout, config).then(() => {
|
||||
this.loading = false;
|
||||
this.changeDetector.detectChanges();
|
||||
}));
|
||||
@@ -277,18 +263,19 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
if (activeBtns.length > 0) {
|
||||
activeBtns.forEach((bt => {
|
||||
if (bt.attributes['data-attr'].value === 'hovermode') {
|
||||
if (bt.attributes['data-val'].value !== this.hoverMode) {
|
||||
if (bt.attributes['data-val'].value !== this.hoverMode && this.previousHoverMode === this.hoverMode) {
|
||||
this._hoverMode = bt.attributes['data-val'].value;
|
||||
this.hoverModeChanged.emit(bt.attributes['data-val'].value);
|
||||
}
|
||||
}
|
||||
}));
|
||||
this.previousHoverMode = this.hoverMode;
|
||||
}
|
||||
});
|
||||
this.listeningToHoverMode = true;
|
||||
}
|
||||
|
||||
drawPlotly(): [Root, Data[], Partial<ExtLayout>, Partial<Config>, Element] {
|
||||
drawPlotly(): [plotly.Root, plotly.Data[], Partial<ExtLayout>, Partial<plotly.Config>, Element] {
|
||||
this.chartData = this.chartData || this.renderer.createElement('div');
|
||||
this.chartElm = this.chartElm || this.renderer.createElement('div');
|
||||
this.chartElm.classList.add('chart');
|
||||
@@ -297,7 +284,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
}
|
||||
|
||||
const graph = this.formatChartLines() as ExtFrame;
|
||||
this.type = (graph?.data?.[0]?.type ?? graph?.layout?.type) as PlotType;
|
||||
this.type = (graph?.data?.map(dat => dat?.type ?? graph?.layout?.type)) as plotly.PlotType[];
|
||||
this.title = (this.isDarkTheme || this.hideTitle) ? '' : this.getTitle(graph);
|
||||
let layout = {
|
||||
...this.hideDownloadButtons ? {} : this.addParametersIfDarkTheme({
|
||||
@@ -313,7 +300,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
...graph.layout,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
...this.addParametersIfDarkTheme({plot_bgcolor: 'transparent'}),
|
||||
height: this.type === 'table' ? this.height - 20 : this.height,
|
||||
height: this.type[0] === 'table' ? this.height - 20 : this.height,
|
||||
width: this.ratio ? (this.height * this.ratio) + RATIO_OFFSET_FIX : undefined,
|
||||
modebar: {
|
||||
color: '#5a658e',
|
||||
@@ -377,7 +364,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
...this.legendConfiguration,
|
||||
...graph.layout.legend,
|
||||
},
|
||||
showlegend: !this.hideLegend && (this.chartElm.layout && Object.prototype.hasOwnProperty.call(this.chartElm.layout, 'showlegend') ? this.chartElm.layout.showlegend : graph.layout?.showlegend !== false),
|
||||
showlegend: !this.moveLegendToTitle && !this.hideLegend && (this.chartElm.layout && Object.prototype.hasOwnProperty.call(this.chartElm.layout, 'showlegend') ? this.chartElm.layout.showlegend : graph.layout?.showlegend !== false),
|
||||
margin: graph.layout.margin ? graph.layout.margin : this.noMargins ? {
|
||||
l: 50,
|
||||
r: 50,
|
||||
@@ -392,53 +379,55 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
b: 90,
|
||||
pad: 0,
|
||||
autoexpand: true
|
||||
} as Partial<Margin>,
|
||||
} as Partial<plotly.Margin>,
|
||||
} as Partial<ExtLayout>;
|
||||
|
||||
if (this.type === 'table') {
|
||||
this.changeDetector.detectChanges();
|
||||
// override header design
|
||||
graph.data[0].header = {
|
||||
...graph.data[0].header,
|
||||
line: {width: 1, color: this.isDarkTheme ? '#5a658e' : '#d4d6e0'},
|
||||
height: 29,
|
||||
align: 'left',
|
||||
font: {
|
||||
color: [this.isDarkTheme ? PALLET.blue200 : PALLET.blue400],
|
||||
size: 12
|
||||
},
|
||||
fill: {...graph.data[0].header?.fill, color: this.isDarkTheme ? PALLET.blue800 : PALLET.blue50}
|
||||
};
|
||||
graph.data.forEach(data => {
|
||||
if (data.type === 'table') {
|
||||
this.changeDetector.detectChanges();
|
||||
// override header design
|
||||
data.header = {
|
||||
...data.header,
|
||||
line: {width: 1, color: this.isDarkTheme ? '#5a658e' : '#d4d6e0'},
|
||||
height: 29,
|
||||
align: 'left',
|
||||
font: {
|
||||
color: [this.isDarkTheme ? PALLET.blue200 : PALLET.blue400],
|
||||
size: 12
|
||||
},
|
||||
fill: {...data.header?.fill, color: this.isDarkTheme ? PALLET.blue800 : PALLET.blue50}
|
||||
};
|
||||
|
||||
// override cells design
|
||||
const isFireFox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
graph.data[0].cells = {
|
||||
...graph.data[0].cells,
|
||||
align: 'left',
|
||||
height: (isFireFox && this.isDarkTheme) ? 'auto' : 30,
|
||||
font: {
|
||||
color: [this.isDarkTheme ? PALLET.blue200 : PALLET.blue400],
|
||||
size: 12
|
||||
},
|
||||
fill: {...graph.data[0].cells, color: this.isDarkTheme ? PALLET.blue950 : '#ffffff'},
|
||||
line: {width: 1, color: this.isDarkTheme ? DARK_THEME_GRAPH_LINES_COLOR : PALLET.blue100}
|
||||
};
|
||||
layout.width = Math.max((graph?.data?.[0]?.cells?.values?.length ?? 0 ) * 100, this.plotlyContainer.nativeElement.offsetWidth - 3);
|
||||
layout.title = {
|
||||
...layout.title as Record<string, any>,
|
||||
xanchor: 'left',
|
||||
xref: 'paper',
|
||||
x: 0
|
||||
};
|
||||
layout.margin = {
|
||||
l: 24,
|
||||
t: 52,
|
||||
r: 24,
|
||||
b: 0
|
||||
};
|
||||
}
|
||||
// override cells design
|
||||
const isFireFox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
data.cells = {
|
||||
...data.cells,
|
||||
align: 'left',
|
||||
height: (isFireFox && this.isDarkTheme) ? 'auto' : 30,
|
||||
font: {
|
||||
color: [this.isDarkTheme ? PALLET.blue200 : PALLET.blue400],
|
||||
size: 12
|
||||
},
|
||||
fill: {...data.cells, color: this.isDarkTheme ? PALLET.blue950 : '#ffffff'},
|
||||
line: {width: 1, color: this.isDarkTheme ? DARK_THEME_GRAPH_LINES_COLOR : PALLET.blue100}
|
||||
};
|
||||
layout.width = Math.max((data?.cells?.values?.length ?? 0) * 100, this.plotlyContainer.nativeElement.offsetWidth - 3);
|
||||
layout.title = {
|
||||
...layout.title as Record<string, any>,
|
||||
xanchor: 'left',
|
||||
xref: 'paper',
|
||||
x: 0
|
||||
};
|
||||
layout.margin = {
|
||||
l: 24,
|
||||
t: 52,
|
||||
r: 24,
|
||||
b: 0
|
||||
};
|
||||
}
|
||||
})
|
||||
const barLayoutConfig = {
|
||||
hovermode: 'closest',
|
||||
hovermode: this.hoverMode ?? 'closest',
|
||||
};
|
||||
|
||||
const scatterLayoutConfig: Partial<ExtLayout> = {
|
||||
@@ -494,22 +483,22 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
};
|
||||
|
||||
if (['multiScalar', 'scalar'].includes(graph.layout.type)) {
|
||||
if (['scatter', 'scatter3d'].includes(this.type)) {
|
||||
if (this.type.includes('scatter') || this.type.includes('scatter3d')) {
|
||||
layout = {hovermode: this.hoverMode, ...layout, ...scatterLayoutConfig} as Partial<ExtLayout>;
|
||||
}
|
||||
}
|
||||
if (['bar'].includes(this.type)) {
|
||||
if (this.type.includes('bar')) {
|
||||
layout = {...layout, ...barLayoutConfig} as Partial<ExtLayout>;
|
||||
}
|
||||
|
||||
const modeBarButtonsToAdd = ['v1hovermode', 'togglespikelines'] as undefined as ModeBarButton[];
|
||||
const modeBarButtonsToAdd = ['v1hovermode', 'togglespikelines'] as undefined as plotly.ModeBarButton[];
|
||||
|
||||
if (['multiScalar', 'scalar'].includes(graph.layout.type)) {
|
||||
modeBarButtonsToAdd.push({
|
||||
name: 'Log view',
|
||||
title: this.getLogButtonTitle(this.yAxisType === 'log'),
|
||||
icon: this.getLogIcon(this.yAxisType === 'log'),
|
||||
click: (gd: PlotlyHTMLElement, ev: MouseEvent) => {
|
||||
click: (gd: plotly.PlotlyHTMLElement, ev: MouseEvent) => {
|
||||
this.yAxisType = this.yAxisType === 'log' ? 'linear' : 'log';
|
||||
const icon = this.getLogIcon(this.yAxisType === 'log');
|
||||
let path: SVGPathElement;
|
||||
@@ -573,7 +562,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.type === 'table') {
|
||||
if (this.type[0] === 'table') {
|
||||
modeBarButtonsToAdd.push({
|
||||
name: 'Download CSV',
|
||||
title: 'Download CSV',
|
||||
@@ -585,7 +574,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
}
|
||||
|
||||
if (this.exportForReport) {
|
||||
const button: ModeBarButton = {
|
||||
const button: plotly.ModeBarButton = {
|
||||
name: 'Embed',
|
||||
title: 'Copy embed code',
|
||||
attr: 'plotly-embedded-modebar-button',
|
||||
@@ -598,7 +587,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
}
|
||||
|
||||
if (this.hideMaximize !== 'hide') {
|
||||
const maximizeButton: ModeBarButton = {
|
||||
const maximizeButton: plotly.ModeBarButton = {
|
||||
name: 'Maximize',
|
||||
title: this.hideMaximize === 'disabled' ? `Can't maximize because an iframe with the same name exists` : 'Maximize Graph',
|
||||
attr: this.hideMaximize === 'disabled' ? 'plotly-disabled-maximize' : '',
|
||||
@@ -611,14 +600,14 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
}
|
||||
|
||||
const config = {
|
||||
modeBarButtonsToRemove: (this.hideDownloadButtons ? ['sendDataToCloud', 'toImage']: ['sendDataToCloud']) as ModeBarDefaultButtons[],
|
||||
modeBarButtonsToRemove: (this.hideDownloadButtons ? ['sendDataToCloud', 'toImage'] : ['sendDataToCloud']) as plotly.ModeBarDefaultButtons[],
|
||||
displaylogo: false,
|
||||
modeBarButtonsToAdd,
|
||||
toImageButtonOptions : {
|
||||
toImageButtonOptions: {
|
||||
filename: this.getExportName(this.chart),
|
||||
scale: 3
|
||||
}
|
||||
} as Config;
|
||||
} as plotly.Config;
|
||||
|
||||
return [this.chartElm, graph.data, layout, config, this.chartData];
|
||||
}
|
||||
@@ -632,16 +621,16 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
}
|
||||
|
||||
private getExportName(graph: ExtFrame) {
|
||||
const title = this.getTitle(graph);
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
const metric = this.chart.metric?.trim();
|
||||
const variant = this.chart.variant?.trim();
|
||||
if (metric && variant) {
|
||||
return `${metric} - ${variant}`;
|
||||
}
|
||||
return `${metric || variant}` || 'chart';
|
||||
const title = this.getTitle(graph);
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
const metric = this.chart.metric?.trim();
|
||||
const variant = this.chart.variant?.trim();
|
||||
if (metric && variant) {
|
||||
return `${metric} - ${variant}`;
|
||||
}
|
||||
return (metric || variant) ? `${metric || variant}` : 'chart';
|
||||
}
|
||||
|
||||
private updateLegend() {
|
||||
@@ -677,24 +666,25 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
|
||||
if (this.xAxisType === ScalarKeyEnum.Timestamp) {
|
||||
const zeroTime = graph.data[i].x[0] as number;
|
||||
graph.data[i].x = (graph.data[i].x as Datum[]).map((timestamp: number) => (timestamp - zeroTime) / timeUnit.time);
|
||||
graph.data[i].x = (graph.data[i].x as plotly.Datum[]).map((timestamp: number) => (timestamp - zeroTime) / timeUnit.time);
|
||||
// graph.data[i].hovertext = graph.data[i].x.map(timestamp => timeInWords((timestamp - zeroTime)));
|
||||
}
|
||||
if (graph.data[i].type === 'bar' && !graph.data[i].marker) {
|
||||
graph.data[i].marker = {} as Partial<PlotMarker>;
|
||||
graph.data[i].marker = {} as Partial<plotly.PlotMarker>;
|
||||
}
|
||||
const genColorKey = this.generateColorKey(graph, i);
|
||||
this.singleColorKey = genColorKey;
|
||||
const wrappedText = this.legendConfiguration.noTextWrap || this.scaleFactor === 100 ?
|
||||
graph.data[i].name :
|
||||
wordWrap(graph.data[i].name, this.legendStringLength / 2);
|
||||
const orgColor = this.getOriginalColor(i);
|
||||
const orgColor = this.isCompare ? undefined : this.getOriginalColor(i);
|
||||
graph.data[i].name = wrappedText + `<span style="display: none;" class="color-key" data-color-key="${genColorKey}" data-origin-color="${orgColor}"></span>`;
|
||||
}
|
||||
const [colorKey, originalColor] = this.extractColorKey(graph.data[i].name);
|
||||
if (!Array.isArray(originalColor)) {
|
||||
let color;
|
||||
if (originalColor && !this.colorHash.hasColor(colorKey)) {
|
||||
const c = tinycolor(this.getOriginalColor(i)).toRgb();
|
||||
const c = new TinyColor(this.getOriginalColor(i)).toRgb();
|
||||
color = [c.r, c.g, c.b];
|
||||
} else {
|
||||
color = this.colorHash.initColor(colorKey, null, this.isDarkTheme);
|
||||
@@ -721,11 +711,11 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
}
|
||||
|
||||
public generateColorKey(graph: ExtFrame, i: number) {
|
||||
const variant = graph.data[i].colorHash || graph.data[i].colorKey || graph.data[i].name;
|
||||
const variant = graph.data[i].colorHash || graph.data[i].colorKey || graph.data[i].name;
|
||||
if (!this.isCompare) {
|
||||
return `${variant}?`; // "?" to adjust desired colors (legend title is removing this ?)
|
||||
} else {
|
||||
const task = graph.data[i].task;
|
||||
const task = graph.data[i].task ?? graph.task;
|
||||
return `${variant}-${task}`;
|
||||
}
|
||||
}
|
||||
@@ -761,10 +751,10 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
)
|
||||
.subscribe(colorObj => {
|
||||
const graph = this.chart;
|
||||
let changed: boolean = false;
|
||||
let changed = false;
|
||||
graph?.data.forEach(trace => {
|
||||
const name = trace.name;
|
||||
const [colorKey, ] = this.extractColorKey(name);
|
||||
const [colorKey,] = this.extractColorKey(name);
|
||||
if (!name || !this.colorHash.hasColor(colorKey)) {
|
||||
return;
|
||||
}
|
||||
@@ -786,26 +776,16 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
|
||||
private subscribeColorButtons(container) {
|
||||
this.repositionModeBar(this.plotlyContainer.nativeElement);
|
||||
if (this.moveLegendToTitle) {
|
||||
const graphTitle = container.querySelector('.gtitle') as SVGTextElement;
|
||||
if (graphTitle) {
|
||||
const endOfTitlePosition = (
|
||||
(this.plotlyContainer.nativeElement.offsetWidth / 2) +
|
||||
(graphTitle.getClientRects()[0].width * this.scaleFactor / 200)
|
||||
);
|
||||
const legend = container.querySelector('.legend') as SVGGElement;
|
||||
legend.style.transform = `translate(${endOfTitlePosition}px, 30px)`;
|
||||
legend.classList.add('hide-text');
|
||||
}
|
||||
}
|
||||
const traces = container.querySelectorAll('.traces');
|
||||
for (const trace of traces) {
|
||||
let originalColor = null
|
||||
const textEl = trace.querySelector('.legendtext') as SVGTextElement;
|
||||
const textElData = textEl.getAttribute('data-unformatted');
|
||||
const [text, orgColor] = textEl ? this.extractColorKey(textElData) : ['', ''];
|
||||
const {r, g, b, a} = tinycolor(orgColor).toRgb();
|
||||
const originalColor = orgColor ? [r, g, b, a] : null;
|
||||
|
||||
if (orgColor) {
|
||||
const {r, g, b, a} = new TinyColor(orgColor).toRgb();
|
||||
originalColor = orgColor ? [r, g, b, a] : null;
|
||||
}
|
||||
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
||||
title.textContent = text.replace('?', '');
|
||||
textEl.parentElement.appendChild(title);
|
||||
@@ -962,7 +942,8 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
// signed url are updated after originChart was cloned - need to update images urls!
|
||||
chart: cloneDeep({
|
||||
...this.originalChart,
|
||||
layout: {...this.originalChart.layout, images: this.chart.layout?.images}
|
||||
data: this.originalChart.data.map((d, i) => ({...d, visible: this._chart.data[i].visible})),
|
||||
layout: {...this.originalChart.layout, showlegend: this.moveLegendToTitle || this.chartElm.layout.showlegend, images: this.chart.layout?.images}
|
||||
}),
|
||||
id: this.identifier,
|
||||
xAxisType: this.xAxisType,
|
||||
@@ -971,6 +952,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
smoothType: this.smoothType,
|
||||
darkTheme: this.isDarkTheme,
|
||||
isCompare: this.isCompare,
|
||||
moveLegendToTitle: this.moveLegendToTitle,
|
||||
legendConfiguration: this.legendConfiguration
|
||||
} as GraphViewerData,
|
||||
panelClass: ['image-viewer-dialog', this.isDarkTheme ? 'dark-theme' : 'light-theme'],
|
||||
@@ -994,7 +976,7 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
}
|
||||
|
||||
public repositionModeBar(singleGraphEl) {
|
||||
if (this.type === 'table') {
|
||||
if (this.type[0] === 'table') {
|
||||
this.modeBar = this.modeBar || this.chartElm.querySelector('.modebar-container');
|
||||
this.modeBar.style.right = `${singleGraphEl.scrollWidth - singleGraphEl.clientWidth - singleGraphEl.scrollLeft}px`;
|
||||
}
|
||||
@@ -1019,11 +1001,15 @@ export class SingleGraphComponent extends PlotlyGraphBaseComponent {
|
||||
|
||||
public redrawPlot() {
|
||||
this.drawGraph$.next({forceRedraw: true, forceSkipReact: false});
|
||||
};
|
||||
}
|
||||
|
||||
private fixGaugeValuePositionAfterResize() {
|
||||
const graph = select(this.plotlyContainer.nativeElement).selectAll('.main-svg .indicatorlayer .trace');
|
||||
const graphPosition = graph.selectAll('.angular').attr('transform');
|
||||
graph.selectAll('.numbers').attr('transform', graphPosition);
|
||||
select(this.plotlyContainer.nativeElement)
|
||||
.selectAll('.main-svg .indicatorlayer .trace')
|
||||
.each((datum, index, group) => {
|
||||
const node = select(group[index]);
|
||||
const graphPosition = node.selectAll('.angular').attr('transform');
|
||||
node.selectAll('.numbers').attr('transform', graphPosition);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {FormsModule} from '@angular/forms';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {TooltipDirective} from '@common/shared/ui-components/indicators/tooltip/tooltip.directive';
|
||||
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||
import {ChooseColorModule} from '@common/shared/ui-components/directives/choose-color/choose-color.module';
|
||||
import {TagListComponent} from "@common/shared/ui-components/tags/tag-list/tag-list.component";
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +30,8 @@ import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||
MatInputModule,
|
||||
TooltipDirective,
|
||||
MatProgressSpinnerModule,
|
||||
ChooseColorModule,
|
||||
TagListComponent,
|
||||
]
|
||||
})
|
||||
export class SingleGraphModule { }
|
||||
|
||||
@@ -34,4 +34,5 @@ export const getSmoothedLine = (arr, weight, smoothType): number[] => {
|
||||
case smoothTypeEnum.exponential:
|
||||
return averageDebiased(arr, weight);
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<div class="single-table-container" [class.dark-theme]="darkTheme">
|
||||
<div class="summary-header position-relative">Summary</div>
|
||||
|
||||
<div class="d-flex items-container">
|
||||
<div class="wrapper"></div>
|
||||
<div *ngFor="let item of data" class="item">
|
||||
<div class="variant">{{item.variant}}</div>
|
||||
<div class="value">{{item.value}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<i class="al-icon al-ico-csv pointer al-color blue-300" (click)="downloadTableAsCSV()"></i>
|
||||
<i *ngIf="createEmbedCode.observed" class="al-icon al-ico-code sm-md clickable-icon" (click)="createEmbedCodeClicked($event)"></i>
|
||||
<i class="al-icon al-ico-csv pointer al-color blue-300" data-id="downloadCSVButton" (click)="downloadTableAsCSV()"></i>
|
||||
<span class="csv-no-icon" (click)="downloadTableAsCSV()">CSV</span>
|
||||
<i *ngIf="createEmbedCode.observed" class="al-icon al-ico-code sm-md clickable-icon" data-id="copyEmbedCodeButton" (click)="createEmbedCodeClicked($event)"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,25 +2,40 @@
|
||||
|
||||
|
||||
.single-table-container {
|
||||
border-radius: 4px;
|
||||
border: solid 1px $blue-100;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.summary-header {
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
max-height: 48px;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
color: $blue-600;
|
||||
text-align: center;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border: 1px solid $blue-100;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
.items-container {
|
||||
border-top: solid 1px $blue-100;
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
left: 0;
|
||||
border-radius: 0 0 4px 4px;
|
||||
border: 1px solid $blue-100;
|
||||
width: 100%;
|
||||
height: calc(100% - 62px);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 12px 24px;
|
||||
text-align: center;
|
||||
margin-bottom: 2px;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: solid 1px $blue-100;
|
||||
@@ -36,6 +51,53 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.csv-no-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.dark-theme {
|
||||
|
||||
.summary-header {
|
||||
color: $blue-300;
|
||||
border-color: $dark-border;
|
||||
}
|
||||
|
||||
.items-container {
|
||||
.wrapper {
|
||||
border-color: $dark-border;
|
||||
}
|
||||
|
||||
.item {
|
||||
&:not(:last-child) {
|
||||
border-color: $dark-border;
|
||||
}
|
||||
|
||||
.variant {
|
||||
color: $blue-300;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: $blue-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.csv-no-icon {
|
||||
display: block;
|
||||
background-color: $blue-300;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
padding: 6px 2px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.al-ico-csv {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host {
|
||||
@@ -70,30 +132,15 @@
|
||||
}
|
||||
|
||||
/////// DARK THEME //////////
|
||||
.dark-theme {
|
||||
&.single-table-container {
|
||||
border: solid 1px $dark-border;
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
color: $blue-300;
|
||||
}
|
||||
|
||||
.items-container {
|
||||
border-top: solid 1px $dark-border;
|
||||
|
||||
@supports (-moz-appearance:none) {
|
||||
.single-table-container .items-container {
|
||||
.item {
|
||||
&:not(:last-child) {
|
||||
border-right: solid 1px $dark-border;
|
||||
}
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.variant {
|
||||
color: $blue-300;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: $blue-100;
|
||||
}
|
||||
.wrapper {
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,14 @@ export class ChipsListComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.calcChipsWidth(this.chips);
|
||||
this.sub.add(this.chips.changes.subscribe((chips) => {
|
||||
this.calcChipsWidth(chips);
|
||||
setTimeout(() => {
|
||||
this.calcChipsWidth(this.chips);
|
||||
this.sub.add(this.chips.changes.subscribe((chips) => {
|
||||
this.calcChipsWidth(chips);
|
||||
this.hideChips();
|
||||
}));
|
||||
this.hideChips();
|
||||
}));
|
||||
this.hideChips();
|
||||
}, 100)
|
||||
}
|
||||
|
||||
hideChips() {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {FORCED_COLORS_FOR_STRING} from '../../../services/color-hash/color-hash-
|
||||
import {Subscription} from 'rxjs';
|
||||
import {getCssTheme} from '../../../utils/shared-utils';
|
||||
import {invertRgb} from '../../../services/color-hash/color-hash.utils';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { TinyColor, mostReadable } from '@ctrl/tinycolor';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-chips',
|
||||
@@ -19,7 +19,7 @@ export class ChipsComponent implements OnInit, OnDestroy {
|
||||
public backgroundColor: string;
|
||||
public colorIsForced: boolean = false;
|
||||
public colorTuple: number[];
|
||||
private _label: any;
|
||||
private _label: string;
|
||||
private colorSub: Subscription;
|
||||
|
||||
@Input() set label(label) {
|
||||
@@ -69,10 +69,10 @@ export class ChipsComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
this.color = `rgb(${color[0]},${color[1]},${color[2]})`;
|
||||
const t = tinycolor(this.color);
|
||||
const background = tinycolor.mostReadable(t.toString(), t.isDark() ?
|
||||
[t.lighten(20).toString(), t.lighten(15).toString(), t.lighten(15).toString()] :
|
||||
[t.darken(20).toString(), t.darken(15).toString(), t.darken(15).toString()], {includeFallbackColors:false, level: 'AA'});
|
||||
const t = new TinyColor(this.color);
|
||||
const background = mostReadable(t.toString(), t.isDark() ?
|
||||
[t.lighten(35).toString(), t.lighten(25).toString(), t.lighten(15).toString()] :
|
||||
[t.darken(35).toString(), t.darken(25).toString(), t.darken(15).toString()], {includeFallbackColors:false, level: 'AA'});
|
||||
this.backgroundColor = background.toRgbString();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,16 @@
|
||||
[class.has-children]="item.value.hasChildren"
|
||||
(click)="selectedItem(item, panelH)"
|
||||
>
|
||||
<mat-panel-title>
|
||||
<div class="header-container" [ngClass]="{'unchecked' : isHideAllMode(item.value) || item.value.visible}">
|
||||
<div class="ellipsis item-key" [smTooltip]="item.key" [matTooltipShowDelay]="500" smShowTooltipIfEllipsis>{{item.key}}</div>
|
||||
<i (click)="groupCheck(item); $event.stopPropagation()"
|
||||
[class]="(isHideAllMode(item.value) || item.value.visible? checkIcon[1] : checkIcon[0]) + ' al-icon sm'"
|
||||
></i>
|
||||
</div>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
<i (click)="groupCheck(item); $event.stopPropagation()"
|
||||
[class]="(isHideAllMode(item.value) || item.value.visible? checkIcon[1] : checkIcon[0]) + ' al-icon sm'"
|
||||
></i>
|
||||
</mat-panel-description>
|
||||
|
||||
</mat-expansion-panel-header>
|
||||
<ul class="list">
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
@import "variables";
|
||||
|
||||
:host {
|
||||
|
||||
display: block;
|
||||
padding: 0 10px 0 24px;
|
||||
margin: 0;
|
||||
overflow: scroll;
|
||||
|
||||
::ng-deep .mat-expansion-panel-header {
|
||||
mat-expansion-panel-header {
|
||||
padding: 0;
|
||||
|
||||
&.mat-expansion-toggle-indicator-before {
|
||||
@@ -37,45 +36,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep mat-expansion-panel.no-children {
|
||||
.mat-expansion-panel-body {
|
||||
mat-panel-description {
|
||||
justify-content: flex-end;
|
||||
color: $blue-400;
|
||||
}
|
||||
|
||||
.no-children {
|
||||
margin-left: -3px;
|
||||
::ng-deep .mat-expansion-panel-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
mat-panel-description {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .mat-expansion-panel-body {
|
||||
padding: 0 0 0 24px;
|
||||
.list {
|
||||
padding: 0;
|
||||
margin: 0 -12px 0 0;
|
||||
|
||||
.list {
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
&.list-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
line-height: 32px;
|
||||
list-style: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $blue-50;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
li {
|
||||
&.list-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 32px;
|
||||
list-style: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $blue-50;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
::ng-deep .mat-expansion-panel-header-description {
|
||||
flex-grow: 0;
|
||||
color: $blue-400;
|
||||
}
|
||||
|
||||
.mat-expansion-panel {
|
||||
@@ -98,32 +98,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .mat-content {
|
||||
padding-right: 2px;
|
||||
|
||||
.mat-expansion-panel-header-title {
|
||||
margin: unset;
|
||||
color: $blue-400;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.gray {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
/*
|
||||
i.icon {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
*/
|
||||
|
||||
.item-key {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ export class GroupedSelectableListComponent implements OnChanges {
|
||||
}
|
||||
|
||||
@Input() checkedList: Array<any>;
|
||||
@Input() selected: SelectableListItem['value'];
|
||||
@Output() onItemSelect = new EventEmitter<string>();
|
||||
@Output() onItemCheck = new EventEmitter<{ pathString: string; parent: string }>();
|
||||
@Output() onGroupCheck = new EventEmitter<any>();
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
</div>
|
||||
<sm-selectable-list
|
||||
[list]="filteredList"
|
||||
[selected]="selected"
|
||||
[checkedList]="checkedList"
|
||||
(onItemSelect)="itemSelect.emit($event)"
|
||||
(onItemCheck)="toggleHide($event)"
|
||||
|
||||
@@ -23,7 +23,6 @@ export class SelectableFilterListComponent {
|
||||
}
|
||||
|
||||
@Input() checkedList: Array<any> = [];
|
||||
@Input() selected: string;
|
||||
@Input() placeholder: string = 'Search';
|
||||
@Input() titleLabel: string;
|
||||
@Input() checkAllIcon: string;
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<sm-grouped-selectable-list
|
||||
[list]="filteredList"
|
||||
[searchTerm]="searchTerm"
|
||||
[selected]="selected"
|
||||
[checkedList]="checkedList"
|
||||
(onItemSelect)="itemSelect.emit($event)"
|
||||
(onItemCheck)="toggleHide($event)"
|
||||
|
||||
@@ -36,11 +36,10 @@ export class SelectableGroupedFilterListComponent {
|
||||
}
|
||||
|
||||
@Input() checkedList: Array<any> = [];
|
||||
@Input() selected: string;
|
||||
@Input() titleLabel: string;
|
||||
@Input() checkAllIcon: string;
|
||||
@Output() itemSelect = new EventEmitter<string>();
|
||||
@Output() hiddenChanged = new EventEmitter<any>();
|
||||
@Output() hiddenChanged = new EventEmitter<string[]>();
|
||||
@Output() searchTermChanged = new EventEmitter<string>();
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ export class SelectableListComponent implements OnChanges{
|
||||
|
||||
@Input() list: SelectableListItem[] = [];
|
||||
@Input() checkedList: string[];
|
||||
@Input() selected: SelectableListItem['value'];
|
||||
@Input() checkIcon: string[] = ['al-ico-show', 'al-ico-hide'];
|
||||
@Output() onItemSelect = new EventEmitter<string>();
|
||||
@Output() onItemCheck = new EventEmitter<string>();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user