diff options
Diffstat (limited to 'frontend/src/app/_elements/folder')
4 files changed, 728 insertions, 0 deletions
diff --git a/frontend/src/app/_elements/folder/folder.component.css b/frontend/src/app/_elements/folder/folder.component.css new file mode 100644 index 00000000..62324d62 --- /dev/null +++ b/frontend/src/app/_elements/folder/folder.component.css @@ -0,0 +1,213 @@ +#folder { + /*position: absolute; + left: 50%; + transform: translateX(-50%);*/ +} + +#tabs { + display: flex; + flex-direction: row; + align-items: flex-end; + height: 3.1rem; +} + +#tabs>.folder-tab:not(:first-child) { + margin-left: -5px; +} + +.folder-tab { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + position: relative; + height: 2.5rem; + background-color: var(--ns-bg-dark-100); + border-color: var(--ns-primary); + color: var(--ns-primary); + border-style: solid; + border-width: 1px 1px 0 1px; +} + +.folder-tab:not(:first-child) { + margin-block-start: auto; +} + +.selected-tab { + height: 3rem; + background-color: var(--ns-primary); + color: var(--offwhite); +} + +.hover-tab { + height: 3.2rem; +} + +.selected-tab, +.hover-tab { + width: fit-content !important; +} + +.tab-link { + color: var(--offwhite) !important; + text-decoration: none !important; + cursor: pointer; + padding: 0.5rem; +} + +.tab-link:active { + text-decoration: underline !important; +} + +.selected-tab { + background-color: var(--ns-primary); +} + +#searchbar { + height: 2.5rem; + background-color: var(--ns-bg-dark-100); + border-bottom: 1px solid var(--ns-primary); + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + width: 100%; +} + +.collapse-horizontal { + white-space: nowrap; + height: 2.5rem; + overflow-x: hidden; +} + +#search-options { + margin-left: auto; + display: flex; + flex-direction: row; + align-items: center; + height: 100%; +} + +#selected-content { + background-color: var(--ns-bg-dark-50); + width: 100%; + /*backdrop-filter: blur(2px);*/ + border-color: var(--ns-primary); + border-style: solid; + border-width: 1px 1px 1px 1px; + border-top-right-radius: 4px; +} + +#footer { + display: flex; + flex-direction: row; + justify-content: center; +} + +.bottom-button { + font-size: large; + position: relative; + background-color: var(--ns-primary); + width: 10rem; + height: 2.3rem; + border-color: var(--ns-primary); + border-style: solid; + border-width: 0px 1px 1px 1px; +} + +.rounded-bottom { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.separator { + border-left-color: var(--ns-primary); + border-left-width: 1px; + border-left-style: solid; +} + +.list-view { + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.list-item { + height: 3rem; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--ns-primary); + flex-shrink: 0; +} + +.list-item:hover { + background-color: var(--ns-bg-dark-100); + box-shadow: 0px 3px 3px var(--ns-primary); +} + +.list-item:hover>.hover-hide { + display: none; +} + +.hover-show { + display: none; +} + +.list-item:hover>.hover-show { + display: initial; +} + +.list-add { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + flex-grow: 1; + height: 100%; +} + +.folder-inside { + width: 100%; + height: 40rem; + overflow-y: auto; +} + +.file-content { + width: 100%; + height: 100%; + position: relative; +} + +.file-bottom-buttons { + position: absolute; + bottom: 15px; + right: 15px; + display: flex; + flex-direction: row-reverse; +} + +.file-button { + position: relative; + color: var(--offwhite); + border-radius: 4px; + border: 1px solid var(--ns-primary); + margin: 5px; + padding: 5px; + cursor: pointer; + z-index: 1001; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.file-button:hover { + background-color: var(--ns-primary); +} + +.form-hidden { + display: none; +}
\ No newline at end of file diff --git a/frontend/src/app/_elements/folder/folder.component.html b/frontend/src/app/_elements/folder/folder.component.html new file mode 100644 index 00000000..bff066be --- /dev/null +++ b/frontend/src/app/_elements/folder/folder.component.html @@ -0,0 +1,111 @@ +<div id="folder"> + <div id="tabs"> + <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}} + </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}"> + <a class="m-1 stretched-link tab-link" (click)="selectFile(i)" (mouseenter)="hoverOverFile(i)" (mouseleave)="hoverOverFile(-1)">{{file.name}}</a> + </div>--> + <div class="folder-tab p-1 rounded-top" *ngFor="let tab of tabsToShow; let i = index" [style]="'z-index:' + (selectedTab == tab ? 11 : (tabsToShow.length - i)) + ' ;'" [ngClass]="{'selected-tab' : selectedTab == tab, 'hover-tab' : hoverTab == tab}"> + <a class="m-1 stretched-link tab-link" (click)="selectTab(tab)" (mouseenter)="hoverOverTab(tab)" (mouseleave)="hoverOverTab(TabType.None)">{{tabTitles[tab]}}</a> + </div> + + <div class="folder-tab p-1 rounded-top" *ngIf="selectedFile" [style]="'z-index:' + (selectedTab == TabType.File ? 11 : (tabsToShow.length)) + ' ;'" [ngClass]="{'selected-tab' : selectedTab == TabType.File, 'hover-tab' : hoverTab == TabType.File}"> + <a class="m-1 stretched-link tab-link" (click)="selectTab(TabType.File)" (mouseenter)="hoverOverTab(TabType.File)" (mouseleave)="hoverOverTab(TabType.None)">{{selectedFile.name}}</a> + </div> + </div> + <div id="selected-content" class="rounded-bottom text-offwhite"> + <div id="searchbar" *ngIf="listView"> + <!-- <div id="path" class="ps-2">{{folderName}} + </div> + <mat-icon>keyboard_arrow_right</mat-icon> --> + <div id="search" class="text-offwhite mx-1"> + <mat-form-field> + <button matPrefix class="btn-clear input-icon"><mat-icon>search</mat-icon></button> + <input type="search" matInput name="search" [(ngModel)]="searchTerm" (input)="searchTermsChanged()"> + <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"> + <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> --> + <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"> + [sort options here TODO] + </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()"> + <mat-icon>view_list</mat-icon> + </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> + <!-- <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> + </div> + <div [ngClass]="{'form-hidden' : !listView}" class="list-view"> + <div *ngFor="let file of filteredFiles; let i = index" class="list-item force-link" (click)="selectFile(file)"> + <div class="mx-2"> + {{file.name}} + </div> + <div class="mx-2 hover-hide"> + {{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> + </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> + <button mat-raised-button *ngSwitchCase="FolderType.Experiment" routerLink="/experiment">Dodaj eksperiment</button> + </div> + </div> + </div> + </div> + <div id="footer" [ngSwitch]="newFileSelected" *ngIf="!listView"> + <button mat-button (click)="saveNewFile()" class="bottom-button text-offwhite rounded-bottom" *ngSwitchCase="true"> + <div class="f-row"> + <div>Sačuvaj</div> + <div class="pt-1"> + <mat-icon>check</mat-icon> + </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> + </div> + </div> + </button> + </div> +</div>
\ No newline at end of file diff --git a/frontend/src/app/_elements/folder/folder.component.spec.ts b/frontend/src/app/_elements/folder/folder.component.spec.ts new file mode 100644 index 00000000..33a573a7 --- /dev/null +++ b/frontend/src/app/_elements/folder/folder.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FolderComponent } from './folder.component'; + +describe('FolderComponent', () => { + let component: FolderComponent; + let fixture: ComponentFixture<FolderComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FolderComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FolderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/_elements/folder/folder.component.ts b/frontend/src/app/_elements/folder/folder.component.ts new file mode 100644 index 00000000..fabb524c --- /dev/null +++ b/frontend/src/app/_elements/folder/folder.component.ts @@ -0,0 +1,379 @@ +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 { DatasetsService } from 'src/app/_services/datasets.service'; +import Shared from 'src/app/Shared'; +import { ModelsService } from 'src/app/_services/models.service'; +import { FormDatasetComponent } from '../form-dataset/form-dataset.component'; +import Experiment from 'src/app/_data/Experiment'; +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'; + +@Component({ + selector: 'app-folder', + templateUrl: './folder.component.html', + styleUrls: ['./folder.component.css'] +}) +export class FolderComponent implements AfterViewInit { + + @ViewChild(FormDatasetComponent) formDataset!: FormDatasetComponent; + @ViewChild(FormModelComponent) formModel!: FormModelComponent; + + @Input() folderName: string = 'Moji podaci'; + @Input() files!: FolderFile[] + + newFile?: Dataset | Model; + + @Input() type: FolderType = FolderType.Dataset; + @Input() forExperiment!: Experiment; + @Input() startingTab!: TabType; + + newFileSelected: boolean = true; + + selectedFileIndex: number = -1; + selectedFile?: FolderFile; + hoveringOverFileIndex: number = -1; + + fileToDisplay?: FolderFile; + + @Output() selectedFileChanged: 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) { + this.tabsToShow.forEach(tab => this.folders[tab] = []); + } + + ngAfterViewInit(): void { + this.refreshFiles(null); + + if (this.signalRService.hubConnection) { + this.signalRService.hubConnection.on("NotifyDataset", (dName: string, dId: string) => { + if (this.type == FolderType.Dataset) { + this.refreshFiles(dId); + } + }); + } else { + console.warn("Dataset-Load: No connection!"); + } + } + + displayFile() { + if (this.type == FolderType.Dataset) + this.formDataset.dataset = <Dataset>this.fileToDisplay; + else if (this.type == FolderType.Model) + this.formModel.newModel = <Model>this.fileToDisplay; + } + + hoverOverFile(i: number) { + /*this.hoveringOverFileIndex = i; + if (i != -1) { + this.fileToDisplay = this.files[i]; + } else { + if (this.newFileSelected) { + this.fileToDisplay = this.newFile; + } else { + this.fileToDisplay = this.files[this.selectedFileIndex]; + } + } + this.displayFile();*/ + } + + selectNewFile() { + if (!this.newFile) { + this.createNewFile(); + } + this.fileToDisplay = this.newFile; + this.newFileSelected = true; + this.listView = false; + this.displayFile(); + if(this.type == FolderType.Dataset) + this.formDataset.clear(); + } + + selectFile(file?: FolderFile) { + this.selectedFile = file; + this.fileToDisplay = file; + this.newFileSelected = false; + this.listView = false; + this.selectedFileChanged.emit(this.selectedFile); + this.selectTab(TabType.File); + this.displayFile(); + + if(this.type == FolderType.Dataset) + this.formDataset.loadExisting(); + } + + createNewFile() { + if (this.type == FolderType.Dataset) { + this.newFile = new Dataset(); + } else if (this.type == FolderType.Model) { + this.newFile = new Model(); + } + } + + ok() { + this.okPressed.emit(); + } + + _initialized: boolean = false; + + refreshFiles(selectedDatasetId: string | null) { + this.files = [] + this.filteredFiles.length = 0; + this.folders[TabType.NewFile] = []; + this.folders[TabType.File] = []; + this.tabsToShow.forEach(tab => { + this.folders[tab] = []; + }); + + this.datasetsService.getMyDatasets().subscribe((datasets) => { + this.folders[TabType.MyDatasets] = datasets; + if (selectedDatasetId) { + this.selectFile(datasets.filter(x => x._id == selectedDatasetId)[0]); + } + }); + + this.experimentsService.getMyExperiments().subscribe((experiments) => { + this.folders[TabType.MyExperiments] = experiments; + }); + + this.datasetsService.getPublicDatasets().subscribe((datasets) => { + this.folders[TabType.PublicDatasets] = datasets; + }); + + this.modelsService.getMyModels().subscribe((models) => { + this.folders[TabType.MyModels] = models; + }); + + /*this.modelsService.getMyModels().subscribe((models) => { + this.folders[TabType.PublicModels] = models; + });*/ + this.folders[TabType.PublicModels] = []; + + this.experimentsService.getMyExperiments().subscribe((experiments) => { + this.folders[TabType.MyExperiments] = experiments; + }); + + if (!this._initialized) { + this.files = this.folders[this.startingTab]; + this.filteredFiles = []; + this.selectTab(this.startingTab); + this._initialized = true; + } + + this.searchTermsChanged(); + } + + saveNewFile() { + 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.refreshFiles(null); + }, + () => { + Shared.openDialog("Neuspeo pokušaj!", "Izvor podataka sa unetim nazivom već postoji u Vašoj kolekciji. Izmenite naziv ili iskoristite postojeći dataset."); + }); + break; + case FolderType.Model: + 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.refreshFiles(null); // 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."); + }); + break; + } + } + + + /*calcZIndex(i: number) { + let zIndex = (this.files.length - i - 1) + if (this.selectedFileIndex == i) + zIndex = this.files.length + 2; + if (this.hoveringOverFileIndex == i) + zIndex = this.files.length + 3; + return zIndex; + } + + newFileZIndex() { + return (this.files.length + 1); + }*/ + + clearSearchTerm() { + this.searchTerm = ''; + this.searchTermsChanged(); + } + + filteredFiles: FolderFile[] = []; + + searchTermsChanged() { + this.filteredFiles.length = 0; + if (!this.files) return; + this.filteredFiles.push(...this.files.filter((file) => file.name.toLowerCase().includes(this.searchTerm.toLowerCase()))); + /*if (this.selectedFile) { + if (!this.filteredFiles.includes(this.selectedFile)) { + if (this.hoverTab === TabType.None && this.getFolderType(this.selectedTab) === this.type) { + this.selectFile(undefined); + console.log(this.getFolderType(this.selectedTab), this.type); + } + } else { + //this.selectedFileIndex = this.filteredFiles.indexOf(this.selectedFile); + } + }*/ + } + + listView: boolean = true; + + toggleListView() { + this.listView = !this.listView; + } + + deleteFile(file: FolderFile, event: Event) { + event.stopPropagation(); + //console.log('delete'); + switch (this.type) { + case FolderType.Dataset: + this.datasetsService.deleteDataset(<Dataset>file).subscribe((response) => { + this.filteredFiles.splice(this.filteredFiles.indexOf(file), 1); + this.refreshFiles(null); + }); + break; + case FolderType.Model: + this.modelsService.deleteModel(<Model>file).subscribe((response) => { + this.refreshFiles(null); + }); + break; + case FolderType.Experiment: + // this.experimentsService.deleteExperiment(<Model>file).subscribe((response) => { + // console.log(response); + // }); + //todo delete za predictor + break; + } + } + + folders: { [tab: number]: FolderFile[] } = {}; + + tabTitles: { [tab: number]: string } = { + [TabType.File]: 'Fajl', + [TabType.NewFile]: 'Novi fajl', + [TabType.MyDatasets]: 'Moji izvori podataka', + [TabType.PublicDatasets]: 'Javni izvori podataka', + [TabType.MyModels]: 'Moje konfiguracije neuronske mreže', + [TabType.PublicModels]: 'Javne konfiguracije neuronske mreže', + [TabType.MyExperiments]: 'Eksperimenti', + }; + + FolderType = FolderType; + Privacy = Privacy; + TabType = TabType; + + privacy: Privacy = Privacy.Private; + + @Input() tabsToShow: TabType[] = [ + TabType.MyDatasets, + TabType.PublicDatasets, + TabType.MyModels, + TabType.PublicModels, + TabType.MyExperiments + ] + + @Input() selectedTab: TabType = TabType.NewFile; + hoverTab: TabType = TabType.None; + + selectTab(tab: TabType) { + setTimeout(() => { + if (tab == TabType.NewFile) { + this.selectNewFile(); + } + + this.listView = this.getListView(tab); + this.type = this.getFolderType(tab); + this.privacy = this.getPrivacy(tab); + this.selectedTab = tab; + this.files = this.folders[tab]; + + if (tab !== TabType.File && tab !== TabType.NewFile) + this.searchTermsChanged(); + }); + } + + getListView(tab: TabType) { + switch (tab) { + case TabType.File: + case TabType.NewFile: + case TabType.None: + return false; + case TabType.MyExperiments: + case TabType.MyDatasets: + case TabType.MyModels: + case TabType.PublicDatasets: + case TabType.PublicModels: + return true; + default: + return false; + } + } + + getFolderType(tab: TabType) { + switch (tab) { + case TabType.MyExperiments: + return FolderType.Experiment; + case TabType.MyDatasets: + case TabType.PublicDatasets: + return FolderType.Dataset; + case TabType.MyModels: + case TabType.PublicModels: + return FolderType.Model; + default: + return this.type; + } + } + + getPrivacy(tab: TabType) { + switch (tab) { + case TabType.PublicDatasets: + case TabType.PublicModels: + return Privacy.Public; + default: + return Privacy.Private; + } + } + + hoverOverTab(tab: TabType) { + this.listView = this.getListView(tab); + this.privacy = this.getPrivacy(tab); + this.hoverTab = tab; + if (tab == TabType.None) { + this.listView = this.getListView(this.selectedTab); + this.files = this.folders[this.selectedTab]; + } else { + this.files = this.folders[tab]; + } + this.searchTermsChanged(); + } +} + +export enum Privacy { + Private, + Public +} + +export enum TabType { + NewFile, + File, + MyDatasets, + PublicDatasets, + MyModels, + PublicModels, + MyExperiments, + None +}
\ No newline at end of file |