Release v1.13 (#64)

Co-authored-by: shallegro <shay@allego.ai>
This commit is contained in:
shyallegro
2023-11-17 10:24:49 +02:00
committed by GitHub
parent aa038f4f82
commit 34f2167598
537 changed files with 18795 additions and 13775 deletions

View File

@@ -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 === '*';

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +0,0 @@
:host{
&{
display: inline-block;
}
}

View File

@@ -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();
});
});

View File

@@ -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();
}
}

View File

@@ -3,6 +3,7 @@
:host {
height: 16px;
user-select: none;
display: inline-flex;
.id-number {
color: $blue-250;

View File

@@ -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>();

View File

@@ -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()">

View File

@@ -23,6 +23,7 @@
height="100%"
[mode]="editMode ? 'editor' : 'preview'"
[options]="options"
[postRender]="postRender"
[upload]="handleUpload"
(onEditorLoaded)="editorReady($event)"
(onPreviewDomChanged)="domFixes()"

View File

@@ -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();
}

View File

@@ -2,6 +2,10 @@
:host {
vertical-align: middle;
.al-icon {
vertical-align: middle;
}
}
.tooltip-container {

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -13,6 +13,7 @@
&.snippets-mode {
grid-template-columns: repeat(auto-fit, minmax(var(--cardWidth), 1fr));
padding: 0 var(--padding);
}
}
}

View File

@@ -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"

View File

@@ -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 = '';
}
}
}

View File

@@ -100,7 +100,7 @@
}
label {
color: $blue-grey;
color: $blue-300;
padding-right: 4px;
margin-bottom: 0;
}

View File

@@ -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);
}

View File

@@ -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() {
}
}

View File

@@ -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[] {

View File

@@ -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 '';
}
}

View File

@@ -22,8 +22,5 @@ export class EntityFooterComponent extends BaseContextMenuComponent {
icons = ICONS;
trackBy = trackByIndex;
constructor(store: Store, eRef: ElementRef) {
super(store, eRef);
}
}

View File

@@ -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 } {

View File

@@ -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);

View File

@@ -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 } {

View File

@@ -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 {

View File

@@ -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 } {

View File

@@ -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 } {

View File

@@ -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 {};
}
}

View File

@@ -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 } {

View File

@@ -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 } {

View File

@@ -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 } {

View File

@@ -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 } {

View File

@@ -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 } {

View File

@@ -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 } {

View File

@@ -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[];

View File

@@ -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>) {

View File

@@ -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)"

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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">

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View 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 '';
}
};
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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(' ');
}
}
}

View 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');
}
}
}

View File

@@ -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({

View File

@@ -102,7 +102,7 @@ export class TimeAgoPipe implements PipeTransform, OnDestroy {
newDate.setHours(hours - offset);
return newDate;
}
return;
return null;
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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() {

View File

@@ -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 -->

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -5,7 +5,8 @@
}
.create-queue-button {
padding-top: 32px;
width: 100%;
padding: 32px 12px 0;
}
small.text-danger {

View File

@@ -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);

View File

@@ -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]] : []),
]
}));
})

View File

@@ -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) {

View File

@@ -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));
}

View File

@@ -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))
);
}

View File

@@ -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 = {};

View File

@@ -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.`
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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"

View File

@@ -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;

View File

@@ -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)) {

View File

@@ -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;

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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);
});
}
}

View File

@@ -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 { }

View File

@@ -34,4 +34,5 @@ export const getSmoothedLine = (arr, weight, smoothType): number[] => {
case smoothTypeEnum.exponential:
return averageDebiased(arr, weight);
}
return arr;
};

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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() {

View File

@@ -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();
}

View File

@@ -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">

View File

@@ -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;
}

View File

@@ -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>();

View File

@@ -14,7 +14,6 @@
</div>
<sm-selectable-list
[list]="filteredList"
[selected]="selected"
[checkedList]="checkedList"
(onItemSelect)="itemSelect.emit($event)"
(onItemCheck)="toggleHide($event)"

View File

@@ -23,7 +23,6 @@ export class SelectableFilterListComponent {
}
@Input() checkedList: Array<any> = [];
@Input() selected: string;
@Input() placeholder: string = 'Search';
@Input() titleLabel: string;
@Input() checkAllIcon: string;

View File

@@ -15,7 +15,6 @@
<sm-grouped-selectable-list
[list]="filteredList"
[searchTerm]="searchTerm"
[selected]="selected"
[checkedList]="checkedList"
(onItemSelect)="itemSelect.emit($event)"
(onItemCheck)="toggleHide($event)"

View File

@@ -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>();

View File

@@ -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