diff options
Diffstat (limited to 'frontend')
62 files changed, 1391 insertions, 577 deletions
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ebe30390..7a0f7b3a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,6 +33,7 @@ "d3-graphviz": "^2.6.1", "file-saver": "^2.0.5", "jquery": "^3.6.0", + "lodash.isequal": "^4.5.0", "material-icons": "^1.10.8", "mdb-angular-ui-kit": "^2.0.0", "ng-multiselect-dropdown": "^0.3.8", @@ -55,6 +56,7 @@ "@types/d3-graphviz": "^2.6.7", "@types/file-saver": "^2.0.5", "@types/jasmine": "~3.10.0", + "@types/lodash.isequal": "^4.5.6", "@types/node": "^12.11.1", "jasmine-core": "~4.0.0", "karma": "~6.3.0", @@ -2821,6 +2823,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "dev": true + }, + "node_modules/@types/lodash.isequal": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz", + "integrity": "sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.2", "dev": true, @@ -6891,6 +6908,11 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "node_modules/log-symbols": { "version": "4.1.0", "dev": true, @@ -12522,6 +12544,21 @@ "version": "7.0.9", "dev": true }, + "@types/lodash": { + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "dev": true + }, + "@types/lodash.isequal": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz", + "integrity": "sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/mime": { "version": "1.3.2", "dev": true @@ -15138,6 +15175,11 @@ "version": "4.0.8", "dev": true }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "log-symbols": { "version": "4.1.0", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index 1596072d..5d32208b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "d3-graphviz": "^2.6.1", "file-saver": "^2.0.5", "jquery": "^3.6.0", + "lodash.isequal": "^4.5.0", "material-icons": "^1.10.8", "mdb-angular-ui-kit": "^2.0.0", "ng-multiselect-dropdown": "^0.3.8", @@ -58,6 +59,7 @@ "@types/d3-graphviz": "^2.6.7", "@types/file-saver": "^2.0.5", "@types/jasmine": "~3.10.0", + "@types/lodash.isequal": "^4.5.6", "@types/node": "^12.11.1", "jasmine-core": "~4.0.0", "karma": "~6.3.0", diff --git a/frontend/src/app/_data/Dataset.ts b/frontend/src/app/_data/Dataset.ts index a962fe6b..c8d5771a 100644 --- a/frontend/src/app/_data/Dataset.ts +++ b/frontend/src/app/_data/Dataset.ts @@ -4,7 +4,7 @@ export default class Dataset extends FolderFile { constructor( name: string = 'Novi izvor podataka', public description: string = '', - public fileId?: number, + public fileId?: string, public extension: string = '.csv', public isPublic: boolean = false, public accessibleByLink: boolean = false, @@ -17,6 +17,7 @@ export default class Dataset extends FolderFile { public rowCount: number = 0, public nullRows: number = 0, public nullCols: number = 0, + public isPreProcess : Boolean = false, public cMatrix: number[][] = [] ) { super(name, dateCreated, lastUpdated); diff --git a/frontend/src/app/_data/Model.ts b/frontend/src/app/_data/Model.ts index cc25c91b..898805d3 100644 --- a/frontend/src/app/_data/Model.ts +++ b/frontend/src/app/_data/Model.ts @@ -2,6 +2,7 @@ import { NgIf } from "@angular/common"; import { FolderFile } from "./FolderFile"; export default class Model extends FolderFile { + public lossFunction: LossFunction; constructor( name: string = 'Novi model', public description: string = '', @@ -12,7 +13,6 @@ export default class Model extends FolderFile { // Neural net training settings public type: ProblemType = ProblemType.Regression, public optimizer: Optimizer = Optimizer.Adam, - public lossFunction: LossFunction = LossFunctionRegression[0], public inputNeurons: number = 1, public hiddenLayers: number = 1, public batchSize: BatchSize = BatchSize.O3, @@ -21,18 +21,19 @@ export default class Model extends FolderFile { public metrics: string[] = [], // TODO add to add-model form public epochs: number = 5, // TODO add to add-model form public inputColNum: number = 5, - public learningRate: LearningRate = LearningRate.LR1, + public learningRate: LearningRate = LearningRate.LR3, public layers: Layer[] = [new Layer()], // Test set settings public randomOrder: boolean = true, public randomTestSet: boolean = true, public randomTestSetDistribution: number = 0.1, //0.1-0.9 (10% - 90%) JESTE OVDE ZAKUCANO 10, AL POSLATO JE KAO 0.1 BACK-U - + public validationSize: number = 0.1, public isPublic: boolean = false, public accessibleByLink: boolean = false ) { super(name, dateCreated, lastUpdated); + this.lossFunction = (this.type == ProblemType.Regression ? LossFunctionRegression[0] : (this.type == ProblemType.BinaryClassification ? LossFunctionBinaryClassification[0] : LossFunctionMultiClassification[0])); } } export class Layer { @@ -45,8 +46,8 @@ export class Layer { ) { } } export enum LearningRate { - LR1 = '0.00001', - LR2 = '0.0001', + // LR1 = '0.00001', + // LR2 = '0.0001', LR3 = '0.001', LR4 = '0.003', LR5 = '0.01', @@ -117,10 +118,10 @@ export enum ActivationFunctionOutputLayer export enum LossFunction { // binary classification loss functions BinaryCrossEntropy = 'binary_crossentropy', - SquaredHingeLoss = 'squared_hinge_loss', - HingeLoss = 'hinge_loss', + SquaredHingeLoss = 'squared_hinge', + HingeLoss = 'hinge', // multi-class classification loss functions - CategoricalCrossEntropy = 'categorical_crossentropy', + // CategoricalCrossEntropy = 'categorical_crossentropy', SparseCategoricalCrossEntropy = 'sparse_categorical_crossentropy', KLDivergence = 'kullback_leibler_divergence', @@ -134,7 +135,7 @@ export enum LossFunction { export const LossFunctionRegression = [LossFunction.MeanAbsoluteError, LossFunction.MeanSquaredError, LossFunction.MeanSquaredLogarithmicError] export const LossFunctionBinaryClassification = [LossFunction.BinaryCrossEntropy, LossFunction.SquaredHingeLoss, LossFunction.HingeLoss] -export const LossFunctionMultiClassification = [LossFunction.CategoricalCrossEntropy, LossFunction.SparseCategoricalCrossEntropy, LossFunction.KLDivergence] +export const LossFunctionMultiClassification = [/*LossFunction.CategoricalCrossEntropy,*/ LossFunction.SparseCategoricalCrossEntropy, LossFunction.KLDivergence] export enum Optimizer { Adam = 'Adam', diff --git a/frontend/src/app/_data/Predictor.ts b/frontend/src/app/_data/Predictor.ts index 55d610ed..8de4cd17 100644 --- a/frontend/src/app/_data/Predictor.ts +++ b/frontend/src/app/_data/Predictor.ts @@ -3,19 +3,28 @@ import { FolderFile } from "./FolderFile"; export default class Predictor extends FolderFile { constructor( name: string = 'Novi prediktor', - public description: string = '', + + public uploaderId: string = '', public inputs: string[] = [], public output: string = '', public isPublic: boolean = false, public accessibleByLink: boolean = false, dateCreated: Date = new Date(), - lastUpdated: Date = new Date(), - public uploaderId: string = '', - //public finalMetrics: Metric[] = [] public experimentId: string = "", public modelId: string = "", + public h5FileId: string = "", + public metricsLoss:number[]=[], + public metricsValLoss:number []=[], + public metricsAcc:number[]=[], + public metricsValAcc: number[]=[], + public metricsMae :number []=[], + public metricsValMae :number []=[], + public metricsMse : number[]=[], + public metricsValMse : number[]=[], + //public metrics: Metric[] = [], + //public finalMetrics: Metric[] = [] ) { - super(name, dateCreated, lastUpdated); + super(name, dateCreated, dateCreated); } } diff --git a/frontend/src/app/_elements/_charts/box-plot/box-plot.component.html b/frontend/src/app/_elements/_charts/box-plot/box-plot.component.html index 688eafae..3b2bf976 100644 --- a/frontend/src/app/_elements/_charts/box-plot/box-plot.component.html +++ b/frontend/src/app/_elements/_charts/box-plot/box-plot.component.html @@ -1,3 +1,3 @@ -<div class="chart-wrapper"> +<div class="chart-wrapper position-relative"> <canvas #boxplot [width]="width" [height]="height"></canvas> </div>
\ No newline at end of file diff --git a/frontend/src/app/_elements/_charts/box-plot/box-plot.component.ts b/frontend/src/app/_elements/_charts/box-plot/box-plot.component.ts index bf5e3fd6..b3d25280 100644 --- a/frontend/src/app/_elements/_charts/box-plot/box-plot.component.ts +++ b/frontend/src/app/_elements/_charts/box-plot/box-plot.component.ts @@ -16,48 +16,59 @@ Chart.register(BoxPlotController, BoxAndWiskers, LinearScale, CategoryScale); }) export class BoxPlotComponent implements AfterViewInit { - @Input() width?: number; - @Input() height?: number; - @Input() mean?: number; - @Input() median?: number; - @Input() min?: number; - @Input() max?: number; - @Input() q1?: number; - @Input() q3?: number; + @Input() width!: number; + @Input() height!: number; + @Input() mean!: number; + @Input() median!: number; + @Input() min!: number; + @Input() max!: number; + @Input() q1!: number; + @Input() q3!: number; - updateChart(min: number, max: number, q1: number, q3: number, median: number) { - if (this.myChart) { - this.boxplotData.datasets[0].data = [[min, q1, median, q3, max]] - this.myChart.update(); - } - }; + + + /* + updatePieChart(uniqueValues: string[], uniqueValuesPercent: number[]){ + //console.log(this.uniqueValues, this.uniqueValuesPercent); + this.pieChartData.datasets = [{ + label: "%", + backgroundColor: ["#3e95cd", "#8e5ea2","#3cba9f","#e8c3b9","#c45850", "#000000"], + data: uniqueValuesPercent, + }]; + this.pieChartData.labels = uniqueValues + console.log(this.uniqueValues, this.uniqueValuesPercent); + this.myChart?.update() + }; + */ @ViewChild('boxplot') chartRef!: ElementRef; constructor() { //this.updateChart(); } - boxplotData = { - // define label tree - //labels: ['January'/*, 'February', 'March', 'April', 'May', 'June', 'July'*/], - datasets: [{ - label: 'Dataset 1', - backgroundColor: '#0063AB', - borderColor: '#dfd7d7', - borderWidth: 1, - outlierColor: '#999999', - scaleFontColor: '#0063AB', - padding: 10, - itemRadius: 0, - data: [ - randomValues(100, 0, 100), - ] - }] - }; + ngAfterViewInit(): void { - this.myChart = new Chart(this.chartRef.nativeElement, { + const boxplotData = { + // define label tree + //labels: ['January'/*, 'February', 'March', 'April', 'May', 'June', 'July'*/], + labels:[""], + datasets: [{ + label: 'Dataset 1', + backgroundColor: '#0063AB', + borderColor: '#dfd7d7', + borderWidth: 1, + outlierColor: '#999999', + scaleFontColor: '#0063AB', + padding: 10, + itemRadius: 0, + data: [ + {min:this.min,q1:this.q1,q3:this.q3,median:this.median,max:this.max,mean:this.mean} + ] + }] + }; + const myChart = new Chart(this.chartRef.nativeElement, { type: "boxplot", - data: this.boxplotData, + data: boxplotData, options: { plugins: { legend: { @@ -74,8 +85,6 @@ export class BoxPlotComponent implements AfterViewInit { } }, y: { - min: this.min, - max: this.max, ticks: { color: '#dfd7d7' }, @@ -88,5 +97,4 @@ export class BoxPlotComponent implements AfterViewInit { }); } - myChart?: Chart; } diff --git a/frontend/src/app/_elements/_charts/line-chart/line-chart.component.css b/frontend/src/app/_elements/_charts/line-chart/line-chart.component.css index a190693a..2eea561e 100644 --- a/frontend/src/app/_elements/_charts/line-chart/line-chart.component.css +++ b/frontend/src/app/_elements/_charts/line-chart/line-chart.component.css @@ -6,4 +6,6 @@ canvas{ background-color: var(--ns-bg-dark-100); border-radius: 5px; margin: 10px; - }
\ No newline at end of file + font-size: 11 !important; + } +
\ No newline at end of file diff --git a/frontend/src/app/_elements/_charts/line-chart/line-chart.component.html b/frontend/src/app/_elements/_charts/line-chart/line-chart.component.html index 5bb7aae6..1c711562 100644 --- a/frontend/src/app/_elements/_charts/line-chart/line-chart.component.html +++ b/frontend/src/app/_elements/_charts/line-chart/line-chart.component.html @@ -1,4 +1,4 @@ - - <canvas id="myChart" > +<div #wrapper class="position-relative" style="width:100%;height:95%;"> + <canvas id="myChart" #canvas> </canvas> -
\ No newline at end of file +</div>
\ No newline at end of file diff --git a/frontend/src/app/_elements/_charts/line-chart/line-chart.component.ts b/frontend/src/app/_elements/_charts/line-chart/line-chart.component.ts index e873618c..7d21129c 100644 --- a/frontend/src/app/_elements/_charts/line-chart/line-chart.component.ts +++ b/frontend/src/app/_elements/_charts/line-chart/line-chart.component.ts @@ -1,4 +1,4 @@ -import { Component, AfterViewInit, ViewChild } from '@angular/core'; +import { Component, AfterViewInit, ElementRef, ViewChild } from '@angular/core'; import { Chart } from 'chart.js'; @Component({ @@ -19,43 +19,63 @@ export class LineChartComponent implements AfterViewInit { dataValLoss:number[]=[]; dataEpoch: number[] = []; + @ViewChild('wrapper') + wrapper!: ElementRef; + @ViewChild('canvas') + canvas!: ElementRef; + constructor() { + } - + width = 700; + height = 400; + myChart!: Chart; - + resize() { + this.width = this.wrapper.nativeElement.offsetWidth; + this.height = this.wrapper.nativeElement.offsetHeight; + + if (this.canvas) { + this.canvas.nativeElement.width = this.width; + this.canvas.nativeElement.height = this.height; + } + } update(myEpochs: number[], myAcc: number[], myLoss: number[], myMae: number[], myMse: number[], myValAcc:number[],myValLoss:number[],myValMae:number[],myValMse:number[]) { - this.dataAcc.length = 0; - this.dataAcc.push(...myAcc); - + this.dataEpoch.length = 0; this.dataEpoch.push(...myEpochs); - this.dataMAE.length = 0; - this.dataMAE.push(...myMae); + this.dataAcc.length = 0; + this.dataAcc.push(...myAcc); this.dataLOSS.length = 0; this.dataLOSS.push(...myLoss); - this.dataMSE.length = 0; - this.dataMSE.push(...myValAcc); + this.dataMAE.length = 0; + this.dataMAE.push(...myMae); this.dataMSE.length = 0; - this.dataMSE.push(...myValLoss); + this.dataMSE.push(...myMse); - this.dataMSE.length = 0; - this.dataMSE.push(...myValMae); + this.dataValAcc.length = 0; + this.dataValAcc.push(...myValAcc); - this.dataMSE.length = 0; - this.dataMSE.push(...myValMse); + this.dataValLoss.length = 0; + this.dataValLoss.push(...myValLoss); - this.dataMSE.length = 0; - this.dataMSE.push(...myMse); + this.dataValMAE.length = 0; + this.dataValMAE.push(...myValMae); + + this.dataValMSE.length = 0; + this.dataValMSE.push(...myValMse); this.myChart.update(); } ngAfterViewInit(): void { + + window.addEventListener('resize', () => { this.resize() }); + this.resize(); this.myChart = new Chart("myChart", { type: 'line', @@ -69,8 +89,8 @@ export class LineChartComponent implements AfterViewInit { }, { - label: 'VAl_Accuracy', - data: this.dataMSE, + label: 'Val_Accuracy', + data: this.dataValAcc, borderWidth: 1 }, { @@ -80,7 +100,7 @@ export class LineChartComponent implements AfterViewInit { }, { label: 'Val_Loss', - data: this.dataMSE, + data: this.dataValLoss, borderWidth: 1 }, { @@ -90,7 +110,7 @@ export class LineChartComponent implements AfterViewInit { }, { label: 'Val_MAE', - data: this.dataMSE, + data: this.dataValMAE, borderWidth: 1 }, { @@ -100,7 +120,7 @@ export class LineChartComponent implements AfterViewInit { }, { label: 'Val_MSE', - data: this.dataMSE, + data: this.dataValMSE, borderWidth: 1 } ] @@ -150,3 +170,4 @@ export class LineChartComponent implements AfterViewInit { ); } } + diff --git a/frontend/src/app/_elements/_charts/pie-chart/pie-chart.component.html b/frontend/src/app/_elements/_charts/pie-chart/pie-chart.component.html index 7faf3af0..fe3998ff 100644 --- a/frontend/src/app/_elements/_charts/pie-chart/pie-chart.component.html +++ b/frontend/src/app/_elements/_charts/pie-chart/pie-chart.component.html @@ -1,3 +1,3 @@ -<div class="chart-wrapper"> +<div class="chart-wrapper position-relative"> <canvas #piechart [width]="width" [height]="height"></canvas> </div>
\ No newline at end of file diff --git a/frontend/src/app/_elements/_charts/pie-chart/pie-chart.component.ts b/frontend/src/app/_elements/_charts/pie-chart/pie-chart.component.ts index c2bd3262..9264e41c 100644 --- a/frontend/src/app/_elements/_charts/pie-chart/pie-chart.component.ts +++ b/frontend/src/app/_elements/_charts/pie-chart/pie-chart.component.ts @@ -14,16 +14,16 @@ export class PieChartComponent implements AfterViewInit { @Input()uniqueValuesPercent?: number[] = []; updatePieChart(uniqueValues: string[], uniqueValuesPercent: number[]){ - console.log(this.uniqueValues, this.uniqueValuesPercent); - const newPieChartData = { - datasets: [{ - label: "Population (millions)", - backgroundColor: ["#3e95cd", "#8e5ea2","#3cba9f","#e8c3b9","#c45850"], - data: [2478,5267,734,784,433], - }] - - } - }; + //console.log(this.uniqueValues, this.uniqueValuesPercent); + this.pieChartData.datasets = [{ + label: "%", + backgroundColor: ["#3e95cd", "#8e5ea2","#3cba9f","#e8c3b9","#c45850", "#000000"], + data: uniqueValuesPercent, + }]; + this.pieChartData.labels = uniqueValues + console.log(this.uniqueValues, this.uniqueValuesPercent); + this.myChart?.update() + }; @ViewChild('piechart') chartRef!: ElementRef; constructor() { } @@ -33,18 +33,31 @@ export class PieChartComponent implements AfterViewInit { label: "Population (millions)", backgroundColor: ["#3e95cd", "#8e5ea2","#3cba9f","#e8c3b9","#c45850"], data: [2478,5267,734,784,433] - }] + }], labels : [""] } + myChart?: Chart; ngAfterViewInit(): void { - const myChart = new Chart(this.chartRef.nativeElement, { + let rem = 100; + const percents : number[] = [] + this.uniqueValuesPercent?.forEach(percent => { + rem-=percent*100; + percents.push(percent*100) + + }) + const data = { + datasets: [{ + label: "%", + backgroundColor: ["#3e95cd", "#8e5ea2","#3cba9f","#e8c3b9","#c45850", "#000000"], + data: [...percents, rem] + }], labels : [...this.uniqueValues!,"Ostalo"] + } + + const myChart = new Chart(this.chartRef.nativeElement, { type: 'pie', - data: this.pieChartData, + data: data + , options: { - /*title: { - display: true, - text: 'Predicted world population (millions) in 2050' - }*/ plugins:{ legend: { display: false diff --git a/frontend/src/app/_elements/column-table/column-table.component.css b/frontend/src/app/_elements/column-table/column-table.component.css index 0477b7be..40ae3c05 100644 --- a/frontend/src/app/_elements/column-table/column-table.component.css +++ b/frontend/src/app/_elements/column-table/column-table.component.css @@ -13,7 +13,7 @@ table.fixed { } table.fixed td { - overflow: hidden; + /* overflow: hidden; */ max-width: 200px; min-width: 200px; vertical-align: middle; @@ -21,8 +21,8 @@ table.fixed td { } table.fixed th { - overflow: hidden; - max-width: 120px; + /* overflow: hidden; */ + max-width: 250px; min-width: 120px; vertical-align: middle; background-color: var(--ns-primary-50); @@ -198,7 +198,7 @@ table ::ng-deep .mat-form-field-wrapper { font-size: large; position: relative; background-color: var(--ns-primary); - width: 10rem; + width: 13rem; height: 2.3rem; border-color: var(--ns-primary); border-style: solid; diff --git a/frontend/src/app/_elements/column-table/column-table.component.html b/frontend/src/app/_elements/column-table/column-table.component.html index d07d50b2..e6a781be 100644 --- a/frontend/src/app/_elements/column-table/column-table.component.html +++ b/frontend/src/app/_elements/column-table/column-table.component.html @@ -35,7 +35,7 @@ </thead> <tbody> <tr *ngFor="let row of tableData; let i = index"> - <th>#{{i}}</th> + <th>#{{((this.begin/10)+1)*10-10+i+1}}</th> <td *ngFor="let col of row; let j = index" [ngClass]="{'text-disabled' : !columnsChecked[j]}"> <div class="text-overflow"> {{col}} @@ -44,6 +44,12 @@ </tr> </tbody> </table> + <div class="mb-3"> + <button mat-button (click)="goBack()"><mat-icon>keyboard_arrow_left</mat-icon></button> + <div style="display: inline;">{{(this.begin/10)+1}}...{{getPage()}}</div> + <button mat-button (click)="goForward()"><mat-icon>keyboard_arrow_right</mat-icon></button> + + </div> </div> <div [ngClass]="{'hidden': tabToDisplay != Table.CorrelationMatrix}"> @@ -211,7 +217,7 @@ <div class="ns-col rounded"> <mat-form-field appearance="fill" class="align-items-center justify-content-center pt-3 w-100"> <mat-label>Izlazna kolona</mat-label> - <mat-select [(value)]="experiment.outputColumn" (selectionChange)="changeProblemType()"> + <mat-select [(value)]="experiment.outputColumn" (selectionChange)="outputColumnChanged()"> <mat-option *ngFor="let inputColumn of experiment.inputColumns" [value]="inputColumn">{{inputColumn}}</mat-option> <mat-option *ngIf="experiment.inputColumns.length == 0" value="-">-</mat-option> </mat-select> diff --git a/frontend/src/app/_elements/column-table/column-table.component.ts b/frontend/src/app/_elements/column-table/column-table.component.ts index 217eda30..a9057695 100644 --- a/frontend/src/app/_elements/column-table/column-table.component.ts +++ b/frontend/src/app/_elements/column-table/column-table.component.ts @@ -14,6 +14,7 @@ import { AlertDialogComponent } from 'src/app/_modals/alert-dialog/alert-dialog. import Shared from 'src/app/Shared'; import { PieChartComponent } from '../_charts/pie-chart/pie-chart.component'; import { BoxPlotComponent } from '../_charts/box-plot/box-plot.component'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-column-table', @@ -41,70 +42,100 @@ export class ColumnTableComponent implements AfterViewInit { columnsChecked: boolean[] = []; //niz svih kolona loaded: boolean = false; + begin:number=0; + step:number=10; - constructor(private datasetService: DatasetsService, private experimentService: ExperimentsService, public csvParseService: CsvParseService, public dialog: MatDialog) { - //ovo mi nece trebati jer primam dataset iz druge komponente - } - updateCharts() { - //min: number, max: number, q1: number, q3: number, median: number - let i = 0; - this.boxplotComp.changes.subscribe(() => { - const bps = this.boxplotComp.toArray(); - this.dataset?.columnInfo.forEach(colInfo => { - if (this.experiment.columnTypes[i] == ColumnType.numerical) { - bps[i].updateChart(colInfo!.min, colInfo.max, colInfo.q1, colInfo.q3, colInfo.median); - i++; - } - }); - }); + + constructor(private datasetService: DatasetsService, private experimentService: ExperimentsService, public csvParseService: CsvParseService, public dialog: MatDialog, private route: ActivatedRoute) { } + resetPagging(){ + this.begin=0; + } + goBack(){ + if(this.begin-10<0) + this.begin=0; + else + { + this.begin-=10; + this.loadData(); + } - updatePieChart() { - //min: number, max: number, q1: number, q3: number, median: number - let i = 0; - const pieChart = this.piechartComp.toArray(); - this.dataset?.columnInfo.forEach(colInfo => { - if (this.experiment.columnTypes[i] == ColumnType.categorical) { - pieChart[i].updatePieChart(colInfo!.uniqueValues, colInfo.uniqueValuesPercent); - i++; - } - }); + } + goForward(){ + if(this.dataset!=undefined){ + this.begin+=10; + if(this.dataset.rowCount<this.begin) + this.begin-=10; + else + this.loadData(); + } + } + getPage(){ + if(this.dataset!=undefined) + return Math.ceil(this.dataset.rowCount/this.step); + return 0; } - loadDataset(dataset: Dataset) { - console.log("LOADED DATASET"); - this.dataset = dataset; - this.setColumnTypeInitial(); - this.dataset.columnInfo.forEach(column => { - this.columnsChecked.push(true); - }); + - this.resetInputColumns(); - this.resetOutputColumn(); - this.resetColumnEncodings(Encoding.Label); - this.setDeleteRowsForMissingValTreatment(); - this.nullValOption = []; - this.dataset.columnInfo.forEach(colInfo => { - this.nullValOption.push(`Obriši redove (${colInfo.numNulls})`); - }); - this.datasetService.getDatasetFilePartial(this.dataset.fileId, 0, 10).subscribe((response: string | undefined) => { + loadDataset(dataset: Dataset) { + console.log("LOADED DATASET"); + + if (this.route.snapshot.paramMap.get("id") == null && this.route.snapshot.paramMap.get("predictorId") == null) { + this.dataset = dataset; + this.setColumnTypeInitial(); + + this.columnsChecked = []; + this.dataset.columnInfo.forEach(column => { + this.columnsChecked.push(true); + }); + + this.resetInputColumns(); + this.resetOutputColumn(); + this.resetColumnEncodings(Encoding.Label); + this.setDeleteRowsForMissingValTreatment(); + + this.nullValOption = []; + this.dataset.columnInfo.forEach(colInfo => { + this.nullValOption.push(`Obriši redove (${colInfo.numNulls})`); + }); + } + else { + this.dataset = dataset; + this.experimentChanged.emit(); + this.columnsChecked = []; + this.dataset.columnInfo.forEach(column => { + if (this.experiment.inputColumns.find(x => x == column.columnName) != undefined) + this.columnsChecked.push(true); + else + this.columnsChecked.push(false); + }); + //this.nullValOption = []; + for (let i = 0; i < this.dataset!.columnInfo.length; i++) { + //let nullValRep = this.experiment.nullValuesReplacers.find(x => x.column == this.dataset!.columnInfo[i].columnName); + let nullValRep = this.experiment.nullValuesReplacers[i]; + this.nullValOption[i] = (nullValRep.option == NullValueOptions.DeleteRows) ? `Obriši redove (${this.dataset.columnInfo[i].numNulls})` : ((nullValRep.option == NullValueOptions.DeleteColumns) ? `Isključi kolonu` : `Popuni sa ${nullValRep.value}`); + } + } + this.resetPagging(); + this.loadData(); + this.loaded = true; + } + + loadData(){ + if(this.dataset!=undefined) + this.datasetService.getDatasetFilePartial(this.dataset.fileId, this.begin, this.step).subscribe((response: string | undefined) => { if (response && this.dataset != undefined) { this.tableData = this.csvParseService.csvToArray(response, (this.dataset.delimiter == "razmak") ? " " : (this.dataset.delimiter == "novi red") ? "\t" : this.dataset.delimiter); } }); - this.loaded = true; - - this.updateCharts(); - this.updatePieChart(); } ngAfterViewInit(): void { - console.log(this.dataset?.columnInfo); - } setColumnTypeInitial() { @@ -124,17 +155,19 @@ export class ColumnTableComponent implements AfterViewInit { } } resetOutputColumn() { - if (this.experiment.inputColumns.length > 0) + if (this.experiment.inputColumns.length > 0) { this.experiment.outputColumn = this.experiment.inputColumns[0]; + this.changeProblemType(); + } else this.experiment.outputColumn = '-'; } setDeleteRowsForMissingValTreatment() { - if (this.experiment != undefined) { + if (this.experiment != undefined && this.dataset != undefined) { this.experiment.nullValues = NullValueOptions.DeleteRows; this.experiment.nullValuesReplacers = []; - for (let i = 0; i < this.experiment.inputColumns.length; i++) { + for (let i = 0; i < this.dataset?.columnInfo.length; i++) { this.experiment.nullValuesReplacers.push({ column: this.experiment.inputColumns[i], option: NullValueOptions.DeleteRows, @@ -164,13 +197,17 @@ export class ColumnTableComponent implements AfterViewInit { } if (this.experiment.inputColumns.length == 1) this.experiment.outputColumn = this.experiment.inputColumns[0]; + + let index = this.dataset?.columnInfo.findIndex(x => x.columnName == columnName)!; + this.nullValOption[index] = (this.experiment.nullValuesReplacers[index].option == NullValueOptions.DeleteRows) ? "Obriši redove (" + this.dataset?.columnInfo[index].numNulls + ")" : "Popuni sa " + this.experiment.nullValuesReplacers[index].value; } else { this.experiment.inputColumns = this.experiment.inputColumns.filter(x => x != columnName); //console.log("Input columns: ", this.experiment.inputColumns); //TODO: da se zatamni kolona koja je unchecked //this.experiment.encodings = this.experiment.encodings.filter(x => x.columnName != columnName); samo na kraju iz enkodinga skloni necekirane - this.experiment.nullValuesReplacers = this.experiment.nullValuesReplacers.filter(x => x.column != columnName); + let index = this.dataset?.columnInfo.findIndex(x => x.columnName == columnName)!; + this.nullValOption[index] = "Isključi kolonu"; if (columnName == this.experiment.outputColumn) { if (this.experiment.inputColumns.length > 0) this.experiment.outputColumn = this.experiment.inputColumns[0]; @@ -182,17 +219,37 @@ export class ColumnTableComponent implements AfterViewInit { } } + outputColumnChanged() { + let outputColReplacer = this.experiment.nullValuesReplacers.find(x => x.column == this.experiment.outputColumn); + //let index = this.experiment.nullValuesReplacers.findIndex(x => x.column == this.experiment.outputColumn); + if (outputColReplacer != undefined) { + outputColReplacer.option = NullValueOptions.DeleteRows; + + let numOfRowsToDelete = (this.dataset!.columnInfo.filter(x => x.columnName == this.experiment.outputColumn)[0]).numNulls; + let index = this.dataset!.columnInfo.findIndex(x => x.columnName == this.experiment.outputColumn); + this.nullValOption[index] = "Obriši redove (" + numOfRowsToDelete + ")"; + } + + this.changeProblemType(); + } + changeProblemType() { if (this.experiment != undefined && this.dataset != undefined) { + //console.log(this.experiment.outputColumn); let i = this.dataset.columnInfo.findIndex(x => x.columnName == this.experiment!.outputColumn); if (i == -1 || this.experiment.columnTypes[i] == ColumnType.numerical) { + //console.log("USAO U REGRESIONI"); this.experiment.type = ProblemType.Regression; } else { - if (this.dataset.columnInfo[i].uniqueValues!.length == 2) + if (this.dataset.columnInfo[i].uniqueValues!.length == 2) { + //console.log("USAO U BINARY"); this.experiment.type = ProblemType.BinaryClassification; - else + } + else { + //console.log("USAO U multi"); this.experiment.type = ProblemType.MultiClassification; + } } this.columnTableChangeDetected(); } @@ -232,16 +289,47 @@ export class ColumnTableComponent implements AfterViewInit { if (this.experiment != undefined && this.dataset != undefined) { if (selectedMissingValuesOption == NullValueOptions.DeleteColumns) { - this.experiment.nullValues = NullValueOptions.DeleteColumns; + for (let i = 0; i < this.dataset.columnInfo.length; i++) { + if (this.dataset.columnInfo[i].numNulls > 0 && this.dataset.columnInfo[i].columnName != this.experiment.outputColumn) { + this.experiment.inputColumns = this.experiment.inputColumns.filter(x => x != this.dataset!.columnInfo[i].columnName); + this.columnsChecked[i] = false; + console.log(this.dataset!.columnInfo[i].columnName); + + this.nullValOption[i] = "Isključi kolonu"; + } + } + /*this.experiment.nullValues = NullValueOptions.DeleteColumns; + + let outputColReplacer = this.experiment.nullValuesReplacers.find(x => x.column == this.experiment.outputColumn); + this.experiment.nullValuesReplacers = []; for (let i = 0; i < this.experiment.inputColumns.length; i++) { - this.experiment.nullValuesReplacers.push({ - column: this.experiment.inputColumns[i], - option: NullValueOptions.DeleteColumns, - value: "" - }); - this.nullValOption[i] = "Obriši kolonu"; - } + if (this.experiment.inputColumns[i] != this.experiment.outputColumn) { + this.experiment.nullValuesReplacers.push({ //ovo zakomentarisano + column: this.experiment.inputColumns[i], + option: NullValueOptions.DeleteColumns, + value: "" + }); + let colIndex = this.dataset.columnInfo.findIndex(x => x.columnName == this.experiment.inputColumns[i]); + this.nullValOption[colIndex] = "Isključi kolonu"; + } + else { + if (outputColReplacer != undefined) { + this.experiment.nullValuesReplacers.push(outputColReplacer); + let numOfRowsToDelete = (this.dataset.columnInfo.filter(x => x.columnName == this.experiment!.inputColumns[i])[0]).numNulls; + let colIndex = this.dataset.columnInfo.findIndex(x => x.columnName == this.experiment.inputColumns[i]); + this.nullValOption[colIndex] = (outputColReplacer.option == NullValueOptions.DeleteRows) ? "Obriši redove (" + numOfRowsToDelete + ")" : "Popuni sa " + outputColReplacer.value + ""; + } + } + }*/ + //obrisi kolone koje sadrze nedostajuce vrednosti iz input kolona + /*for (let i = 0; i < this.dataset.columnInfo.length; i++) { + if (this.dataset.columnInfo[i].numNulls > 0) { + this.experiment.inputColumns = this.experiment.inputColumns.filter(x => x != this.dataset!.columnInfo[i].columnName); + this.columnsChecked[i] = false; + console.log(this.dataset!.columnInfo[i].columnName); + } + }*/ } else if (selectedMissingValuesOption == NullValueOptions.DeleteRows) { this.experiment.nullValues = NullValueOptions.DeleteRows; @@ -253,7 +341,8 @@ export class ColumnTableComponent implements AfterViewInit { value: "" }); let numOfRowsToDelete = (this.dataset.columnInfo.filter(x => x.columnName == this.experiment!.inputColumns[i])[0]).numNulls; - this.nullValOption[i] = "Obriši redove (" + numOfRowsToDelete + ")"; + let colIndex = this.dataset.columnInfo.findIndex(x => x.columnName == this.experiment.inputColumns[i]); + this.nullValOption[colIndex] = "Obriši redove (" + numOfRowsToDelete + ")"; } } this.columnTableChangeDetected(); @@ -261,7 +350,8 @@ export class ColumnTableComponent implements AfterViewInit { } openMissingValuesDialog() { const dialogRef = this.dialog.open(MissingvaluesDialogComponent, { - width: '400px' + width: '500px', + panelClass: 'custom-modalbox' }); dialogRef.afterClosed().subscribe(selectedMissingValuesOption => { if (selectedMissingValuesOption != undefined) @@ -295,7 +385,16 @@ export class ColumnTableComponent implements AfterViewInit { MissValsDeleteClicked(event: Event, replacementType: NullValueOptions, index: number) { if (this.experiment != undefined && this.dataset != undefined) { - let columnName = (<HTMLInputElement>event.currentTarget).value; + + this.experiment.nullValuesReplacers[index].option = NullValueOptions.DeleteRows; + this.experiment.nullValuesReplacers[index].value = ""; + + this.nullValOption[index] = "Obriši redove (" + this.dataset.columnInfo[index].numNulls + ")"; + + this.columnTableChangeDetected(); + } + + /*let columnName = (<HTMLInputElement>event.currentTarget).value; let arrayElement = this.experiment.nullValuesReplacers.filter(x => x.column == columnName)[0]; if (arrayElement == undefined) { @@ -311,14 +410,21 @@ export class ColumnTableComponent implements AfterViewInit { } let numOfRowsToDelete = (this.dataset.columnInfo.filter(x => x.columnName == this.experiment!.inputColumns[index])[0]).numNulls; - this.nullValOption[index] = (replacementType == NullValueOptions.DeleteColumns) ? "Obriši kolonu" : "Obriši redove (" + numOfRowsToDelete + ")"; + this.nullValOption[index] = (replacementType == NullValueOptions.DeleteColumns) ? "Isključi kolonu" : "Obriši redove (" + numOfRowsToDelete + ")"; this.columnTableChangeDetected(); - } + }*/ } MissValsReplaceClicked(event: Event, columnName: string, index: number) { if (this.experiment != undefined) { - let fillValue = (<HTMLInputElement>event.currentTarget).value; + this.experiment.nullValuesReplacers[index].option = NullValueOptions.Replace; + let value = (<HTMLInputElement>event.currentTarget).value; + this.experiment.nullValuesReplacers[index].value = value; + this.nullValOption[index] = "Popuni sa " + value; + + this.columnTableChangeDetected(); + + /*let fillValue = (<HTMLInputElement>event.currentTarget).value; let arrayElement = this.experiment.nullValuesReplacers.filter(x => x.column == columnName)[0]; if (arrayElement == undefined) { @@ -334,7 +440,7 @@ export class ColumnTableComponent implements AfterViewInit { } this.nullValOption[index] = "Popuni sa: " + fillValue; - this.columnTableChangeDetected(); + this.columnTableChangeDetected();*/ } } getValue(columnName: string): string { diff --git a/frontend/src/app/_elements/datatable/datatable.component.css b/frontend/src/app/_elements/datatable/datatable.component.css index e69de29b..4e0a3737 100644 --- a/frontend/src/app/_elements/datatable/datatable.component.css +++ b/frontend/src/app/_elements/datatable/datatable.component.css @@ -0,0 +1,4 @@ +.proba { + max-width: 1620px; + +}
\ No newline at end of file diff --git a/frontend/src/app/_elements/folder/folder.component.css b/frontend/src/app/_elements/folder/folder.component.css index ada2dba0..682fc645 100644 --- a/frontend/src/app/_elements/folder/folder.component.css +++ b/frontend/src/app/_elements/folder/folder.component.css @@ -1,7 +1,6 @@ #folder { - /*position: absolute; - left: 50%; - transform: translateX(-50%);*/ + width: 100%; + } #tabs { @@ -107,8 +106,9 @@ .bottom-button { font-size: large; position: relative; + text-align: center; background-color: var(--ns-primary); - width: 10rem; + width: 12rem; height: 2.3rem; border-color: var(--ns-primary); border-style: solid; @@ -167,24 +167,26 @@ align-items: center; flex-grow: 1; height: 100%; + padding: 50px; } .folder-inside { width: 100%; - height: 40rem; - overflow-y: auto; + min-height: 33rem; + height: 100%; + max-height: 100%; } .file-content { width: 100%; - height: 100%; + height: 95%; position: relative; } .file-bottom-buttons { position: absolute; - bottom: 15px; - right: 15px; + bottom: 5px; + right: 4%; display: flex; flex-direction: row-reverse; } diff --git a/frontend/src/app/_elements/folder/folder.component.html b/frontend/src/app/_elements/folder/folder.component.html index 8896e7e5..a557edde 100644 --- a/frontend/src/app/_elements/folder/folder.component.html +++ b/frontend/src/app/_elements/folder/folder.component.html @@ -3,7 +3,7 @@ <div id="new-file-tab" *ngIf="newFile" class="folder-tab p-1 rounded-top" [style]="'z-index:' + (selectedTab == TabType.NewFile ? 11 : 10) + ' ;'" [ngClass]="{'selected-tab' : selectedTab == TabType.NewFile, 'hover-tab' : hoverTab == TabType.NewFile}"> <mat-icon class="text-offwhite">add</mat-icon> <a class="stretched-link tab-link" (click)="selectTab(TabType.NewFile)" (mouseenter)="hoverOverTab(TabType.NewFile)" (mouseleave)="hoverOverTab(TabType.None)"> - {{newFile.name}} + {{newTabTitles[type]}} </a> </div> <!--<div class="folder-tab p-1 rounded-top" *ngFor="let file of filteredFiles; let i = index" [style]="'z-index:' + calcZIndex(i) + ' ;'" [ngClass]="{'selected-tab' : selectedFileIndex == i, 'hover-tab' : hoveringOverFileIndex == i}"> @@ -29,42 +29,48 @@ <button matSuffix class="btn-clear input-icon" (click)="clearSearchTerm()"><mat-icon>clear</mat-icon></button> </mat-form-field> </div> - <div id="search-options"> - <!-- <div id="collapseFilters" class="collapse collapse-horizontal"> + <div id="modelFilter" *ngIf="type == FolderType.Model && forExperiment"> + Filter: {{forExperiment.type}} + </div> + <!--<div id="search-options"> + <div id="collapseFilters" class="collapse collapse-horizontal"> <mat-icon class="text-offwhite ">timeline</mat-icon> Regresioni <mat-icon class="text-offwhite ">looks_two</mat-icon> Binarni klasifikacioni <mat-icon class="text-offwhite ">auto_awesome_motion</mat-icon> Multiklasifikacioni - </div> --> + </div> <button class="btn-clear icon-toggle" data-bs-toggle="collapse" data-bs-target="#collapseFilters" aria-expanded="false" aria-controls="collapseFilters"> <mat-icon>filter_alt</mat-icon> </button> - <!-- <div id="collapseSort" class="collapse collapse-horizontal"> + <div id="collapseSort" class="collapse collapse-horizontal"> [sort options here TODO] - </div> --> + </div> <button class="btn-clear icon-toggle" data-bs-toggle="collapse" data-bs-target="#collapseSort" aria-expanded="false" aria-controls="collapseSort"> <mat-icon>sort</mat-icon> </button> - <!-- <button class="btn-clear icon-toggle separator" [ngClass]="{'icon-toggle-on': listView}" (click)="toggleListView()"> + <button class="btn-clear icon-toggle separator" [ngClass]="{'icon-toggle-on': listView}" (click)="toggleListView()"> <mat-icon>view_list</mat-icon> - </button> --> - </div> + </button> + </div>--> </div> <!--{{fileToDisplay ? fileToDisplay.name : 'No file selected.'}} {{selectedFileIndex}} {{hoveringOverFileIndex}}--> <div class="folder-inside bg-blur"> <div class="file-content" [ngClass]="{'form-hidden' : listView}"> <div class="file-bottom-buttons" *ngIf="selectedTab != TabType.NewFile"> - <button *ngIf="this.selectedFile && selectedTab == TabType.File" class="btn-clear file-button" (click)="deleteFile(this.selectedFile, $event)"> - <mat-icon>delete</mat-icon> + <button *ngIf="this.selectedFile && selectedTab == TabType.File && privacy != Privacy.Public" class="btn-clear file-button" (click)="deleteFile(this.selectedFile, $event)"> + <mat-icon matTooltip="Obriši" matTooltipPosition="right">delete</mat-icon> + </button> + <button *ngIf="this.selectedFile && selectedTab==TabType.File && FolderType.Dataset==this.type" class="btn-clear file-button" (click)="downloadFile(this.selectedFile,$event)" style="display: inline-block;"> + <mat-icon matTooltip="Preuzmi" matTooltipPosition="before">download</mat-icon> </button> <!-- <button class="btn-clear file-button"> <mat-icon>zoom_out_map</mat-icon> </button> --> </div> - <app-form-model [ngClass]="{'form-hidden': type != FolderType.Model}" [forExperiment]="forExperiment"></app-form-model> - <app-form-dataset [ngClass]="{'form-hidden': type != FolderType.Dataset}" [forExperiment]="forExperiment"></app-form-dataset> + <app-form-model [ngClass]="{'form-hidden': type != FolderType.Model}" [forExperiment]="forExperiment" [hideProblemType]="(forExperiment ? true : false)" [forProblemType]="(forExperiment ? forExperiment.type : ProblemType.Regression)" (editEvent)="onFileChange()"></app-form-model> + <app-form-dataset [ngClass]="{'form-hidden': type != FolderType.Dataset}" [forExperiment]="forExperiment" (editEvent)="onFileChange()"></app-form-dataset> </div> <div [ngClass]="{'form-hidden' : !listView}" class="list-view"> <div *ngFor="let file of filteredFiles; let i = index"> @@ -76,13 +82,21 @@ {{file.lastUpdated | date}} </div> <div class="mx-2 hover-show" *ngIf="selectedTab !== TabType.PublicDatasets && selectedTab !== TabType.PublicModels"> - <button class="btn-clear file-button" (click)="deleteFile(file, $event)"> - <mat-icon>delete</mat-icon> + <button *ngIf="selectedTab==TabType.MyDatasets" class="btn-clear file-button" (click)="downloadFile(file,$event)" style="display: inline-block;"> + <mat-icon matTooltip="Preuzmi" matTooltipPosition="before">download</mat-icon> + </button> + <button class="btn-clear file-button" (click)="deleteFile(file, $event)" style="display: inline-block;"> + <mat-icon matTooltip="Obriši" matTooltipPosition="right">delete</mat-icon> + </button> + </div> + <div class="mx-2 hover-show" *ngIf="selectedTab == TabType.PublicDatasets || selectedTab == TabType.PublicModels"> + <button class="btn-clear file-button" (click)="addFile(file, $event)"> + <mat-icon matTooltip="Uvezi" matTooltipPosition="right">note_add</mat-icon> </button> </div> </div> <div *ngIf="type == FolderType.Experiment" class="list-view"> - <div *ngFor="let predictor of predictorsForExp[file._id];" class="list-item"> + <div *ngFor="let predictor of predictorsForExp[file._id];" class="list-item" (click)="goToExperimentPageWithPredictor(file, predictor)"> <div class="mx-3"> <div class="f-row"> @@ -94,7 +108,7 @@ {{predictor.lastUpdated | date}} </div> <div class="mx-2 hover-show"> - <button class="btn-clear file-button" (click)="deleteFile(predictor, $event)"> + <button class="btn-clear file-button" (click)="deleteFile(predictor, $event, true)"> <mat-icon>delete</mat-icon> </button> </div> @@ -102,9 +116,10 @@ </div> </div> - <div class="list-add" [ngSwitch]="type"> - <button mat-raised-button *ngSwitchCase="FolderType.Dataset" (click)="selectNewFile()">Dodaj {{privacy == Privacy.Public ? 'javni ' : ' '}}izvor podataka</button> - <button mat-raised-button *ngSwitchCase="FolderType.Model" (click)="selectNewFile()">Dodaj {{privacy == Privacy.Public ? 'javnu ' : ' '}}konfiguraciju neuronske mreže</button> + <div class="list-add" [ngSwitch]="type" *ngIf="privacy != Privacy.Public" > + <!-- {{privacy == Privacy.Public ? 'javni ' : ' '}} --> + <button mat-raised-button *ngSwitchCase="FolderType.Dataset" (click)="selectNewFile()">Dodaj izvor podataka</button> + <button mat-raised-button *ngSwitchCase="FolderType.Model" (click)="selectNewFile()">Dodaj konfiguraciju neuronske mreže</button> <button mat-raised-button *ngSwitchCase="FolderType.Experiment" routerLink="/experiment">Dodaj eksperiment</button> </div> </div> @@ -119,14 +134,30 @@ </div> </div> </button> - <button mat-button (click)="ok()" class="bottom-button text-offwhite rounded-bottom" *ngSwitchCase="false"> - <div class="f-row"> - <div>Ok</div> - <div class="icon-double pt-1"> - <mat-icon>check</mat-icon> - <mat-icon>check</mat-icon> + <ng-container *ngSwitchCase="false"> + <button mat-button (click)="ok()" class="bottom-button text-offwhite rounded-bottom" *ngIf="!selectedFileHasChanges && !archive"> + <div class="f-row"> + <div>Ok</div> + <div class="p-1 w-100" *ngIf="loadingAction"> + <app-spinner></app-spinner> + </div> + <div class="icon-double pt-1" *ngIf="!loadingAction"> + <mat-icon>check</mat-icon> + <mat-icon>check</mat-icon> + </div> </div> - </div> - </button> + </button> + <button mat-button (click)="updateFile()" class="bottom-button text-offwhite rounded-bottom" *ngIf="selectedFileHasChanges" [disabled]="loadingAction"> + <div class="f-row"> + <div *ngIf="!loadingAction">Sačuvaj izmene</div> + <div class="pt-1" *ngIf="!loadingAction"> + <mat-icon>edit</mat-icon> + </div> + <div class="pt-1 w-100" *ngIf="loadingAction"> + <app-spinner></app-spinner> + </div> + </div> + </button> + </ng-container> </div> </div>
\ No newline at end of file diff --git a/frontend/src/app/_elements/folder/folder.component.ts b/frontend/src/app/_elements/folder/folder.component.ts index 665659a8..93e8ba98 100644 --- a/frontend/src/app/_elements/folder/folder.component.ts +++ b/frontend/src/app/_elements/folder/folder.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import Dataset from 'src/app/_data/Dataset'; import { FolderFile, FolderType } from 'src/app/_data/FolderFile'; -import Model from 'src/app/_data/Model'; +import Model, { ProblemType } from 'src/app/_data/Model'; import { DatasetsService } from 'src/app/_services/datasets.service'; import Shared from 'src/app/Shared'; import { ModelsService } from 'src/app/_services/models.service'; @@ -11,8 +11,10 @@ import { ExperimentsService } from 'src/app/_services/experiments.service'; import { PredictorsService } from 'src/app/_services/predictors.service'; import { SignalRService } from 'src/app/_services/signal-r.service'; import { FormModelComponent } from '../form-model/form-model.component'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import Predictor from 'src/app/_data/Predictor'; +import FileSaver from 'file-saver'; +import isEqual from 'lodash.isequal'; @Component({ selector: 'app-folder', @@ -42,11 +44,12 @@ export class FolderComponent implements AfterViewInit { fileToDisplay?: FolderFile; @Output() selectedFileChanged: EventEmitter<FolderFile> = new EventEmitter(); + @Output() fileFromRoute: EventEmitter<FolderFile> = new EventEmitter(); @Output() okPressed: EventEmitter<string> = new EventEmitter(); searchTerm: string = ''; - constructor(private datasetsService: DatasetsService, private experimentsService: ExperimentsService, private modelsService: ModelsService, private predictorsService: PredictorsService, private signalRService: SignalRService, private router: Router) { + constructor(private datasetsService: DatasetsService, private experimentsService: ExperimentsService, private modelsService: ModelsService, private predictorsService: PredictorsService, private signalRService: SignalRService, private router: Router, private route: ActivatedRoute) { this.tabsToShow.forEach(tab => this.folders[tab] = []); } @@ -65,8 +68,10 @@ export class FolderComponent implements AfterViewInit { } displayFile() { - if (this.type == FolderType.Dataset) + if (this.type == FolderType.Dataset) { this.formDataset.dataset = <Dataset>this.fileToDisplay; + this.formDataset.existingFlag = false; + } else if (this.type == FolderType.Model) this.formModel.newModel = <Model>this.fileToDisplay; } @@ -93,15 +98,18 @@ export class FolderComponent implements AfterViewInit { this.newFileSelected = true; this.listView = false; this.displayFile(); - if (this.type == FolderType.Dataset) + if (this.type == FolderType.Dataset) { this.formDataset.clear(); + } } selectFile(file?: FolderFile) { + this.formDataset.resetPagging(); this.selectedFile = file; + Object.assign(this.lastFileData, this.selectedFile); this.fileToDisplay = file; if (this.type == FolderType.Experiment && file) { - this.router.navigate(['/experiment'/*, file._id*/]) + this.router.navigate(['/experiment/' + file._id]); } this.newFileSelected = false; this.listView = false; @@ -113,6 +121,10 @@ export class FolderComponent implements AfterViewInit { this.formDataset.loadExisting(); } + goToExperimentPageWithPredictor(file: FolderFile, predictor: Predictor) { + this.router.navigate(['/experiment/p/' + predictor._id]); + } + createNewFile() { if (this.type == FolderType.Dataset) { this.newFile = new Dataset(); @@ -128,10 +140,7 @@ export class FolderComponent implements AfterViewInit { _initialized: boolean = false; refreshFiles(selectedDatasetId: string | null = null, selectedModelId: string | null = null) { - this.files = [] - this.filteredFiles.length = 0; - this.folders[TabType.NewFile] = []; - this.folders[TabType.File] = []; + this.tabsToShow.forEach(tab => { this.folders[tab] = []; }); @@ -173,6 +182,9 @@ export class FolderComponent implements AfterViewInit { this.folders[TabType.MyModels] = models; if (selectedModelId) { this.selectFile(models.filter(x => x._id == selectedModelId)[0]); + setTimeout(() => { + this.okPressed.emit(); + }); } this.searchTermsChanged(); }); @@ -180,12 +192,18 @@ export class FolderComponent implements AfterViewInit { this.folders[TabType.PublicModels] = models; this.searchTermsChanged(); });*/ - this.folders[TabType.PublicModels] = []; + + this.modelsService.getPublicModels().subscribe((models) => { + this.folders[TabType.PublicModels] = models; + this.searchTermsChanged(); + }); + //this.folders[TabType.PublicModels] = []; } refreshDatasets(selectedDatasetId: string | null) { this.datasetsService.getMyDatasets().subscribe((datasets) => { this.folders[TabType.MyDatasets] = datasets; + console.log(this.filteredFiles); if (selectedDatasetId) { this.selectFile(datasets.filter(x => x._id == selectedDatasetId)[0]); } @@ -208,7 +226,7 @@ export class FolderComponent implements AfterViewInit { this.predictorsForExp[exp._id].forEach(pred => { const model = this.folders[TabType.MyModels].find(model => model._id == pred.modelId); pred.name = model?.name!; - pred.lastUpdated = model?.lastUpdated!; + //pred.lastUpdated = model?.lastUpdated!; }) /* ------------------------------------------------ */ this.searchTermsChanged(); @@ -218,11 +236,14 @@ export class FolderComponent implements AfterViewInit { } saveNewFile() { + this.loadingAction = true; switch (this.type) { case FolderType.Dataset: this.formDataset!.uploadDataset((dataset: Dataset) => { this.newFile = undefined; - Shared.openDialog("Obaveštenje", "Uspešno ste dodali novi izvor podataka u kolekciju. Molimo sačekajte par trenutaka da se procesira."); + this.loadingAction = false; + this.okPressed.emit(); + //Shared.openDialog("Obaveštenje", "Uspešno ste dodali novi izvor podataka u kolekciju. Molimo sačekajte par trenutaka da se obradi."); this.refreshFiles(); }, () => { @@ -230,9 +251,11 @@ export class FolderComponent implements AfterViewInit { }); break; case FolderType.Model: + this.formModel.newModel.type = this.formModel.forProblemType; this.modelsService.addModel(this.formModel.newModel).subscribe(model => { this.newFile = undefined; - Shared.openDialog("Obaveštenje", "Uspešno ste dodali novu konfiguraciju neuronske mreže u kolekciju."); + this.loadingAction = false; + //Shared.openDialog("Obaveštenje", "Uspešno ste dodali novu konfiguraciju neuronske mreže u kolekciju."); this.refreshFiles(null, model._id); // todo select model }, (err) => { Shared.openDialog("Neuspeo pokušaj!", "Konfiguracija neuronske mreže sa unetim nazivom već postoji u Vašoj kolekciji. Izmenite naziv ili iskoristite postojeću konfiguraciju."); @@ -253,7 +276,12 @@ export class FolderComponent implements AfterViewInit { searchTermsChanged() { this.filteredFiles.length = 0; if (!this.files) return; - this.filteredFiles.push(...this.files.filter((file) => file.name.toLowerCase().includes(this.searchTerm.toLowerCase()))); + this.filteredFiles.push(...this.files.filter((file) => { + return (file.name.toLowerCase().includes(this.searchTerm.toLowerCase()) + && (!this.forExperiment + || this.type != FolderType.Model + || (this.type == FolderType.Model && (<Model>file).type == this.forExperiment.type))) + })); /*if (this.selectedFile) { if (!this.filteredFiles.includes(this.selectedFile)) { if (this.hoverTab === TabType.None && this.getFolderType(this.selectedTab) === this.type) { @@ -268,23 +296,136 @@ export class FolderComponent implements AfterViewInit { listView: boolean = true; - deleteFile(file: FolderFile, event: Event) { + loadingAction = false; + selectedFileHasChanges = false; + lastFileData = {}; + + onFileChange() { + console.log(this.selectedFile, this.lastFileData) + setTimeout(() => { + this.selectedFileHasChanges = !((this.selectedTab == TabType.NewFile) || isEqual(this.selectedFile, this.lastFileData)); + }); + } + + updateFile() { + const file = this.selectedFile; + this.loadingAction = true; + switch (this.type) { + case FolderType.Dataset: + this.datasetsService.editDataset(<Dataset>file).subscribe((response) => { + this.fileUpdatedSuccess(); + }); + break; + case FolderType.Model: + this.modelsService.editModel(<Model>file).subscribe((response) => { + this.fileUpdatedSuccess(); + }); + break; + } + } + + fileUpdatedSuccess() { + this.loadingAction = false; + this.selectedFileHasChanges = false; + Object.assign(this.lastFileData, this.selectedFile); + this.refreshFiles(); + } + + deleteFile(file: FolderFile, event: Event, deletePredictor: boolean = false) { event.stopPropagation(); - //console.log('delete'); + switch (this.type) { case FolderType.Dataset: - this.datasetsService.deleteDataset(<Dataset>file).subscribe((response) => { + const dataset = <Dataset>file; + Shared.openYesNoDialog("Obriši izvor podataka", "Eksperimenti i trenirani modeli nad ovim izvorom podataka će takođe biti obrisani, da li ste sigurni da želite da obrišete izvor: " + dataset.name + "?", () => { + this.filteredFiles.splice(this.filteredFiles.indexOf(file), 1); + this.files.splice(this.files.indexOf(file), 1); + this.loadingAction = true; + this.datasetsService.deleteDataset(dataset).subscribe((response) => { + this.loadingAction = false; + }); + }) + break; + case FolderType.Model: + const model = <Model>file; + Shared.openYesNoDialog("Obriši konfiguraciju neuronske mreže", "Trenirani modeli za ovu konfiguraciju će takođe biti obrisani, da li ste sigurni da želite da obrišete konfiguraciju: " + model.name + "?", () => { this.filteredFiles.splice(this.filteredFiles.indexOf(file), 1); + this.files.splice(this.files.indexOf(file), 1); + this.loadingAction = true; + this.modelsService.deleteModel(<Model>file).subscribe((response) => { + this.loadingAction = false; + }); + }) + + break; + case FolderType.Experiment: + if (deletePredictor) { + const predictor = <Predictor>file; + Shared.openYesNoDialog("Obriši trenirani model", "Da li ste sigurni da želite da obrišete trenirani model: " + predictor.name + "?", () => { + this.filteredFiles.splice(this.filteredFiles.indexOf(file), 1); + this.files.splice(this.files.indexOf(file), 1); + this.loadingAction = true; + this.predictorsService.deletePredictor(predictor).subscribe((response) => { + this.loadingAction = false; + }); + }); + } else { + const experiment = <Experiment>file; + Shared.openYesNoDialog("Obriši eksperiment", "Trenirani modeli za ovaj eksperiment će takođe biti obrisani, da li ste sigurni da želite da obrišete eksperiment: " + experiment.name + "?", () => { + this.filteredFiles.splice(this.filteredFiles.indexOf(file), 1); + this.files.splice(this.files.indexOf(file), 1); + this.loadingAction = true; + this.experimentsService.deleteExperiment(experiment).subscribe((response) => { + this.loadingAction = false; + }); + }); + } + break; + } + } + downloadFile(file: FolderFile, event: Event) { + event.stopPropagation(); + if (this.type == FolderType.Dataset) { + const fileId = (<Dataset>file).fileId; + const name = (<Dataset>file).name; + const ext = (<Dataset>file).extension; + if (fileId != undefined) + this.datasetsService.downloadFile(fileId).subscribe((response) => { + FileSaver.saveAs(response, name + ext); + + }); + + } + } + + addFile(file: FolderFile, event: Event) { + event.stopPropagation(); + switch (this.type) { + case FolderType.Dataset: + (<Dataset>file)._id = ""; + (<Dataset>file).isPreProcess = true; + (<Dataset>file).isPublic = false; + this.datasetsService.stealDataset(<Dataset>file).subscribe((response) => { + Shared.openDialog("Obaveštenje", "Uspešno ste dodali javni izvor podataka u vašu kolekciju."); this.refreshFiles(null); + }, (error: any) => { + if (error.error == "Dataset with this name already exists") { + Shared.openDialog("Obaveštenje", "Izvor podataka sa ovim imenom postoji u vašoj kolekciji."); + } }); break; case FolderType.Model: - this.modelsService.deleteModel(<Model>file).subscribe((response) => { + this.modelsService.stealModel(<Model>file).subscribe((response) => { + Shared.openDialog("Obaveštenje", "Uspešno ste dodali javnu konfiguraciju neuronske mreže u vašu kolekciju."); this.refreshFiles(null); + }, (error: any) => { + if (error.error == "Model already exisits or validation size is not between 0-1") { + Shared.openDialog("Obaveštenje", "Model sa ovim imenom postoji u vašoj kolekciji."); + } }); break; case FolderType.Experiment: - // this.experimentsService.deleteExperiment(<Model>file).subscribe((response) => { + // this.experimentsService.addExperiment(<Model>file).subscribe((response) => { // console.log(response); // }); //todo delete za predictor @@ -305,6 +446,7 @@ export class FolderComponent implements AfterViewInit { }; FolderType = FolderType; + ProblemType = ProblemType; Privacy = Privacy; TabType = TabType; @@ -325,6 +467,7 @@ export class FolderComponent implements AfterViewInit { setTimeout(() => { if (tab == TabType.NewFile) { this.selectNewFile(); + this.selectedFile=undefined!; } this.listView = this.getListView(tab); @@ -395,9 +538,16 @@ export class FolderComponent implements AfterViewInit { updateExperiment() { if (this.formModel) { - this.formModel.updateGraph(); + setTimeout(() => { + this.formModel.updateGraph(); + }); } } + + newTabTitles: { [tab: number]: string } = { + [FolderType.Dataset]: 'Novi izvor podataka', + [FolderType.Model]: 'Nova konfiguracija neuronske mreže', + }; } export enum Privacy { diff --git a/frontend/src/app/_elements/form-dataset/form-dataset.component.css b/frontend/src/app/_elements/form-dataset/form-dataset.component.css index 079711d0..43accd6d 100644 --- a/frontend/src/app/_elements/form-dataset/form-dataset.component.css +++ b/frontend/src/app/_elements/form-dataset/form-dataset.component.css @@ -7,7 +7,7 @@ .topBar { display: table; width: 100%; - height: 100px; + height: 80px; margin-left: 1.5%; margin-top: 1.5% ; } @@ -63,4 +63,8 @@ .file-container input{ border-radius: 5px; left: 0%; +} + +.naslov{ + font-size: 30px; }
\ No newline at end of file diff --git a/frontend/src/app/_elements/form-dataset/form-dataset.component.html b/frontend/src/app/_elements/form-dataset/form-dataset.component.html index b96276bd..6194a1a8 100644 --- a/frontend/src/app/_elements/form-dataset/form-dataset.component.html +++ b/frontend/src/app/_elements/form-dataset/form-dataset.component.html @@ -1,52 +1,51 @@ <div class="folderBox" *ngIf="dataset"> + <div class="topBar"> + <div class="kolona mb-3"> + <div class="fileButton"> + <button type="button" mat-raised-button (click)="fileInput.click()"><span *ngIf="!firstInput">Dodaj izvor podataka</span><span *ngIf="firstInput">{{filename}}</span></button> + </div> + </div> - - <div class="topBar"> - <div class="kolona mb-3"> - <div class="fileButton"> - <button type="button" mat-raised-button (click)="fileInput.click()">Dodaj izvor podataka</button> - - </div> - </div> - - <div class="kolona"> - <div role="group"> - <mat-form-field class="example-full-width" appearance="fill"> - <mat-label>Naziv</mat-label> - <input type="text" matInput value="{{dataset?.name}}" [(ngModel)]="dataset.name"> + <div class="kolona"> + <div role="group"> + <mat-form-field class="example-full-width" appearance="fill"> + <mat-label>Naziv</mat-label> + <input type="text" matInput value="{{dataset?.name}}" [(ngModel)]="dataset.name" (input)="editEvent.emit()"> - <mat-error *ngIf="nameFormControl.hasError('required')"> - Naziv je <strong>obavezan</strong> - </mat-error> - </mat-form-field> - </div> - </div> - <div class="kolona"> - <mat-form-field appearance="fill"> - <mat-label>Delimiter</mat-label> - <mat-select id="delimiterOptions" [(ngModel)]="dataset.delimiter" (selectionChange)="update()" value=","> - <mat-option *ngFor="let option of delimiterOptions" [value]="option"> - {{ option }} - </mat-option> - </mat-select> - </mat-form-field> - </div> + <mat-error *ngIf="nameFormControl.hasError('required')"> + Naziv je <strong>obavezan</strong> + </mat-error> + </mat-form-field> + </div> + </div> + <div class="kolona"> + <mat-form-field appearance="fill"> + <mat-label>Delimiter</mat-label> + <mat-select id="delimiterOptions" [(ngModel)]="dataset.delimiter" (selectionChange)="update(); editEvent.emit();" value=","> + <mat-option *ngFor="let option of delimiterOptions" [value]="option"> + {{ option }} + </mat-option> + </mat-select> + </mat-form-field> </div> - - <div class="row" *ngIf="firstInput"> - <label class=" mt-5">{{filename}}</label> - </div> + <div class="row" style="margin-right: 0;"> <div class="file-container" [ngClass]="{'dottedClass': !tableData.hasInput}"> <i class="material-icons-outlined icon-display" [ngClass]="{'hidden': tableData.hasInput}">file_upload</i> - <input class="file" id="file-upload" (change)="changeListener($event)" #fileInput type="file" accept=".csv"> - <div class="mt-5 datatable"> + <input class="file" id="file-upload" (change)="changeListener($event)" (valueChange)="dataset.isPreProcess = false; editEvent.emit()" #fileInput type="file" accept=".csv"> + + <div class="datatable"> + <div [ngClass]="{'hidden': (!existingFlag)}" class="text-center"> + <button mat-button (click)="goBack()"><mat-icon>keyboard_arrow_left</mat-icon></button> + <div style="display: inline;">{{(this.begin/10)+1}}...{{getPage()}}</div> + <button mat-button (click)="goForward()"><mat-icon>keyboard_arrow_right</mat-icon></button> + </div> <app-datatable [tableData]="tableData"></app-datatable> </div> diff --git a/frontend/src/app/_elements/form-dataset/form-dataset.component.ts b/frontend/src/app/_elements/form-dataset/form-dataset.component.ts index 94ef9905..35d68526 100644 --- a/frontend/src/app/_elements/form-dataset/form-dataset.component.ts +++ b/frontend/src/app/_elements/form-dataset/form-dataset.component.ts @@ -24,8 +24,12 @@ export class FormDatasetComponent { files: File[] = []; rowsNumber: number = 0; colsNumber: number = 0; + begin: number = 0; + step: number = 10; + existingFlag: boolean = false; @Input() dataset: Dataset; //dodaj ! potencijalno + @Output() editEvent = new EventEmitter(); tableData: TableData = new TableData(); @@ -40,10 +44,31 @@ export class FormDatasetComponent { } //@ViewChild('fileImportInput', { static: false }) fileImportInput: any; cemu je ovo sluzilo? + resetPagging() { + this.begin = 0; + } + goBack() { + if (this.begin - 10 < 0) + this.begin = 0; + else { + this.begin -= 10; + this.loadExisting(); + } - clear(){ + } + goForward() { + this.begin += 10; + if (this.dataset.rowCount < this.begin) + this.begin -= 10; + else + this.loadExisting(); + } + clear() { this.tableData.hasInput = false; } + getPage() { + return Math.ceil(this.dataset.rowCount / 10) + } changeListener($event: any): void { this.files = $event.srcElement.files; @@ -56,6 +81,7 @@ export class FormDatasetComponent { this.filename = this.files[0].name; this.tableData.loaded = false; + this.existingFlag = false; this.update(); } @@ -64,7 +90,6 @@ export class FormDatasetComponent { update() { this.firstInput = true - if (this.files.length < 1) return; @@ -90,23 +115,30 @@ export class FormDatasetComponent { this.dataset.name = this.filename.slice(0, this.filename.length - 4); } - loadExisting(){ + loadExisting() { + this.existingFlag = true; this.firstInput = false; this.tableData.hasInput = true; this.tableData.loaded = false; - - this.datasetsService.getDatasetFile(this.dataset.fileId).subscribe((file: string | undefined) => { - if (file) { - this.tableData.loaded = true; - this.tableData.numRows = this.dataset.rowCount; - this.tableData.numCols = this.dataset.columnInfo.length; - this.tableData.data = this.csv.csvToArray(file, (this.dataset.delimiter == "razmak") ? " " : (this.dataset.delimiter == "novi red") ? "\t" : this.dataset.delimiter); - - } + this.datasetsService.getDatasetHeader(this.dataset.fileId).subscribe((header: string | undefined) => { + + this.datasetsService.getDatasetFilePaging(this.dataset.fileId, this.begin, this.step).subscribe((file: string | undefined) => { + if (file) { + this.tableData.loaded = true; + this.tableData.numRows = this.dataset.rowCount; + this.tableData.numCols = this.dataset.columnInfo.length; + this.tableData.data = this.csv.csvToArray(header + '\n' + file, (this.dataset.delimiter == "razmak") ? " " : (this.dataset.delimiter == "novi red") ? "\t" : this.dataset.delimiter); + + } + else { + this.begin -= 10; + this.loadExisting(); + } + }); }); - + } /*exportAsXLSX():void { diff --git a/frontend/src/app/_elements/form-model/form-model.component.css b/frontend/src/app/_elements/form-model/form-model.component.css index 95ace1ef..34c1a5c5 100644 --- a/frontend/src/app/_elements/form-model/form-model.component.css +++ b/frontend/src/app/_elements/form-model/form-model.component.css @@ -86,12 +86,12 @@ hr { max-height: 20 rem; } -mat-slider { - width: 50%; -} - .slider { background-color: transparent; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } .center-center { @@ -102,3 +102,29 @@ mat-slider { font-size: 20px !important; font-weight: 600; } + +.slider-pad { + min-width: 10%; + background-color: gray; + height: 2px; +} + +.slider-pad:first-child { + margin-right: -8px; +} + +.slider-pad:last-child { + margin-left: -8px; +} + +.slider-extended { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 95%; +} + +.slide { + width: 80%; +}
\ No newline at end of file diff --git a/frontend/src/app/_elements/form-model/form-model.component.html b/frontend/src/app/_elements/form-model/form-model.component.html index 09e44a99..b986e154 100644 --- a/frontend/src/app/_elements/form-model/form-model.component.html +++ b/frontend/src/app/_elements/form-model/form-model.component.html @@ -5,18 +5,13 @@ <div class="ns-col"> <mat-form-field class="example-full-width" appearance="fill" class="mat-fix"> <mat-label>Naziv</mat-label> - <input type="text" matInput [(ngModel)]="newModel.name"> + <input type="text" matInput [(ngModel)]="newModel.name" (valueChange)="editEvent.emit()"> </mat-form-field> </div> <div class="ns-col"> <mat-form-field appearance="fill" class="mat-fix"> <mat-label>Tip problema</mat-label> - <mat-select [(ngModel)]="newModel.type" (selectionChange)="filterLossFunction()" *ngIf="this.hideProblemType" disabled="true"> - <mat-option *ngFor="let option of Object.keys(ProblemType); let optionName of Object.values(ProblemType)" [value]="option"> - {{ optionName }} - </mat-option> - </mat-select> - <mat-select [(ngModel)]="newModel.type" (selectionChange)="filterLossFunction()" *ngIf="!this.hideProblemType" disabled="false"> + <mat-select [(ngModel)]="forProblemType" [disabled]="this.hideProblemType" (valueChange)="editEvent.emit(); newModel.type = forProblemType;"> <mat-option *ngFor="let option of Object.keys(ProblemType); let optionName of Object.values(ProblemType)" [value]="option"> {{ optionName }} </mat-option> @@ -29,7 +24,7 @@ <div class="ns-col"> <mat-form-field appearance="fill" class="mat-fix"> <mat-label>Optimizacija</mat-label> - <mat-select [(ngModel)]="newModel.optimizer"> + <mat-select [(ngModel)]="newModel.optimizer" (valueChange)="editEvent.emit()"> <mat-option *ngFor="let option of Object.keys(Optimizer); let optionName of Object.values(Optimizer)" [value]="option"> {{ optionName }} </mat-option> @@ -40,8 +35,8 @@ <div class="ns-col"> <mat-form-field appearance="fill" class="mat-fix"> <mat-label>Funkcija troška</mat-label> - <mat-select [(ngModel)]="newModel.lossFunction"> - <mat-option *ngFor="let option of Object.keys(lossFunction); let optionName of Object.values(lossFunction)" [value]="option"> + <mat-select [(ngModel)]="lossFunction" (valueChange)="newModel.lossFunction = lossFunction!; editEvent.emit();"> + <mat-option *ngFor="let option of Object.keys(lossFunctions[forProblemType]); let optionName of Object.values(lossFunctions[forProblemType])" [value]="option"> {{ optionName }} </mat-option> </mat-select> @@ -53,7 +48,7 @@ <div class="ns-col"> <mat-form-field appearance="fill" class="mat-fix"> <mat-label>Funkcija aktivacije izlaznog sloja</mat-label> - <mat-select name="outputLayerActivationFunction" [(ngModel)]="newModel.outputLayerActivationFunction"> + <mat-select name="outputLayerActivationFunction" [(ngModel)]="outputLayerActivationFunction" (valueChange)="newModel.outputLayerActivationFunction = outputLayerActivationFunction!; editEvent.emit();"> <mat-option *ngFor="let option of Object.keys(ActivationFunction); let optionName of Object.values(ActivationFunction)" [value]="option"> {{ optionName }} </mat-option> @@ -63,7 +58,7 @@ <div class="ns-col"> <mat-form-field appearance="fill" class="mat-fix"> <mat-label>Stopa učenja</mat-label> - <mat-select [(ngModel)]="newModel.learningRate"> + <mat-select [(ngModel)]="newModel.learningRate" (valueChange)="editEvent.emit()"> <mat-option *ngFor="let option of Object.keys(LearningRate); let optionName of Object.values(LearningRate)" [value]="option"> {{ optionName }} </mat-option> @@ -76,14 +71,14 @@ <div class="ns-col"> <mat-form-field appearance="fill" class="mat-fix"> <mat-label>Broj epoha</mat-label> - <input type="number" matInput [(ngModel)]="newModel.epochs" min="1" max="1000"> + <input type="number" matInput [(ngModel)]="newModel.epochs" min="1" max="1000" (valueChange)="editEvent.emit()"> </mat-form-field> </div> <div class="ns-col"> <mat-form-field appearance="fill" class="mat-fix"> <mat-label>Broj uzoraka po iteraciji</mat-label> - <mat-select matNativeControl [(value)]="newModel.batchSize"> + <mat-select matNativeControl [(value)]="newModel.batchSize" (valueChange)="editEvent.emit()"> <mat-option *ngFor="let option of Object.keys(BatchSize); let optionName of Object.values(BatchSize)" [value]="option">{{option}}</mat-option> </mat-select> </mat-form-field> @@ -96,26 +91,38 @@ <div class="m-2"> <div class="row"> - <div class="col-sm-3 rounded" style="border:1px solid var(--ns-primary);margin-top: 10px;"> - <div class="row slider rounded mb-3" style="margin-left: 10px;"> - + <div class="col-sm-3 rounded" style="border:1px solid var(--ns-primary);margin-top: 10px; width: fit-content;"> + <!-- <label>Veličina skupa za validaciju: </label><b>{{validationSize}} %</b> <div class="text-center pt-3 pb-0 mb-0"><b>{{testSetDistribution}}%</b> : <b>{{100-testSetDistribution}}%</b></div> - <div class="text-center pt-0 mt-0">Trening - <mat-slider min="10" max="90" step="10" [(ngModel)]="testSetDistribution" (input)="updateTestSet($event)"></mat-slider> - Test</div> - - </div> - <div class="row slider rounded text-offwhite justify-content-center align-items-center" style="margin-left: 10px;"> - <mat-checkbox class="pt-4 mb-3" color="accent">Nasumični redosled podataka</mat-checkbox> + <div class="text-center pt-0 mt-0 p-0">Trening ////slider////// Test</div> + --> + <div class="slider mt-2"> + <div> + Trening {{testSetDistribution}}% : Test i validacija {{100-testSetDistribution}}% + </div> + <div class="slider-extended"> + <div class="slider-pad"></div> + <mat-slider class="slide" min="10" max="90" step="5" [(ngModel)]="testSetDistribution" (input)="updateTestSet($event)" (valueChange)="editEvent.emit()"></mat-slider> + <div class="slider-pad"></div> + </div> + <div> + Test {{validationSize}}% : Validacija {{100-validationSize}}% + </div> + <div class="slider-extended"> + <div class="slider-pad"></div> + <mat-slider class="slide" min="10" max="90" step="5" [(ngModel)]="validationSize" (input)="updateValidation($event)" (valueChange)="editEvent.emit()"></mat-slider> + <div class="slider-pad"></div> + </div> </div> - + <div class="d-flex justify-content-center mt-3"> + <mat-checkbox color="accent" (valueChange)="editEvent.emit()">Nasumični redosled podataka</mat-checkbox> + </div> </div> <div class="col-sm-9"> <!-- {{forExperiment._columnsSelected}} --> - <app-graph [model]="newModel" *ngIf="forExperiment._columnsSelected" [inputColumns]="getInputColumns()"></app-graph> - <app-graph [model]="newModel" *ngIf="!forExperiment._columnsSelected" [inputColumns]="['Nisu odabrane ulazne kolone']"></app-graph> + <app-graph [model]="newModel" [inputColumns]="getInputColumns()"></app-graph> </div> </div> </div> @@ -213,7 +220,7 @@ <mat-form-field appearance="fill" class="mat-fix"> <mat-label>Regularizacija</mat-label> - <mat-select [(ngModel)]="newModel.layers[i].regularisation"> + <mat-select [(ngModel)]="newModel.layers[i].regularisation" (valueChange)="editEvent.emit()"> <mat-option *ngFor="let option of Object.keys(Regularisation); let optionName of Object.values(Regularisation)" [value]="option"> {{ optionName }} </mat-option> diff --git a/frontend/src/app/_elements/form-model/form-model.component.ts b/frontend/src/app/_elements/form-model/form-model.component.ts index e01c2339..646af08c 100644 --- a/frontend/src/app/_elements/form-model/form-model.component.ts +++ b/frontend/src/app/_elements/form-model/form-model.component.ts @@ -15,15 +15,22 @@ export class FormModelComponent implements AfterViewInit { @ViewChild(GraphComponent) graph!: GraphComponent; @Input() forExperiment!: Experiment; @Output() selectedModelChangeEvent = new EventEmitter<Model>(); - @Input() hideProblemType:boolean; - @Input() forProblemType:ProblemType; + @Input() hideProblemType!: boolean; + @Input() forProblemType!: ProblemType; testSetDistribution: number = 70; - constructor() { - this.hideProblemType=false; - this.forProblemType=ProblemType.BinaryClassification; + validationSize: number = 15; + constructor() { } - ngAfterViewInit(): void { } + @Output() editEvent = new EventEmitter(); + + ngAfterViewInit(): void { + this.lossFunction = this.lossFunctions[this.forProblemType][0]; + this.outputLayerActivationFunction = this.outputLayerActivationFunctions[this.forProblemType][0]; + + this.newModel.lossFunction = this.lossFunction; + this.newModel.outputLayerActivationFunction = this.outputLayerActivationFunction; + } selectFormControl = new FormControl('', Validators.required); nameFormControl = new FormControl('', [Validators.required, Validators.email]); @@ -58,10 +65,21 @@ export class FormModelComponent implements AfterViewInit { term: string = ""; selectedMetrics = []; - lossFunction: any = LossFunction; + lossFunctions: { [index: string]: LossFunction[] } = { + [ProblemType.Regression]: LossFunctionRegression, + [ProblemType.BinaryClassification]: LossFunctionBinaryClassification, + [ProblemType.MultiClassification]: LossFunctionMultiClassification + }; + + outputLayerActivationFunctions: { [index: string]: ActivationFunction[] } = { + [ProblemType.Regression]: [ActivationFunction.Linear], + [ProblemType.BinaryClassification]: [ActivationFunction.Sigmoid], + [ProblemType.MultiClassification]: [ActivationFunction.Softmax] + }; loadModel(model: Model) { this.newModel = model; + this.forProblemType = model.type; } updateGraph() { @@ -74,17 +92,20 @@ export class FormModelComponent implements AfterViewInit { this.newModel.layers.splice(this.newModel.layers.length - 1, 1); this.newModel.hiddenLayers -= 1; this.updateGraph(); + this.editEvent.emit(); } } + addLayer() { - if (this.newModel.hiddenLayers < 128) { + if (this.newModel.hiddenLayers < 150) { this.newModel.layers.push(new Layer(this.newModel.layers.length, this.selectedActivation, this.selectedNumberOfNeurons, this.selectedRegularisation, this.selectedRegularisationRate)); this.newModel.hiddenLayers += 1; this.updateGraph(); + this.editEvent.emit(); } - } + numSequence(n: number): Array<number> { return Array(n); } @@ -93,59 +114,64 @@ export class FormModelComponent implements AfterViewInit { if (this.newModel.layers[index].neurons > 1) { this.newModel.layers[index].neurons -= 1; this.updateGraph(); + this.editEvent.emit(); } } + addNeuron(index: number) { if (this.newModel.layers[index].neurons < 18) { this.newModel.layers[index].neurons += 1; this.updateGraph(); + this.editEvent.emit(); } } + selectedActivation: ActivationFunction = ActivationFunction.Sigmoid; selectedRegularisationRate: RegularisationRate = RegularisationRate.RR1; selectedRegularisation: Regularisation = Regularisation.L1; selectedNumberOfNeurons: number = 3; + lossFunction: LossFunction = LossFunction.MeanAbsoluteError; + outputLayerActivationFunction: ActivationFunction = ActivationFunction.Linear; + changeAllActivation() { for (let i = 0; i < this.newModel.layers.length; i++) { this.newModel.layers[i].activationFunction = this.selectedActivation; } + this.editEvent.emit(); } changeAllRegularisation() { for (let i = 0; i < this.newModel.layers.length; i++) { this.newModel.layers[i].regularisation = this.selectedRegularisation; } + this.editEvent.emit(); } changeAllRegularisationRate() { for (let i = 0; i < this.newModel.layers.length; i++) { this.newModel.layers[i].regularisationRate = this.selectedRegularisationRate; } + this.editEvent.emit(); } changeAllNumberOfNeurons() { for (let i = 0; i < this.newModel.layers.length; i++) { this.newModel.layers[i].neurons = this.selectedNumberOfNeurons; - this.updateGraph(); } + this.updateGraph(); + this.editEvent.emit(); } updateTestSet(event: MatSliderChange) { this.testSetDistribution = event.value!; } - filterLossFunction() { - if(this.newModel.type==ProblemType.Regression){ - this.lossFunction = LossFunctionRegression; - this.newModel.lossFunction=LossFunction.MeanSquaredError; - } - else if(this.newModel.type==ProblemType.BinaryClassification){ - this.lossFunction= LossFunctionBinaryClassification; - this.newModel.lossFunction=LossFunction.BinaryCrossEntropy; - } - else if(this.newModel.type==ProblemType.MultiClassification){ - this.lossFunction = LossFunctionMultiClassification; - this.newModel.lossFunction=LossFunction.SparseCategoricalCrossEntropy; - } - -} -getInputColumns() { - return this.forExperiment.inputColumns.filter(x => x != this.forExperiment.outputColumn); -} + + getInputColumns() { + if (this.forExperiment) + return this.forExperiment.inputColumns.filter(x => x != this.forExperiment.outputColumn); + else + return ['Nisu odabrane ulazne kolone.'] + } + + updateValidation(event: MatSliderChange) { + this.validationSize = event.value!; + this.editEvent.emit(); + } } diff --git a/frontend/src/app/_elements/graph/graph.component.html b/frontend/src/app/_elements/graph/graph.component.html index 35753d40..411e8b53 100644 --- a/frontend/src/app/_elements/graph/graph.component.html +++ b/frontend/src/app/_elements/graph/graph.component.html @@ -1,4 +1,4 @@ -<div #graphWrapper class="w-100 position-relative" style="height: 14rem;"> +<div #graphWrapper class="position-relative" style="height: 14rem; width: 100%;"> <!-- <ng-container *ngFor="let layer of layers; let i = index"> <div [ngClass]="{'inputs': i==0}" class="node-text" *ngFor="let node of layer; let j = index" [style.left.%]="node.x * 99.4" [style.top.%]="node.y * 100"> {{ i == 0 ? (inputColumns && inputColumns.length >= j ? inputColumns[j] : 'nepoznato') : (i > 0 && i diff --git a/frontend/src/app/_elements/metric-view/metric-view.component.ts b/frontend/src/app/_elements/metric-view/metric-view.component.ts index fbca2edf..65b1ef9b 100644 --- a/frontend/src/app/_elements/metric-view/metric-view.component.ts +++ b/frontend/src/app/_elements/metric-view/metric-view.component.ts @@ -16,7 +16,7 @@ export class MetricViewComponent implements OnInit { history: any[] = []; - update(history: any[]) { + update(history: any[],totalEpochs:number) { const myAcc: number[] = []; const myMae: number[] = []; const myMse: number[] = []; @@ -29,7 +29,15 @@ export class MetricViewComponent implements OnInit { const myEpochs: number[] = []; this.history = history; this.history.forEach((metrics, epoch) => { - myEpochs.push(epoch + 1); + if(totalEpochs>100) + { + let epochEstimate=epoch*Math.round(Math.sqrt(totalEpochs)) + if(epochEstimate>totalEpochs) + epochEstimate=totalEpochs; + myEpochs.push(epochEstimate); + } + else + myEpochs.push(epoch + 1); for (let key in metrics) { let value = metrics[key]; //console.log(key, ':::', value, epoch); diff --git a/frontend/src/app/_elements/navbar/navbar.component.html b/frontend/src/app/_elements/navbar/navbar.component.html index 105151aa..8cc742eb 100644 --- a/frontend/src/app/_elements/navbar/navbar.component.html +++ b/frontend/src/app/_elements/navbar/navbar.component.html @@ -9,7 +9,7 @@ <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0"> <li><a routerLink="experiment" class="nav-link px-2" [class]="(currentUrl === '/experiment') ? 'text-primary' : 'text-offwhite'">Napravi eksperiment</a> </li> - <li><a routerLink="archive" class="nav-link px-2" [class]="(currentUrl === '/archive') ? 'text-primary' : 'text-offwhite'">Arhiva</a> + <li><a routerLink="archive" class="nav-link px-2" [class]="(currentUrl === '/archive') ? 'text-primary' : 'text-offwhite'">Kolekcije</a> </li> </ul> diff --git a/frontend/src/app/_elements/spinner/spinner.component.css b/frontend/src/app/_elements/spinner/spinner.component.css new file mode 100644 index 00000000..78adc872 --- /dev/null +++ b/frontend/src/app/_elements/spinner/spinner.component.css @@ -0,0 +1,78 @@ +.wrap { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.loader { + color: #ffffff; + font-size: 20px; + margin: auto; + width: 1em; + height: 1em; + border-radius: 50%; + text-indent: -9999em; + -webkit-animation: load4 1.3s infinite linear; + animation: load4 1.3s infinite linear; + -webkit-transform: scale(0.2); + -ms-transform: scale(0.2); + transform: scale(0.2); +} + +@-webkit-keyframes load4 { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +@keyframes load4 { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +}
\ No newline at end of file diff --git a/frontend/src/app/_elements/spinner/spinner.component.html b/frontend/src/app/_elements/spinner/spinner.component.html new file mode 100644 index 00000000..c655abf0 --- /dev/null +++ b/frontend/src/app/_elements/spinner/spinner.component.html @@ -0,0 +1,3 @@ +<div class="wrap"> + <div class="loader">Loading...</div> +</div>
\ No newline at end of file diff --git a/frontend/src/app/_elements/spinner/spinner.component.spec.ts b/frontend/src/app/_elements/spinner/spinner.component.spec.ts new file mode 100644 index 00000000..061f78d5 --- /dev/null +++ b/frontend/src/app/_elements/spinner/spinner.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SpinnerComponent } from './spinner.component'; + +describe('SpinnerComponent', () => { + let component: SpinnerComponent; + let fixture: ComponentFixture<SpinnerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SpinnerComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SpinnerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/_elements/spinner/spinner.component.ts b/frontend/src/app/_elements/spinner/spinner.component.ts new file mode 100644 index 00000000..f0080edd --- /dev/null +++ b/frontend/src/app/_elements/spinner/spinner.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-spinner', + templateUrl: './spinner.component.html', + styleUrls: ['./spinner.component.css'] +}) +export class SpinnerComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/frontend/src/app/_modals/alert-dialog/alert-dialog.component.html b/frontend/src/app/_modals/alert-dialog/alert-dialog.component.html index 2d7e4d86..84793260 100644 --- a/frontend/src/app/_modals/alert-dialog/alert-dialog.component.html +++ b/frontend/src/app/_modals/alert-dialog/alert-dialog.component.html @@ -1,9 +1,9 @@ - - - <h2 mat-dialog-title >{{data.title}}</h2> - <div mat-dialog-content class="mt-4 text-offwhite" > - {{data.message}} - </div> - <div mat-dialog-actions class="d-flex justify-content-center mt-4"> +<h2 mat-dialog-title>{{data.title}}</h2> +<div mat-dialog-content class="mt-4 text-offwhite"> + <form (keydown)="withEnterKey($event)"> + {{data.message}} + </form> +</div> +<div mat-dialog-actions class="d-flex justify-content-center mt-4"> <button mat-raised-button cdkFocusInitial (click)="onOkClick()" color="basic">OK</button> - </div>
\ No newline at end of file +</div>
\ No newline at end of file diff --git a/frontend/src/app/_modals/alert-dialog/alert-dialog.component.ts b/frontend/src/app/_modals/alert-dialog/alert-dialog.component.ts index e15f3c6f..7558b527 100644 --- a/frontend/src/app/_modals/alert-dialog/alert-dialog.component.ts +++ b/frontend/src/app/_modals/alert-dialog/alert-dialog.component.ts @@ -20,9 +20,11 @@ export class AlertDialogComponent { //public dialog: MatDialog ) {} + withEnterKey(keyboardEvent: KeyboardEvent) { + if (keyboardEvent.code == "Enter" || keyboardEvent.code == "NumpadEnter") + this.onOkClick(); + } onOkClick(): void { this.dialogRef.close(); } - - } diff --git a/frontend/src/app/_modals/encoding-dialog/encoding-dialog.component.html b/frontend/src/app/_modals/encoding-dialog/encoding-dialog.component.html index 08c1f26b..fed8f8d7 100644 --- a/frontend/src/app/_modals/encoding-dialog/encoding-dialog.component.html +++ b/frontend/src/app/_modals/encoding-dialog/encoding-dialog.component.html @@ -1,15 +1,17 @@ <h2 mat-dialog-title class="text-center">Enkodiranje svih kolona</h2> <div mat-dialog-content class="mt-5 mb-4"> <p>Sve izabrane kolone biće enkodirane metodom:</p> - <form (keydown)="withEnterKey($event)"> - <mat-form-field> - <mat-select matNativeControl [(value)]="selectedEncodingType" cdkFocusInitial> - <mat-option *ngFor="let option of Object.keys(Encoding); let optionName of Object.values(Encoding)" [value]="option"> - {{ optionName }} - </mat-option> - </mat-select> - </mat-form-field> - </form> + <div class="d-flex justify-content-center"> + <form (keydown)="withEnterKey($event)"> + <mat-form-field> + <mat-select matNativeControl [(value)]="selectedEncodingType" cdkFocusInitial> + <mat-option *ngFor="let option of Object.keys(Encoding); let optionName of Object.values(Encoding)" [value]="option"> + {{ optionName }} + </mat-option> + </mat-select> + </mat-form-field> + </form> + </div> </div> <div mat-dialog-actions class="justify-content-center"> <button id="btnYes" mat-stroked-button color="basic" (click)="onYesClick()">Potvrdi</button> diff --git a/frontend/src/app/_modals/missingvalues-dialog/missingvalues-dialog.component.css b/frontend/src/app/_modals/missingvalues-dialog/missingvalues-dialog.component.css index e99a1e1e..64d7bd21 100644 --- a/frontend/src/app/_modals/missingvalues-dialog/missingvalues-dialog.component.css +++ b/frontend/src/app/_modals/missingvalues-dialog/missingvalues-dialog.component.css @@ -5,4 +5,8 @@ #btnNo { color: gray; +} + +::ng-deep.mat-dialog-content { + overflow: visible; }
\ No newline at end of file diff --git a/frontend/src/app/_modals/missingvalues-dialog/missingvalues-dialog.component.html b/frontend/src/app/_modals/missingvalues-dialog/missingvalues-dialog.component.html index 7ab92d02..3332ef41 100644 --- a/frontend/src/app/_modals/missingvalues-dialog/missingvalues-dialog.component.html +++ b/frontend/src/app/_modals/missingvalues-dialog/missingvalues-dialog.component.html @@ -2,8 +2,8 @@ <div mat-dialog-content class="mt-5 mb-4"> <form (keydown)="withEnterKey($event)"> <mat-radio-group [(ngModel)]="selectedMissingValuesOption" [ngModelOptions]="{standalone: true}"> - <mat-radio-button [value]="NullValueOptions.DeleteColumns" checked>Obriši sve kolone koje sadrže nedostajuće vrednosti</mat-radio-button> - <mat-radio-button [value]="NullValueOptions.DeleteRows">Obriši sve redove koji sadrže nedostajuće vrednosti</mat-radio-button> + <mat-radio-button [value]="NullValueOptions.DeleteColumns" checked>Isključi sve kolone koje sadrže nedostajuće vrednosti</mat-radio-button> + <mat-radio-button [value]="NullValueOptions.DeleteRows" class="mt-1">Obriši sve redove koji sadrže nedostajuće vrednosti</mat-radio-button> </mat-radio-group> </form> </div> diff --git a/frontend/src/app/_modals/register-modal/register-modal.component.html b/frontend/src/app/_modals/register-modal/register-modal.component.html index 0c791a61..5182198c 100644 --- a/frontend/src/app/_modals/register-modal/register-modal.component.html +++ b/frontend/src/app/_modals/register-modal/register-modal.component.html @@ -33,6 +33,7 @@ <mat-icon matSuffix></mat-icon> </mat-form-field> <p *ngIf="wrongUsernameBool" class="wrong-creds">Unesite ispravno korisničko ime.</p> + <p *ngIf="usernameAlreadyExistsBool" class="wrong-creds">Uneto korisničko ime je zauzeto.</p> </div> <!--Email--> <div> @@ -42,6 +43,7 @@ <mat-icon matSuffix></mat-icon> </mat-form-field> <p *ngIf="wrongEmailBool" class="wrong-creds">Unesite ispravno e-mail adresu.</p> + <p *ngIf="emailAlreadyExistsBool" class="wrong-creds">Uneta e-mail adresa je zauzeta.</p> </div> <!-- Lozinka 1. --> <div> diff --git a/frontend/src/app/_modals/register-modal/register-modal.component.ts b/frontend/src/app/_modals/register-modal/register-modal.component.ts index a5c6ddc6..b1129668 100644 --- a/frontend/src/app/_modals/register-modal/register-modal.component.ts +++ b/frontend/src/app/_modals/register-modal/register-modal.component.ts @@ -25,9 +25,11 @@ export class RegisterModalComponent implements OnInit { wrongEmailBool: boolean = false; wrongPass1Bool: boolean = false; wrongPass2Bool: boolean = false; + usernameAlreadyExistsBool: boolean = false; + emailAlreadyExistsBool: boolean = false; pattName: RegExp = /^[a-zA-ZšŠđĐčČćĆžŽ]+([ \-][a-zA-ZšŠđĐčČćĆžŽ]+)*$/; - pattUsername: RegExp = /^[a-zA-Z0-9]{6,18}$/; + pattUsername: RegExp = /^[a-zA-Z0-9]{4,18}$/; pattTwoSpaces: RegExp = / /; pattEmail: RegExp = /^[a-zA-Z0-9]+([\.\-\+][a-zA-Z0-9]+)*\@([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}$/; pattPassword: RegExp = /.{6,30}$/; @@ -59,7 +61,7 @@ export class RegisterModalComponent implements OnInit { } resetData() { this.firstName = this.lastName = this.username = this.email = this.pass1 = this.pass2 = ''; - this.wrongFirstNameBool = this.wrongLastNameBool = this.wrongUsernameBool = this.wrongEmailBool = this.wrongPass1Bool = this.wrongPass2Bool = false; + this.wrongFirstNameBool = this.wrongLastNameBool = this.wrongUsernameBool = this.wrongEmailBool = this.wrongPass1Bool = this.wrongPass2Bool = this.usernameAlreadyExistsBool = this.emailAlreadyExistsBool = false; this.password1Shown = false; this.password2Shown = false; } @@ -160,10 +162,13 @@ export class RegisterModalComponent implements OnInit { dateCreated:new Date() } + this.authService.register(user) .subscribe( (response) => { if (response == 'User added') { + this.usernameAlreadyExistsBool = false; + this.emailAlreadyExistsBool = false; //nakon sto je registrovan, nek bude ulogovan this.authService.login(this.username, this.pass1).subscribe((response) => { @@ -174,12 +179,16 @@ export class RegisterModalComponent implements OnInit { }, (error) => console.warn(error)); } else if (response == 'Email Already Exists') { - shared.openDialog("Greška!", "Nalog sa unetim email-om već postoji!"); - (<HTMLSelectElement>document.getElementById('email')).focus(); + /*shared.openDialog("Greška!", "Nalog sa unetim email-om već postoji!"); + (<HTMLSelectElement>document.getElementById('email')).focus();*/ + this.usernameAlreadyExistsBool = false; + this.emailAlreadyExistsBool = true; } else if (response == 'Username Already Exists') { - shared.openDialog("Greška!", "Nalog sa unetim korisničkim imenom već postoji!"); - (<HTMLSelectElement>document.getElementById('username-register')).focus(); + /*shared.openDialog("Greška!", "Nalog sa unetim korisničkim imenom već postoji!"); + (<HTMLSelectElement>document.getElementById('username-register')).focus();*/ + this.emailAlreadyExistsBool = false; + this.usernameAlreadyExistsBool = true; } } ); diff --git a/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.css b/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.css index e69de29b..e99a1e1e 100644 --- a/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.css +++ b/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.css @@ -0,0 +1,8 @@ +#btnYes { + background-color: var(--offwhite); + color: var(--ns-bg-dark-100); +} + +#btnNo { + color: gray; +}
\ No newline at end of file diff --git a/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.html b/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.html index 06e74093..0470d395 100644 --- a/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.html +++ b/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.html @@ -1,8 +1,10 @@ -<h2 mat-dialog-title class="text-muted">{{data.title}}</h2> -<div mat-dialog-content class="mt-4" style="color: rgb(81, 76, 76);"> - {{data.message}} +<h2 mat-dialog-title class="text-center">{{data.title}}</h2> +<div mat-dialog-content class="mt-5 mb-4 mx-1"> + <form (keydown)="withEnterKey($event)"> + {{data.message}} + </form> </div> <div mat-dialog-actions class="d-flex justify-content-center mt-4"> - <button mat-button cdkFocusInitial (click)="onYesClick()" style="background-color: lightgray;">Da</button> - <button mat-button cdkFocusInitial (click)="onNoClick()" style="background-color: lightgray;">Ne</button> + <button id="btnYes" mat-stroked-button color="basic" (click)="onYesClick()">Da</button> + <button id="btnNo" mat-stroked-button (click)="onNoClick()">Ne</button> </div>
\ No newline at end of file diff --git a/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.ts b/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.ts index de1cdd4f..a7db1e7f 100644 --- a/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.ts +++ b/frontend/src/app/_modals/yes-no-dialog/yes-no-dialog.component.ts @@ -24,10 +24,13 @@ export class YesNoDialogComponent { onNoClick(): void { this.dialogRef.close(); } - onYesClick():void{ + + withEnterKey(keyboardEvent: KeyboardEvent) { + if (keyboardEvent.code == "Enter" || keyboardEvent.code == "NumpadEnter") + this.onYesClick(); + } + onYesClick():void { this.data.yesFunction(); this.dialogRef.close(); } - - } diff --git a/frontend/src/app/_pages/archive/archive.component.css b/frontend/src/app/_pages/archive/archive.component.css index e69de29b..3a8145dc 100644 --- a/frontend/src/app/_pages/archive/archive.component.css +++ b/frontend/src/app/_pages/archive/archive.component.css @@ -0,0 +1,3 @@ +.pro-center{ + width: 80%; +}
\ No newline at end of file diff --git a/frontend/src/app/_pages/archive/archive.component.html b/frontend/src/app/_pages/archive/archive.component.html index f9cce56b..0c5fa8aa 100644 --- a/frontend/src/app/_pages/archive/archive.component.html +++ b/frontend/src/app/_pages/archive/archive.component.html @@ -1,5 +1,5 @@ <div class="d-flex flex-column align-items-center my-5"> - <app-folder [archive]="true" [startingTab]="TabType.MyExperiments"></app-folder> + <app-folder [archive]="true" [startingTab]="TabType.MyExperiments" class="pro-center"></app-folder> <!--<div class="my-5" style="height: fit-content;"> <app-playlist [datasets]="publicDatasets"></app-playlist> diff --git a/frontend/src/app/_pages/experiment/experiment.component.css b/frontend/src/app/_pages/experiment/experiment.component.css index 4c063f87..59e004e9 100644 --- a/frontend/src/app/_pages/experiment/experiment.component.css +++ b/frontend/src/app/_pages/experiment/experiment.component.css @@ -36,32 +36,42 @@ mat-stepper { display: flex; flex-direction: column; width: 100%; + height: 100%; overflow-y: auto; } .step-content { position: relative; - width: 100%; + width: 98%; + min-height: 100%; display: flex; flex-direction: row; justify-content: center; align-items: center; + /*margin-bottom: 100px;*/ } .step-content-inside { width: 98%; height: 98%; - overflow-y: auto; } -.ekspName{ + +.ekspName { font-weight: bold; font-size: large; } - -.addedElement{ - color:var(--ns-accent); +.addedElement { + color: var(--ns-accent); } + .text-overflow { overflow-wrap: break-word; + width: 170px; + display: inline-block; + text-align: left; } + +.text-overflow-experiment-name { + overflow-wrap: break-word; +}
\ No newline at end of file diff --git a/frontend/src/app/_pages/experiment/experiment.component.html b/frontend/src/app/_pages/experiment/experiment.component.html index a2ede838..17a6539d 100644 --- a/frontend/src/app/_pages/experiment/experiment.component.html +++ b/frontend/src/app/_pages/experiment/experiment.component.html @@ -1,47 +1,52 @@ <div class="container-fluid p-0 text-offwhite holder" style="height: calc(100vh - 64px); text-align: center;"> <div class="d-flex flex-colum align-items-center sidenav"> <div> - <div class="ekspName " style="width: 250px;"> - <div class="text-overflow" *ngIf="experiment.name=='exp1'" style="width: 250px;"> - Novi Eksperiment - </div> - <div class="text-overflow" *ngIf="experiment.name!='exp1'" style="width: 250px;"> - {{experiment.name}} - </div> + <div class="ekspName " style="width: 250px;"> + <div class="text-overflow-experiment-name" *ngIf="experiment.name=='exp1'" style="width: 250px;"> + Novi Eksperiment + </div> + <div class="text-overflow-experiment-name" *ngIf="experiment.name!='exp1'" style="width: 250px;"> + {{experiment.name}} + </div> - </div> - <mat-stepper orientation="vertical" (selectionChange)="changePage($event)"> - <mat-step> - <!--editable="false"--> - <ng-template matStepLabel> - <span class="label" *ngIf="dataset==undefined">Izvor podataka</span> - <span class="label addedElement" *ngIf="dataset!=undefined">{{dataset.name}}</span> - </ng-template> - <ng-template matStepContent> - <p class="text-left">Izaberite vas izvor podataka</p> - </ng-template> - </mat-step> - <mat-step> - <ng-template matStepLabel> - <span class="label addedElement" *ngIf="experiment.name!='exp1'">Predvideti:{{experiment.outputColumn}}</span> - <span class="label" *ngIf="experiment.name=='exp1'">Odabir kolona</span> - </ng-template> - <ng-template matStepContent> - <p class="text-left">Pripremite podatke i izaberite izlazne kolone</p> - </ng-template> - </mat-step> - <mat-step> - <ng-template matStepLabel> - <span class="label addedElement" *ngIf="modelToTrain!=undefined">{{modelToTrain.name}}</span> - <span class="label" *ngIf="modelToTrain==undefined">Treniranje</span> - </ng-template> - <p class="text-left">Odaberite parametre i trenirajte model</p> - </mat-step> - <mat-step> - <ng-template matStepLabel><span class="label">Pregled rezultata treniranja</span></ng-template> - <p class="text-left">Pregledajte tok treniranja i grafički prikaz rezultata</p> - </mat-step> - </mat-stepper> + </div> + <mat-stepper orientation="vertical" (selectionChange)="changePage($event)" [linear]="true"> + <mat-step [completed]="this.step1"> + <!--editable="false"--> + <ng-template matStepLabel> + <span class="label text-overflow" *ngIf="dataset==undefined">Izvor podataka </span> + <span class="label addedElement text-overflow" *ngIf="dataset!=undefined">{{dataset.name}}</span> + </ng-template> + <ng-template matStepContent> + <p class="text-left text-overflow">Izaberite izvor podataka</p> + </ng-template> + </mat-step> + <mat-step [completed]="experiment._id!=''"> + <ng-template matStepLabel> + <span class="label addedElement text-overflow" *ngIf="experiment._id!=''">Predvideti:{{experiment.outputColumn}}</span> + <span *ngIf="!this.step1" class="align-middle"><mat-icon>lock</mat-icon></span> + <span class="label text-overflow" *ngIf="experiment._id==''">Odabir kolona </span> + </ng-template> + <ng-template matStepContent> + <p class="text-left text-overflow">Pripremite podatke i izaberite izlazne kolone</p> + </ng-template> + </mat-step> + <mat-step [completed]="this.step3"> + <ng-template matStepLabel> + <span *ngIf="experiment._id==''" class="align-middle"><mat-icon>lock</mat-icon></span> + <span class="label addedElement text-overflow" *ngIf="modelToTrain!=undefined">{{modelToTrain.name}}</span> + <span class="label text-overflow" *ngIf="modelToTrain==undefined">Treniranje</span> + </ng-template> + <p class="text-left text-overflow">Odaberite parametre i trenirajte model</p> + </mat-step> + <mat-step [completed]="this.step4"> + <ng-template matStepLabel> + <span *ngIf="!this.step3" class="align-middle"><mat-icon>lock</mat-icon></span> + <span class="label text-overflow align-middle">Pregled rezultata<br> treniranja</span> + </ng-template> + <p class="text-left text-overflow">Pregledajte tok treniranja i<br> grafički prikaz rezultata</p> + </mat-step> + </mat-stepper> </div> </div> <div #stepsContainer class="steps-container"> @@ -50,20 +55,20 @@ <app-folder #folderDataset [type]="FolderType.Dataset" [forExperiment]="experiment" [startingTab]="TabType.NewFile" [tabsToShow]="[TabType.MyDatasets, TabType.PublicDatasets]" (okPressed)="goToPage(1)" (selectedFileChanged)="setDataset($event)"></app-folder> </div> </div> - <div #steps id="step_2" class="step-content"> + <div #steps id="step_2" class="step-content" *ngIf="step1"> <div class="step-content-inside"> <app-column-table (okPressed)="goToPage(2); experiment._columnsSelected = true;" (columnTableChanged)="columnTableChangedEvent()" (experimentChanged)="experimentChangedEvent()" [experiment]="experiment" [dataset]="dataset"></app-column-table> </div> </div> - <div #steps id="step_3" class="step-content"> + <div #steps id="step_3" class="step-content" *ngIf="step2"> <div class="step-content-inside"> <app-folder #folderModel [type]="FolderType.Model" [forExperiment]="experiment" [startingTab]="TabType.NewFile" [tabsToShow]="[TabType.MyModels]" (okPressed)="goToPage(3); trainModel();" (selectedFileChanged)="setModel($event)"></app-folder> </div> </div> - <div #steps id="step_4" class="step-content"> + <div #steps id="step_4" class="step-content" *ngIf="step3"> <div class="step-content-inside"> <app-metric-view #metricView></app-metric-view> </div> </div> </div> -</div> +</div>
\ No newline at end of file diff --git a/frontend/src/app/_pages/experiment/experiment.component.ts b/frontend/src/app/_pages/experiment/experiment.component.ts index abf4b697..398321f2 100644 --- a/frontend/src/app/_pages/experiment/experiment.component.ts +++ b/frontend/src/app/_pages/experiment/experiment.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, ElementRef, ViewChild, ViewChildren, Input } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, ViewChild, ViewChildren, Input, OnInit } from '@angular/core'; import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { MatStepper } from '@angular/material/stepper'; import Shared from 'src/app/Shared'; @@ -12,6 +12,9 @@ import Dataset from 'src/app/_data/Dataset'; import { ColumnTableComponent } from 'src/app/_elements/column-table/column-table.component'; import { SignalRService } from 'src/app/_services/signal-r.service'; import { MetricViewComponent } from 'src/app/_elements/metric-view/metric-view.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DatasetsService } from 'src/app/_services/datasets.service'; +import { PredictorsService } from 'src/app/_services/predictors.service'; @Component({ selector: 'app-experiment', @@ -32,7 +35,12 @@ export class ExperimentComponent implements AfterViewInit { @ViewChild("folderModel") folderModel!: FolderComponent; @ViewChild("metricView") metricView!: MetricViewComponent; - constructor(private experimentsService: ExperimentsService, private modelsService: ModelsService, private signalRService: SignalRService) { + step1: boolean = false; + step2: boolean = false; + step3: boolean = false; + step4: boolean = false; + + constructor(private experimentsService: ExperimentsService, private modelsService: ModelsService, private datasetsService: DatasetsService, private predictorsService: PredictorsService, private signalRService: SignalRService, private route: ActivatedRoute) { this.experiment = new Experiment("exp1"); } @@ -49,6 +57,7 @@ export class ExperimentComponent implements AfterViewInit { Shared.openDialog('Greška', 'Morate odabrati konfiguraciju neuronske mreže'); } else { this.modelsService.trainModel(this.modelToTrain._id, this.experiment._id).subscribe(() => { console.log("pocelo treniranje") }); + this.step4 = true; } } @@ -82,11 +91,57 @@ export class ExperimentComponent implements AfterViewInit { stat = stat.replace(/'/g, '"'); //console.log('JSON', this.trainingResult); this.history.push(JSON.parse(stat)); - this.metricView.update(this.history); + this.metricView.update(this.history,this.modelToTrain.epochs); } }); } + + this.route.queryParams.subscribe(params => { + + let experimentId = this.route.snapshot.paramMap.get("id"); + let predictorId = this.route.snapshot.paramMap.get("predictorId"); + console.log("paramexp: ", experimentId, ", parampredictor: ", predictorId); + if (predictorId != null) { + this.predictorsService.getPredictor(predictorId!).subscribe((response) => { + let predictor = response; + //console.log("predictor: ", predictor); + this.experimentsService.getExperimentById(predictor.experimentId).subscribe((response) => { + this.experiment = response; + //console.log("experiment: ", this.experiment); + this.datasetsService.getDatasetById(this.experiment.datasetId).subscribe((response: Dataset) => { + this.dataset = response; + //console.log("dataset: ", this.dataset); + this.folderDataset.forExperiment = this.experiment; + this.folderDataset.selectFile(this.dataset); //sad 3. i 4. korak da se ucitaju + + this.modelsService.getModelById(predictor.modelId).subscribe((response) => { + let model = response; + //console.log("model: ", model); + this.folderModel.formModel.newModel = model; + this.step3 = true; + let numOfEpochsArray = Array.from({length: model.epochs}, (_, i) => i + 1); + //console.log("metric view1:", this.metricView); + setTimeout(() => { + this.metricView.linechartComponent.update(numOfEpochsArray, predictor.metricsAcc, predictor.metricsLoss, predictor.metricsMae, predictor.metricsMse, predictor.metricsValAcc, predictor.metricsValLoss, predictor.metricsValMae, predictor.metricsValMse); + }) + }); + }); + }); + }); + } + else if (experimentId != null) { + this.experimentsService.getExperimentById(experimentId).subscribe((response) => { + this.experiment = response; + this.datasetsService.getDatasetById(this.experiment.datasetId).subscribe((response: Dataset) => { + this.dataset = response; + this.folderDataset.forExperiment = this.experiment; + this.folderDataset.selectFile(this.dataset); + }); + }); + } + + }); } history: any[] = []; @@ -148,16 +203,27 @@ export class ExperimentComponent implements AfterViewInit { } experimentChangedEvent() { - this.folderModel.updateExperiment(); + this.step2 = true; + setTimeout(() => { + this.folderModel.updateExperiment(); + }); } - setDataset(dataset: FolderFile) { + setDataset(dataset: FolderFile | null) { + if (dataset == null) { + this.columnTable.loaded = false; + this.dataset = undefined; + this.experiment.datasetId = ''; + return; + } const d = <Dataset>dataset; this.experiment.datasetId = d._id; this.dataset = d; - this.columnTable.loadDataset(this.dataset); - + this.step1 = true; + setTimeout(() => { + this.columnTable.loadDataset(d); + }); } modelToTrain?: Model; @@ -165,5 +231,6 @@ export class ExperimentComponent implements AfterViewInit { setModel(model: FolderFile) { const m = <Model>model; this.modelToTrain = m; + this.step3 = true; } } diff --git a/frontend/src/app/_pages/home/home.component.css b/frontend/src/app/_pages/home/home.component.css index 906f5728..51f22320 100644 --- a/frontend/src/app/_pages/home/home.component.css +++ b/frontend/src/app/_pages/home/home.component.css @@ -11,10 +11,20 @@ h1 { font-weight: 900 !important; margin-top: 1rem; margin-bottom: 2.5rem; + font-family: /*'Garamond',*/ + 'Courier New', Courier, monospace; } .card { margin: 2.5rem !important; padding: 1.5rem; width: 26rem !important; +} + +a { + text-decoration: none; +} + +.subtitle-class { + letter-spacing: 0.2rem; }
\ No newline at end of file diff --git a/frontend/src/app/_pages/home/home.component.html b/frontend/src/app/_pages/home/home.component.html index 2825b3bf..534854b6 100644 --- a/frontend/src/app/_pages/home/home.component.html +++ b/frontend/src/app/_pages/home/home.component.html @@ -11,9 +11,9 @@ <div class="card shadowed bg-light text-light col-3 m-3" style="width: 18rem;"> <div class="card-body"> <mat-icon width="48px" height="48px" style="font-size: 48px; margin-left: 50%; transform: translateX(-100%);">model_training</mat-icon> - <h3 class="card-title my-2">Experimentiši</h3> + <h2 class="card-title my-2 subtitle-class">Eksperimentiši</h2> <p class="card-text"> - U tri koraka <a class="stretched-link" routerLink="experiment">napravite novu neuronsku mrežu</a>. Koristite postojeće izvore podataka, modele, itd. + U tri koraka <a class="stretched-link text-light" routerLink="experiment">napravite novu neuronsku mrežu</a>. Koristite postojeće izvore podataka, modele, itd. </p> </div> </div> @@ -21,9 +21,9 @@ <div class="card-body"> <mat-icon width="48px" height="48px" style="font-size: 48px; margin-left: 50%; transform: translateX(-100%);">storage </mat-icon> - <h3 class="card-title my-2">Arhiva</h3> + <h2 class="card-title my-2 subtitle-class">Kolekcije</h2> <p class="card-text"> - <a class="stretched-link" routerLink="archive">Upravljajte</a> izvorima podataka, eksperimentima, modelima, i rezultatima treniranja. Pogledajte podatke koje su podelili drugi korisnici. + <a class="stretched-link text-light" routerLink="archive">Upravljajte</a> izvorima podataka, eksperimentima, modelima i rezultatima treniranja. Pogledajte podatke koje su podelili drugi korisnici. </p> </div> </div> diff --git a/frontend/src/app/_pages/my-models/my-models.component.html b/frontend/src/app/_pages/my-models/my-models.component.html deleted file mode 100644 index 9b281239..00000000 --- a/frontend/src/app/_pages/my-models/my-models.component.html +++ /dev/null @@ -1,43 +0,0 @@ -<div id="header"> - <h1>Moji modeli</h1> -</div> -<div id="wrapper"> - <div id="container" class="container p-5" style="background-color: rgba(255, 255, 255, 0.8); min-height: 100%;"> - <div class="row mt-3 mb-2 d-flex justify-content-center"> - - <div class="col-sm-6" style="margin-bottom: 10px;"> - </div> - - <div class="row"> - <div class="col-sm-4" style="margin-bottom: 10px;" *ngFor="let model of myModels"> - <app-item-model [model]="model"></app-item-model> - - <div class="panel-footer row"><!-- panel-footer --> - <div class="col-xs-6 text-center"> - <div> - <button type="button" class="btn btn-default btn-lg"style="min-width: 7rem;float: left;" (click)="useThisModel(model)" mat-raised-button color="primary">Koristi - <span class="glyphicon glyphicon-search"></span> - </button> - <button (click)="deleteThisModel(model)" mat-raised-button color="warn" style="min-width: 7rem;float: right" ><mat-icon>delete</mat-icon></button> - - - </div> - </div> - </div><!-- end panel-footer --> - - - - </div> - </div> - <div class="text-center" *ngIf="this.myModels.length == 0" > - <h2>Nema rezultata</h2> - </div> - </div> - - </div> - - - - - - </div> diff --git a/frontend/src/app/_pages/my-models/my-models.component.ts b/frontend/src/app/_pages/my-models/my-models.component.ts deleted file mode 100644 index d379fa69..00000000 --- a/frontend/src/app/_pages/my-models/my-models.component.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import shared from 'src/app/Shared'; -import Model from 'src/app/_data/Model'; -import { ModelsService } from 'src/app/_services/models.service'; - -@Component({ - selector: 'app-my-models', - templateUrl: './my-models.component.html', - styleUrls: ['./my-models.component.css'] -}) -export class MyModelsComponent implements OnInit { - myModels: Model[] = []; - //myModel: Model; - - constructor(private modelsS : ModelsService, private router : Router) { - - - - } - - ngOnInit(): void { - this.getAllMyModels(); - - } -/* - editModel(): void{ - this.modelsS.editModel().subscribe(m => { - this.myModel = m; - - }) - } -*/ - -deleteThisModel(model: Model): void{ - shared.openYesNoDialog('Brisanje seta podataka','Da li ste sigurni da želite da obrišete model?',() => { - this.modelsS.deleteModel(model).subscribe((response) => { - this.getAllMyModels(); - }, (error) =>{ - if (error.error == "Model with name = {name} deleted") { - shared.openDialog("Obaveštenje", "Greška prilikom brisanja modela."); - } - }); - }); -} - - -useThisModel(model: Model): void{ - - this.router.navigate(['/training']) - -} - getAllMyModels(): void{ - this.modelsS.getMyModels().subscribe(m => { - this.myModels = m; - }); - } - -} diff --git a/frontend/src/app/_pages/profile/profile.component.css b/frontend/src/app/_pages/profile/profile.component.css index bbd4c9ba..48b1304b 100644 --- a/frontend/src/app/_pages/profile/profile.component.css +++ b/frontend/src/app/_pages/profile/profile.component.css @@ -22,4 +22,9 @@ mat-form-field { .selectedPicture { background-color: var(--ns-accent); +} + +.mat-raised-button { + width: 180px !important; + height: 50px !important; }
\ No newline at end of file diff --git a/frontend/src/app/_pages/profile/profile.component.html b/frontend/src/app/_pages/profile/profile.component.html index 8d655513..39f33255 100644 --- a/frontend/src/app/_pages/profile/profile.component.html +++ b/frontend/src/app/_pages/profile/profile.component.html @@ -27,11 +27,11 @@ <div class="row gx-3 mb-3"> <!-- Form Group (password)--> <div class="col-md-6"> - <small *ngIf="wrongPassBool" class="form-text danger-Text">Neispravna lozinka.</small> <mat-form-field appearance="fill"> <mat-label>Važeća lozinka</mat-label> <input matInput id="inputPassword" name="inputPassword" type="password" placeholder="" [(ngModel)]="this.oldPass"> </mat-form-field> + <small *ngIf="wrongPassBool" class="form-text danger-Text">Neispravna lozinka.</small> <small *ngIf="wrongOldPassBool" class="form-text danger-Text">Pogrešan format.</small> </div> <!-- Form Group (new password)--> @@ -50,7 +50,7 @@ <div class="col-md-6"> <div class="col text-center"> <!-- Save changes button--> - <button mat-raised-button color="primary" (click)="savePasswordChanges()">Promeni lozinku</button> + <button mat-raised-button color="basic" (click)="savePasswordChanges()">Promeni lozinku</button> </div> </div> <!-- Form Group (new password again)--> @@ -122,7 +122,7 @@ <div class="container"> <div class="card-group"> <!--bootstrap card with 3 horizontal images--> - <div class="row overflow-auto" style="max-height: 200px;"> + <div class="row overflow-auto" style="max-height: 255px;"> <div class="card col-md-3" *ngFor="let picture of this.pictures" (click)="this.photoId = picture.photoId.toString()" [ngClass]="{'selectedPicture': this.photoId == picture.photoId.toString()}"> <img src="{{picture.path}}"> </div> @@ -134,7 +134,7 @@ <div class="row mt-5"> <div class="col text-center"> <!-- Save changes button--> - <button mat-raised-button color="primary" (click)="saveInfoChanges()">Sačuvaj izmene</button> + <button mat-raised-button color="basic" (click)="saveInfoChanges()">Sačuvaj izmene</button> </div> </div> </form> diff --git a/frontend/src/app/_pages/profile/profile.component.ts b/frontend/src/app/_pages/profile/profile.component.ts index fdcd347c..aed8e40c 100644 --- a/frontend/src/app/_pages/profile/profile.component.ts +++ b/frontend/src/app/_pages/profile/profile.component.ts @@ -7,7 +7,8 @@ import { PICTURES } from 'src/app/_data/ProfilePictures'; import { Picture } from 'src/app/_data/ProfilePictures'; import shared from '../../Shared'; import { share } from 'rxjs'; - +import { MatDialog } from '@angular/material/dialog'; +import { AlertDialogComponent } from 'src/app/_modals/alert-dialog/alert-dialog.component'; @Component({ selector: 'app-profile', @@ -43,13 +44,13 @@ export class ProfileComponent implements OnInit { wrongNewPass2Bool: boolean = false; pattName: RegExp = /^[a-zA-ZšŠđĐčČćĆžŽ]+([ \-][a-zA-ZšŠđĐčČćĆžŽ]+)*$/; - pattUsername: RegExp = /^[a-zA-Z0-9]{6,18}$/; + pattUsername: RegExp = /^[a-zA-Z0-9]{4,18}$/; pattTwoSpaces: RegExp = / /; pattEmail: RegExp = /^[a-zA-Z0-9]+([\.\-\+][a-zA-Z0-9]+)*\@([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}$/; pattPassword: RegExp = /.{6,30}$/; - constructor(private userInfoService: UserInfoService, private authService: AuthService, private router: Router) { } + constructor(private userInfoService: UserInfoService, private authService: AuthService, private router: Router, public dialog?: MatDialog) { } ngOnInit(): void { this.userInfoService.getUserInfo().subscribe((response) => { @@ -95,19 +96,24 @@ export class ProfileComponent implements OnInit { if (this.user.username != editedUser.username) { //promenio username, ide logout this.user = editedUser; this.resetInfo(); - shared.openDialog("Obaveštenje", "Nakon promene korisničkog imena, moraćete ponovo da se ulogujete."); - this.authService.logOut(); - this.router.navigate(['']); - return; + const dialogRef = this.dialog?.open(AlertDialogComponent, { + width: '350px', + data: { title: "Obaveštenje", message: "Nakon promene korisničkog imena, moraćete ponovo da se ulogujete." } + }); + dialogRef?.afterClosed().subscribe(res => { + this.authService.logOut(); + this.router.navigate(['']); + return; + }); + } + else { + shared.openDialog("Obaveštenje", "Podaci su uspešno promenjeni."); + this.user = editedUser; + this.resetInfo(); } - shared.openDialog("Obaveštenje", "Podaci su uspešno promenjeni."); - this.user = editedUser; - this.resetInfo(); }, (error: any) =>{ if (error.error == "Username already exists!") { shared.openDialog("Obaveštenje", "Ukucano korisničko ime je već zauzeto! Izaberite neko drugo."); - //(<HTMLSelectElement>document.getElementById("inputUsername")).focus(); - //poruka obavestenja ispod inputa this.resetInfo(); } }); @@ -122,12 +128,10 @@ export class ProfileComponent implements OnInit { if (this.newPass1 == '' && this.newPass2 == '') //ne zeli da promeni lozinku return; - //console.log("zeli da promeni lozinku"); if (this.newPass1 != this.newPass2) { //netacno ponovio novu lozinku this.wrongNewPassBool = true; this.resetNewPassInputs(); - //console.log("Netacno ponovljena lozinka"); return; } @@ -135,19 +139,23 @@ export class ProfileComponent implements OnInit { this.userInfoService.changeUserPassword(passwordArray).subscribe((response: any) => { //console.log("PROMENIO LOZINKU"); this.resetNewPassInputs(); - shared.openDialog("Obaveštenje", "Nakon promene lozinke, moraćete ponovo da se ulogujete."); - this.authService.logOut(); - this.router.navigate(['']); + const dialogRef = this.dialog?.open(AlertDialogComponent, { + width: '350px', + data: { title: "Obaveštenje", message: "Nakon promene lozinke, moraćete ponovo da se ulogujete." } + }); + dialogRef?.afterClosed().subscribe(res => { + this.authService.logOut(); + this.router.navigate(['']); + return; + }); }, (error: any) => { if (error.error == 'Wrong old password!') { this.wrongPassBool = true; - //(<HTMLSelectElement>document.getElementById("inputPassword")).focus(); return; } else if (error.error == 'Identical password!') { shared.openDialog("Obaveštenje", "Stara i nova lozinka su identične."); this.resetNewPassInputs(); - //(<HTMLSelectElement>document.getElementById("inputNewPassword")).focus(); return; } }); diff --git a/frontend/src/app/_services/datasets.service.ts b/frontend/src/app/_services/datasets.service.ts index 2211996f..2775613c 100644 --- a/frontend/src/app/_services/datasets.service.ts +++ b/frontend/src/app/_services/datasets.service.ts @@ -24,18 +24,35 @@ export class DatasetsService { return this.http.post(`${Configuration.settings.apiURL}/dataset/add`, dataset, { headers: this.authService.authHeader() }); } + stealDataset(dataset: Dataset): Observable<any> { + return this.http.post(`${Configuration.settings.apiURL}/dataset/stealDs`, dataset, { headers: this.authService.authHeader() }); + } + getDatasetFile(fileId: any): any { return this.http.get(`${Configuration.settings.apiURL}/file/csvRead/${fileId}/-1/11`, { headers: this.authService.authHeader(), responseType: 'text' }); } + getDatasetFilePaging(fileId:any,begin:any,end:any){ + return this.http.get(`${Configuration.settings.apiURL}/file/csvRead/${fileId}/${begin}/${end}`, { headers: this.authService.authHeader(), responseType: 'text' }); + } + getDatasetHeader(fileId:any){ + return this.http.get(`${Configuration.settings.apiURL}/file/csvRead/${fileId}/-1/1`, { headers: this.authService.authHeader(), responseType: 'text' }); + } getDatasetFilePartial(fileId: any, startRow: number, rowNum: number): Observable<any> { return this.http.get(`${Configuration.settings.apiURL}/file/csvRead/${fileId}/${startRow}/${rowNum}`, { headers: this.authService.authHeader(), responseType: 'text' }); } + getDatasetById(datasetId: string): Observable<Dataset> { + return this.http.get<Dataset>(`${Configuration.settings.apiURL}/dataset/get/${datasetId}`, { headers: this.authService.authHeader() }); + } - editDataset(dataset: Dataset): Observable<Dataset> { - return this.http.put<Dataset>(`${Configuration.settings.apiURL}/dataset/` + dataset._id, dataset, { headers: this.authService.authHeader() }); + editDataset(dataset: Dataset){ + return this.http.put(`${Configuration.settings.apiURL}/dataset/` + dataset._id, dataset, { headers: this.authService.authHeader() ,responseType:'text'}); } deleteDataset(dataset: Dataset) { return this.http.delete(`${Configuration.settings.apiURL}/dataset/` + dataset._id, { headers: this.authService.authHeader(), responseType: "text" }); } + + downloadFile(id:string):Observable<Blob>{ + return this.http.get(`${Configuration.settings.apiURL}/file/Download?id=`+id, { headers: this.authService.authHeader(), responseType: 'blob' }); + } } diff --git a/frontend/src/app/_services/experiments.service.ts b/frontend/src/app/_services/experiments.service.ts index 29569fca..4b209ff0 100644 --- a/frontend/src/app/_services/experiments.service.ts +++ b/frontend/src/app/_services/experiments.service.ts @@ -20,7 +20,15 @@ export class ExperimentsService { return this.http.get<Experiment[]>(`${Configuration.settings.apiURL}/experiment/getmyexperiments`, { headers: this.authService.authHeader() }); } + getExperimentById(id: string): Observable<Experiment> { + return this.http.get<Experiment>(`${Configuration.settings.apiURL}/experiment/get/${id}`, { headers: this.authService.authHeader() }); + } + updateExperiment(experiment: Experiment): Observable<Experiment> { return this.http.put<Experiment>(`${Configuration.settings.apiURL}/experiment/` + experiment._id, experiment, { headers: this.authService.authHeader() }); } + + deleteExperiment(experiment: Experiment) { + return this.http.delete(`${Configuration.settings.apiURL}/experiment/` + experiment._id, { headers: this.authService.authHeader(), responseType: "text" }); + } } diff --git a/frontend/src/app/_services/models.service.ts b/frontend/src/app/_services/models.service.ts index fc888556..2b8fe8f2 100644 --- a/frontend/src/app/_services/models.service.ts +++ b/frontend/src/app/_services/models.service.ts @@ -31,6 +31,10 @@ export class ModelsService { addModel(model: Model): Observable<any> { return this.http.post(`${Configuration.settings.apiURL}/model/add`, model, { headers: this.authService.authHeader() }); } + + stealModel(model: Model): Observable<any> { + return this.http.post(`${Configuration.settings.apiURL}/model/stealModel`, model, { headers: this.authService.authHeader() }); + } addDataset(dataset: Dataset): Observable<any> { return this.http.post(`${Configuration.settings.apiURL}/dataset/add`, dataset, { headers: this.authService.authHeader() }); } @@ -47,7 +51,7 @@ export class ModelsService { } editModel(model: Model): Observable<Model> { - return this.http.put<Model>(`${Configuration.settings.apiURL}/model/`, model, { headers: this.authService.authHeader() }); + return this.http.put<Model>(`${Configuration.settings.apiURL}/model/` + model.name, model, { headers: this.authService.authHeader() }); } deleteModel(model: Model) { @@ -58,4 +62,8 @@ export class ModelsService { return this.http.get<Model[]>(`${Configuration.settings.apiURL}/model/publicmodels`, { headers: this.authService.authHeader() }); } + getModelById(modelId: string): Observable<Model> { + return this.http.get<Model>(`${Configuration.settings.apiURL}/model/byid/${modelId}`, { headers: this.authService.authHeader() }); + } + } diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index f5f1ccae..d5552ce9 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -12,12 +12,14 @@ import { TestComponent } from './_pages/test/test.component'; const routes: Routes = [ { path: '', component: HomeComponent, data: { title: 'Početna strana' } }, + { path: 'experiment/p/:predictorId', component: ExperimentComponent, data: { title: 'Eksperiment' } }, + { path: 'experiment/:id', component: ExperimentComponent, data: { title: 'Eksperiment' } }, { path: 'experiment', component: ExperimentComponent, data: { title: 'Eksperiment' } }, { path: 'archive', component: ArchiveComponent, data: { title: 'Arhiva' } }, { path: 'profile', component: ProfileComponent, canActivate: [AuthGuardService], data: { title: 'Profil' } }, { path: 'playground', component: PlaygroundComponent, data: { title: 'Zabava' } }, - { path: 'sonja', component: ColumnTableComponent }, - { path: 'test', component: TestComponent, data: { title: 'Test' } } + { path: 'test', component: TestComponent, data: { title: 'Test' } }, + { path: '**', redirectTo: '' } ]; @NgModule({ diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index e301b46f..5660f676 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -6,6 +6,7 @@ import { AuthService } from './_services/auth.service'; import { SignalRService } from './_services/signal-r.service'; import { HttpClient } from '@angular/common/http'; import Shared from './Shared'; +import { Chart } from 'chart.js'; @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -14,6 +15,117 @@ import Shared from './Shared'; export class AppComponent implements OnInit, AfterViewInit { constructor(private router: Router, private titleService: Title, private authService: AuthService, private signalRService: SignalRService, private http: HttpClient) { + const getOrCreateTooltip = (chart: { canvas: { parentNode: { querySelector: (arg0: string) => any; appendChild: (arg0: any) => void; }; }; }) => { + let tooltipEl = chart.canvas.parentNode.querySelector('div'); + + if (!tooltipEl) { + tooltipEl = document.createElement('div'); + tooltipEl.style.background = 'rgba(0, 0, 0, 0.7)'; + tooltipEl.style.borderRadius = '3px'; + tooltipEl.style.color = 'white'; + tooltipEl.style.opacity = 1; + tooltipEl.style.pointerEvents = 'none'; + tooltipEl.style.position = 'absolute'; + tooltipEl.style.transform = 'translate(-50%, 0)'; + tooltipEl.style.transition = 'all .1s ease'; + tooltipEl.style.zIndex = 9000; + + tooltipEl.classList.add("clearfix"); + + const table = document.createElement('table'); + table.style.margin = '0px'; + + tooltipEl.appendChild(table); + chart.canvas.parentNode.appendChild(tooltipEl); + } + + return tooltipEl; + }; + + const externalTooltipHandler = (context: { chart: any; tooltip: any; }) => { + // Tooltip Element + const { chart, tooltip } = context; + const tooltipEl = getOrCreateTooltip(chart); + + // Hide if no tooltip + if (tooltip.opacity === 0) { + tooltipEl.style.opacity = 0; + return; + } + + // Set Text + if (tooltip.body) { + const titleLines = tooltip.title || []; + const bodyLines = tooltip.body.map((b: { lines: any; }) => b.lines); + + const tableHead = document.createElement('thead'); + + titleLines.forEach((title: string) => { + const tr = document.createElement('tr'); + tr.style.borderWidth = '' + 0; + + const th = document.createElement('th'); + th.style.borderWidth = '' + 0; + const text = document.createTextNode(title); + + th.appendChild(text); + tr.appendChild(th); + tableHead.appendChild(tr); + }); + + const tableBody = document.createElement('tbody'); + bodyLines.forEach((body: string, i: string | number) => { + const colors = tooltip.labelColors[i]; + + const span = document.createElement('span'); + span.style.background = colors.backgroundColor; + span.style.borderColor = colors.borderColor; + span.style.borderWidth = '2px'; + span.style.marginRight = '10px'; + span.style.height = '10px'; + span.style.width = '10px'; + span.style.display = 'inline-block'; + + const tr = document.createElement('tr'); + tr.style.backgroundColor = 'inherit'; + tr.style.borderWidth = '' + 0; + + const td = document.createElement('td'); + td.style.borderWidth = '' + 0; + + const text = document.createTextNode(body); + + td.appendChild(span); + td.appendChild(text); + tr.appendChild(td); + tableBody.appendChild(tr); + }); + + const tableRoot = tooltipEl.querySelector('table'); + + // Remove old children + while (tableRoot.firstChild) { + tableRoot.firstChild.remove(); + } + + // Add new children + tableRoot.appendChild(tableHead); + tableRoot.appendChild(tableBody); + } + + const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas; + + // Display, position, and set styles for font + tooltipEl.style.opacity = 1; + tooltipEl.style.left = positionX + tooltip.caretX + 'px'; + tooltipEl.style.top = positionY + tooltip.caretY + 'px'; + tooltipEl.style.font = tooltip.options.bodyFont.string; + tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px'; + }; + + Chart.defaults.plugins.tooltip.enabled = false; + Chart.defaults.plugins.tooltip.position = 'nearest'; + Chart.defaults.plugins.tooltip.external = externalTooltipHandler; } ngAfterViewInit(): void { } @@ -40,7 +152,7 @@ export class AppComponent implements OnInit, AfterViewInit { } }); if (!this.authService.isAuthenticated()) { - if(!this.authService.alreadyGuest()) + if (!this.authService.alreadyGuest()) this.authService.addGuestToken(); } this.signalRService.startConnection(); diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index d44bf6ad..89d53115 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -52,6 +52,7 @@ import { DoughnutChartComponent } from './_elements/_charts/doughnut-chart/dough import { HeatmapComponent } from './_elements/_charts/heatmap/heatmap.component'; import { HeatMapAllModule } from '@syncfusion/ej2-angular-heatmap'; import { MetricViewComponent } from './_elements/metric-view/metric-view.component'; +import { SpinnerComponent } from './_elements/spinner/spinner.component'; export function initializeApp(appConfig: Configuration) { return () => appConfig.load(); @@ -91,7 +92,8 @@ export function initializeApp(appConfig: Configuration) { HeatmapComponent, MetricViewComponent, LineChartComponent, - SaveExperimentDialogComponent + SaveExperimentDialogComponent, + SpinnerComponent ], imports: [ BrowserModule, diff --git a/frontend/src/styles/font.css b/frontend/src/styles/font.css index a4d876f5..79e71ca6 100644 --- a/frontend/src/styles/font.css +++ b/frontend/src/styles/font.css @@ -1,6 +1,5 @@ -/*p, -a { +div{ font-family: Helvetica, sans-serif; - letter-spacing: 2px; + letter-spacing: 1px; font-weight: 100; -}*/
\ No newline at end of file +}
\ No newline at end of file diff --git a/frontend/src/styles/helper.css b/frontend/src/styles/helper.css index 971a2ed6..b6a58243 100644 --- a/frontend/src/styles/helper.css +++ b/frontend/src/styles/helper.css @@ -17,8 +17,7 @@ .bg-controls { position: fixed; bottom: 10px; - left: 50%; - right: 50%; + left: 5rem; } .center-horizontal { @@ -142,4 +141,12 @@ select:-webkit-autofill { a { color: var(--ns-accent) !important; +} + +.clearfix:after { + content: "\0020"; + display: block; + height: 0; + clear: both; + visibility: hidden; }
\ No newline at end of file |