diff --git a/README.md b/README.md index 4feed35a..876ea234 100644 --- a/README.md +++ b/README.md @@ -72,14 +72,24 @@ Analysim requires two databases to operate: one SQL database (PostgreSQL) for re "Audience": "https://www.analysim.tech/" }, "UserQuota": 100000000, - "registrationCodes": [ "123" ] + "registrationCodes": [ "123" ], + "AdminUsers": [ + "ADMIN", + "XXX" + ] } ``` +#### Adding admin users + +Admin access in Analysim is controlled through the AdminUsers section of the `appsettings.json` and `appsettings.Development.json`. Each entry in the list corresponds to the username of a registered Analysim user. Admin users will see an Admin link in the navigation bar and can access the /admin section of the platform. To add or remove admin privileges, simply update this list and restart the server. + +⚠️ Important: The usernames must exactly match the usernames stored in the database (case-sensitive). + #### SQL database (also see Docker Compose option below) -If you don't have a SQL database yet, download and install [PostgreSQL](https://www.postgresql.org/download/). See the example for [installing on Ubuntu 22.04](https://linuxhint.com/install-and-setup-postgresql-database-ubuntu-22-04/). Create a user account ([tutorial](https://medium.com/coding-blocks/creating-user-database-and-adding-access-on-postgresql-8bfcd2f4a91e)) and replace the `XXX` values in the `DBConnectionString` above with the correct ones. Once you entered the correct details, you must be able to initialize and populate the database by using the Entity Framework migration tool by rinning the following command in the `src/Analysim.Web` folder: +If you don't have a SQL database yet, download and install [PostgreSQL](https://www.postgresql.org/download/). See the example for [installing on Ubuntu 22.04](https://linuxhint.com/install-and-setup-postgresql-database-ubuntu-22-04/). Create a user account ([tutorial](https://medium.com/coding-blocks/creating-user-database-and-adding-access-on-postgresql-8bfcd2f4a91e)) and replace the `XXX` values in the `DBConnectionString` above with the correct ones. Once you entered the correct details, you must be able to initialize and populate the database by using the Entity Framework migration tool by running the following command in the `src/Analysim.Web` folder: ``` dotnet ef database update diff --git a/src/Analysim.Web/ClientApp/src/app/admin/admin-routing.module.ts b/src/Analysim.Web/ClientApp/src/app/admin/admin-routing.module.ts index 8c21719a..27f1a267 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/admin-routing.module.ts +++ b/src/Analysim.Web/ClientApp/src/app/admin/admin-routing.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AdminComponent } from './admin.component'; import { AuthGuardService } from '../guards/auth-guard.service'; +import { AdminGuard } from '../guards/admin.guard'; import { NotebooksComponent } from './components/notebooks/notebooks.component'; import { UsersComponent } from './components/users/users.component'; import { DatasetsComponent } from './components/datasets/datasets.component'; @@ -11,8 +12,9 @@ const routes: Routes = [ { path: '', component: AdminComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuardService, AdminGuard], children: [ + { path: 'notebooks/:notebookRoute', component: NotebooksComponent }, { path: 'notebooks', component: NotebooksComponent }, { path: 'users', component: UsersComponent }, { path: 'datasets', component: DatasetsComponent }, diff --git a/src/Analysim.Web/ClientApp/src/app/admin/admin.module.ts b/src/Analysim.Web/ClientApp/src/app/admin/admin.module.ts index d89d13db..189bc92a 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/admin.module.ts +++ b/src/Analysim.Web/ClientApp/src/app/admin/admin.module.ts @@ -1,26 +1,41 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; import { AdminRoutingModule } from './admin-routing.module'; import { AdminComponent } from './admin.component'; import { NotebooksComponent } from './components/notebooks/notebooks.component'; import { UsersComponent } from './components/users/users.component'; import { DatasetsComponent } from './components/datasets/datasets.component'; +import { FormsModule } from '@angular/forms'; +import { SaveConfirmationModalComponent } from './components/notebooks/save-confirmation-modal/save-confirmation-modal.component'; +import { SaveNotebookModalComponent } from './components/notebooks/save-notebook-modal/save-notebook-modal.component'; import { ProjectsComponent } from './components/projects/projects.component'; import { AdminNotebookItemComponent } from './components/notebooks/admin-notebook-item/admin-notebook-item.component'; +import { AdminNotebookItemDisplayComponent } from './components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component'; +import { ProjectsModule } from '../projects/projects.module'; +import { UserDisplayComponent } from './components/users/user-display/user-display.component'; +import { ProjectDisplayComponent } from './components/projects/project-display/project-display.component'; +import { DatasetActionsComponent } from './components/datasets/dataset-actions/dataset-actions.component'; @NgModule({ declarations: [ AdminComponent, NotebooksComponent, UsersComponent, + SaveConfirmationModalComponent, + SaveNotebookModalComponent, DatasetsComponent, ProjectsComponent, - AdminNotebookItemComponent + AdminNotebookItemComponent, + AdminNotebookItemDisplayComponent, + UserDisplayComponent, + ProjectDisplayComponent, + DatasetActionsComponent ], imports: [ CommonModule, - AdminRoutingModule + AdminRoutingModule, + ProjectsModule, + FormsModule ] }) export class AdminModule { } diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.html new file mode 100644 index 00000000..6ee2c8f1 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.html @@ -0,0 +1,56 @@ + + + + + + + diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.scss b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.spec.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.spec.ts new file mode 100644 index 00000000..95563aeb --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DatasetActionsComponent } from './dataset-actions.component'; + +describe('DatasetActionsComponent', () => { + let component: DatasetActionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DatasetActionsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DatasetActionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.ts new file mode 100644 index 00000000..f0fd9167 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/dataset-actions/dataset-actions.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit } from '@angular/core'; +import { EventEmitter, Input, Output } from '@angular/core'; +import { BlobFile } from 'src/app/interfaces/blob-file'; +import { ProjectService } from 'src/app/services/project.service'; + +@Component({ + selector: 'app-dataset-actions', + templateUrl: './dataset-actions.component.html', + styleUrls: ['./dataset-actions.component.scss'] +}) +export class DatasetActionsComponent implements OnInit { + + @Input() dataset : BlobFile; + @Output() datasetDeleted: EventEmitter = new EventEmitter(); + @Output() datasetPreview: EventEmitter = new EventEmitter(); + constructor(private projectService : ProjectService) { } + + ngOnInit(): void { + } + + preview() { + this.datasetPreview.emit(this.dataset); + } + + deleteDataset() { + this.projectService.deleteFile(this.dataset.blobFileID, true).subscribe(res => { + this.datasetDeleted.emit(); + }) + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/datasets.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/datasets.component.html index 99d74ffd..a5492a98 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/datasets.component.html +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/datasets.component.html @@ -1,31 +1,38 @@ - -
-
-
All Datasets
-
-
- - - - - - - - - - - - - - - - - -
NameDirectorySizeDate Created
{{d.name}}{{d.extension}}{{d.directory || '/'}}{{d.size | number}} bytes{{d.dateCreated | date:'short'}}
+ +
+

All Datasets

+
Loading…
+
{{ error }}
-
-
Loading… -
-
{{ error }}
-
+ + + + + + + + + + + + + + + + + +
NameDirectorySize (bytes)Actions
{{ ds.name + ds.extension }}{{ ds.directory }}{{ ds.size }}
+ + + + + diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/datasets.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/datasets.component.ts index fd18c305..18f48c12 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/datasets.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/datasets/datasets.component.ts @@ -1,6 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +// src/app/admin/components/datasets/admin-datasets.component.ts +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { BlobFile } from 'src/app/interfaces/blob-file'; import { ProjectService } from 'src/app/services/project.service'; -import { BlobFile } from '../../../interfaces/blob-file'; @Component({ selector: 'app-datasets', @@ -9,21 +11,43 @@ import { BlobFile } from '../../../interfaces/blob-file'; }) export class DatasetsComponent implements OnInit { datasets: BlobFile[] = []; - loading = true; - error: string = null; + loading = false; + error: string | null = null; - constructor(private project: ProjectService) { } + selectedDataset: BlobFile | null = null; + @ViewChild('previewModal') previewModal!: TemplateRef; + previewModalRef!: BsModalRef; + + constructor( + private projectService: ProjectService, + private modalService: BsModalService + ) {} ngOnInit() { - this.project.getAllDatasets().subscribe({ - next: result => { - this.datasets = result; + this.loadDatasets(); + } + + loadDatasets() { + this.loading = true; + this.error = null; + this.projectService.getAllDatasets().subscribe({ + next: list => { + this.datasets = list; this.loading = false; }, - error: () => { - this.error = 'Failed to load datasets'; + error: e => { + this.error = e.message || 'Failed to load datasets'; this.loading = false; } }); } + + preview(ds: BlobFile) { + this.selectedDataset = ds; + this.previewModalRef = this.modalService.show(this.previewModal, { class: 'modal-lg' }); + } + + closePreview() { + this.previewModalRef.hide(); + } } diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.html new file mode 100644 index 00000000..e735a5b3 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.html @@ -0,0 +1,26 @@ + \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.scss b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.scss new file mode 100644 index 00000000..edbc2d89 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.scss @@ -0,0 +1,41 @@ +.card-header { + background-color: var(--primary-color); + font-size: 18px; + font-weight: 500; +} + +.full-screen-modal { + max-width: 100%; + margin: 0; +} + +.spinner-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + gap: 20px; +} + +.spinner-border { + width: 3rem; + height: 3rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin: 9px; +} + +.commit { + width: max-content; + background-color: rgb(81, 81, 223); + color: white; + border-radius: 5px; + padding: 5px 10px; + cursor: pointer; + font-size: 15px; +} \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.spec.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.spec.ts new file mode 100644 index 00000000..8f7e544f --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminNotebookItemDisplayComponent } from './admin-notebook-item-display.component'; + +describe('AdminNotebookItemDisplayComponent', () => { + let component: AdminNotebookItemDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AdminNotebookItemDisplayComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminNotebookItemDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.ts new file mode 100644 index 00000000..c7ec7712 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/admin-notebook-item-display/admin-notebook-item-display.component.ts @@ -0,0 +1,229 @@ +import { Component, ElementRef, EventEmitter, Input, OnInit, Output, Renderer2, ViewChild } from '@angular/core'; +import { Notebook, NotebookFile } from '../../../../../../interfaces/notebook'; +import { ProjectService } from '../../../../../../services/project.service'; +import { HttpClient } from '@angular/common/http'; +import { JupyterLiteStorageService } from '../forageIndexDb'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; + +@Component({ + selector: 'app-admin-notebook-item-display', + templateUrl: './admin-notebook-item-display.component.html', + styleUrls: ['./admin-notebook-item-display.component.scss'] +}) +export class AdminNotebookItemDisplayComponent { + + + @Input() notebook: Notebook; + @Input() version: number; + + @Output() closeModal: EventEmitter = new EventEmitter(); + showSaveWarningModal = false; + showSaveNotebookModal = false; + + constructor(private projectService: ProjectService, private _renderer2: Renderer2, private http: HttpClient + , private sanitizer: DomSanitizer, private jupyterLiteStorageService: JupyterLiteStorageService + ) { } + + @ViewChild('observablehqPanel', { read: ElementRef }) observablehqPanel; + @ViewChild('jupyterFrame') jupyterFrame: ElementRef; + @ViewChild('notebookWindow') notebookWindow: ElementRef; + jupyterFrameSrc: SafeResourceUrl; + isLoading = true; + timeoutId: any; + notebookFile: NotebookFile; + commitChangesLoading = false; + + ngOnInit(): void { + window.addEventListener('message', this.receiveMessage.bind(this)); + if (this.notebook.type === 'notebook' || this.notebook.type === 'new') { + this.loadNotebook(); + } + this.setTimeoutForLoading(); + } + + ngAfterViewInit(): void { + if (this.notebook.type === 'observable') { + this.isLoading = false; + this.generateObservableNotebook(); + } + } + + setTimeoutForLoading(): void { + this.timeoutId = setTimeout(() => { + if (this.isLoading) { + this.isLoading = false; + this.closeModal.emit(); + console.log("some unknown error occurred , please open the notebook again."); + alert("some unknown error occurred , please open the notebook again."); + } + }, 30000); + } + + loadNotebook() { + const url = `../../../../../../../assets/jupyter/dist/lab/index.html?path=${this.notebook.name}${this.notebook.extension}`; + this.jupyterFrameSrc = this.sanitizer.bypassSecurityTrustResourceUrl(url); + + this.projectService.getNotebookFile(this.notebook, this.version) + .subscribe(nbContent => { + const notebookName = `${this.notebook.name}${this.notebook.extension}`; + const notebookData = { + content: nbContent, // The content of the notebook + created: new Date().toISOString(), + format: "json", + hash: null, + hash_algorithm: null, + last_modified: new Date().toISOString(), + mimetype: 'application/x-ipynb+json', + name: notebookName, + size: this.notebook.size, + path: notebookName, + type: 'notebook', + writable: true, + }; + + // Fetch and add datasets of the notebook + let datasets = this.notebook.observableNotebookDatasets; + if (datasets) { + datasets.forEach(dataset => { + this.projectService.downloadCSV(dataset.blobFileID).subscribe(data => { + const datasetName = dataset.datasetName; + const datasetData = { + content: data, // The content of the dataset + created: new Date().toISOString(), + format: "text", + last_modified: new Date().toISOString(), + mimetype: 'text/csv', + name: datasetName, + path: datasetName, + size: 0, + type: 'file', + writable: true, + } + + this.jupyterLiteStorageService.addFile(datasetName, datasetData).then( + () => { + }, + (error) => { + console.error('Error adding dataset:', error); + } + ); + }); + }); + } + + // Add the notebook + this.jupyterLiteStorageService.addFile(notebookName, notebookData).then( + () => { + console.log('File added successfully'); + }, + (error) => { + console.error('Error adding file:', error); + } + ); + }); + } + + receiveMessage(event: MessageEvent): void { + if (event.data === 'jupyterlite-load') { + this.isLoading = false; + clearTimeout(this.timeoutId); + console.log('Notebook loaded successfully'); + } + } + + generateObservableNotebook() { + + let script = this._renderer2.createElement('script'); + script.type = `module`; + script.text = this.generateScript; + this._renderer2.appendChild(this.observablehqPanel.nativeElement, script); + } + + get generateScript(): String { + return ` import {Runtime, Inspector} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js"; + var notebookLink = "https://api.` + this.notebook.uri.replace("https://", "") + `.js?v=3"; + import(notebookLink).then((define) =>{ + var notebook = define.default; + (new Runtime).module(notebook, name =>{ + return Inspector.into("#notebook")(); + }); + });` + } + + saveNotebook() { + this.showSaveNotebookModal = true; + } + + onConfirmSaveNotebook() { + if (this.notebook.type === 'notebook' || this.notebook.type === 'new') { + this.commitChangesLoading = true; + this.jupyterLiteStorageService.getFile(`${this.notebook.name}${this.notebook.extension}`).then( + (notebookJson) => { + const notebookBlob = new Blob([JSON.stringify(notebookJson.content)], { type: 'application/json' }); + const file = new File([notebookBlob], `${this.notebook.name}${this.notebook.extension}`, { type: 'application/json' }); + this.notebookFile = { + 'file': file, + 'name': `${this.notebook.name}`, + 'projectID': this.notebook.projectID, + } + this.projectService.uploadNotebookNewVersion(this.notebookFile, this.notebook.directory).subscribe(result => { + this.commitChangesLoading = false; + this.showSaveNotebookModal = false; + }); + }, + (error) => { + console.error('Error getting file:', error); + } + ); + } + } + + onCancelSaveNotebook() { + this.showSaveNotebookModal = false; + } + + closeNotebook() { + this.showSaveWarningModal = true; + } + + onConfirmSave() { + clearTimeout(this.timeoutId); + this.closeModal.emit(); + if (this.notebook.type === 'notebook' || this.notebook.type === 'new') { + this.jupyterLiteStorageService.getFile(`${this.notebook.name}${this.notebook.extension}`).then( + (file) => { + console.log('File:', file); + }, + (error) => { + console.error('Error getting file:', error); + } + ); + this.jupyterLiteStorageService.removeFile(`${this.notebook.name}${this.notebook.extension}`).then( + () => { + console.log('File removed successfully'); + }, + (error) => { + console.error('Error removing file:', error); + } + ); + } + + let datasets = this.notebook.observableNotebookDatasets; + if (datasets) { + datasets.forEach(dataset => { + this.jupyterLiteStorageService.removeFile(dataset.datasetName).then( + () => { + console.log(`Dataset ${dataset.datasetName} removed successfully`); + }, + (error) => { + console.error('Error removing dataset:', error); + } + ); + }); + } + } + + onCancelSave() { + this.showSaveWarningModal = false; + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/forageIndexDb.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/forageIndexDb.ts new file mode 100644 index 00000000..82ef1128 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item-display/forageIndexDb.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import * as localforage from 'localforage'; + +@Injectable({ + providedIn: 'root', +}) +export class JupyterLiteStorageService { + private filesStore: LocalForage; + private checkpointsStore: LocalForage; + + constructor() { + this.filesStore = localforage.createInstance({ + name: 'JupyterLite Storage', + storeName: 'files', // Object store name + description: 'Storage for JupyterLite files', + }); + this.checkpointsStore = localforage.createInstance({ + name: 'JupyterLite Storage', + storeName: 'checkpoints', // Object store name + description: 'Storage for JupyterLite checkpoints', + }); + } + + // Add a file to the IndexedDB + addFile(fileName: string, fileData: any): Promise { + return this.filesStore.setItem(fileName, fileData); + } + + // Get a file by its name + getFile(fileName: string): Promise { + return this.filesStore.getItem(fileName); + } + + getCheckpoints(fileName: string): Promise { + return this.checkpointsStore.getItem(fileName); + } + + removeFile(fileName: string): Promise { + return this.filesStore.removeItem(fileName); + } + + // Get all files + getAllFiles(): Promise { + const files: any[] = []; + return this.filesStore.iterate((value, key) => { + files.push({ key, value }); + }).then(() => files); + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item.component.html index b06904b7..642c6db7 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item.component.html +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item.component.html @@ -1 +1,52 @@ -

admin-notebook-item works!

+ +
+ + {{notebook.name+notebook.extension}} + +
+ + + +
+
+ + + + \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item.component.ts index 60ed75f8..8ccf63cd 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/admin-notebook-item/admin-notebook-item.component.ts @@ -1,4 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Input, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { Notebook } from '../../../../interfaces/notebook'; +import { ProjectService } from '../../../../services/project.service'; @Component({ selector: 'app-admin-notebook-item', @@ -7,9 +11,113 @@ import { Component, OnInit } from '@angular/core'; }) export class AdminNotebookItemComponent implements OnInit { - constructor() { } + constructor(private modalService: BsModalService, private router: Router, private projectService: ProjectService) { } + + @Input() notebook: Notebook; + @ViewChild('displayNotebookModal') displayNotebookModal: TemplateRef; + + @ViewChild('displayRenameNotebookModal') displayRenameNotebookModal: TemplateRef; + @ViewChild('displayDatasetsModal') displayDatasetsModal: TemplateRef; + + displayNotebookModalRef: BsModalRef; + + displayRenameNotebookModalRef: BsModalRef; + + displayDatasetsModalRef: BsModalRef; + + versions: number[] = []; + selectedVersion: number = 0; ngOnInit(): void { + if (this.notebook.type === "new") { + this.loadVersions(); + } + } + + loadVersions() { + this.projectService.getNotebookVersions(this.notebook).subscribe(versions => { + this.versions = versions; + if (this.versions.length > 0) { + this.selectedVersion = this.versions[0]; + } + }); + } + + onVersionChange(version: number) { + this.selectedVersion = version; + } + + showNotebook() { + this.displayNotebookModalRef = this.modalService.show(this.displayNotebookModal); + } + + showRenameNotebookModal() { + this.displayRenameNotebookModalRef = this.modalService.show(this.displayRenameNotebookModal); + } + + closeRenameNotebookModal() { + this.displayRenameNotebookModalRef.hide(); + } + + changeNotebookName(notebookName: string) { + this.notebook.name = notebookName; + } + + navigateToNotebook() { + let datasetParams = {}; + if (this.notebook.type === "observable") { + this.notebook.observableNotebookDatasets.forEach(observableNotebook => { + datasetParams[observableNotebook.datasetName] = observableNotebook.datasetURL; + }) + } + this.router.navigate([this.router.url + "/" + this.notebook.name], { + queryParams: { + isNotebook: true, + notebookId: this.notebook.notebookID, + version: this.selectedVersion, + ...datasetParams + }, queryParamsHandling: 'merge' + }); + } + + deleteNotebook() { + this.projectService.deleteNotebook(this.notebook.notebookID, this.selectedVersion, true).subscribe(res => { + + }) + } + + downloadNotebook() { + this.projectService.downloadNotebook(this.notebook, this.selectedVersion).subscribe(res => { + let url = window.URL.createObjectURL(res); + let a = document.createElement('a'); + document.body.appendChild(a); + a.setAttribute('style', 'display: none'); + a.href = url; + a.download = this.notebook.name + "_v" + this.selectedVersion + this.notebook.extension; + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); + }) + } + + goBack() { + console.log("Go Back"); + } + + openObservableNotebook() { + let datasetParams = {}; + if (this.notebook.type === "observable") { + this.notebook.observableNotebookDatasets.forEach(observableNotebook => { + datasetParams[observableNotebook.datasetName] = observableNotebook.datasetURL; + }) + let url = this.notebook.uri + "?"; + let queryParams = [] + for (const key of Object.keys(datasetParams)) { + queryParams.push(key + "=" + datasetParams[key]); + } + url += queryParams.join('&'); + window.open(url, '_blank'); + } } } diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/notebooks.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/notebooks.component.html index db96c244..9dbedae9 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/notebooks.component.html +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/notebooks.component.html @@ -21,7 +21,7 @@

All Notebooks

{{ i + 1 }} - {{ nb.name+nb.extension }} + {{ nb.route }} {{ @@ -34,3 +34,7 @@

All Notebooks

+ + + diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/notebooks.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/notebooks.component.ts index e85aa726..911908af 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/notebooks.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/notebooks.component.ts @@ -1,6 +1,9 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { Notebook } from 'src/app/interfaces/notebook'; import { ProjectService } from 'src/app/services/project.service'; +import { Params, Router, ActivatedRoute, NavigationEnd } from '@angular/router'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; + @Component({ selector: 'app-notebooks', @@ -11,11 +14,60 @@ export class NotebooksComponent implements OnInit { notebooks: Notebook[] = []; loading = false; error: string | null = null; + notebookID = null; + version: number = 0; + currentNotebook: Notebook = null; - constructor(private projectService: ProjectService) { } + constructor(private modalService: BsModalService, private projectService: ProjectService, private router: Router, private route: ActivatedRoute) { } + + @ViewChild('displayNotebookModal') displayNotebookModal: TemplateRef; + + displayNotebookModalRef: BsModalRef; ngOnInit(): void { this.loadNotebooks(); + this.router.events.subscribe((ev) => { + if (ev instanceof NavigationEnd) { + this.loadNotebooks(); + } + }); + + this.route.queryParams.subscribe((params: Params) => { + const { + isNotebook, + notebookId, + version + } = params; + this.notebookID = notebookId; + this.version = version; + }) + this.getNotebook(); + } + + getNotebook() { + this.projectService.getNotebook(this.notebookID).subscribe(result => { + this.currentNotebook = result; + this.displayNotebook(this.currentNotebook); + }); + } + + displayNotebook(notebook: Notebook) { + this.currentNotebook = notebook; + // console.log(this.currentNotebook); + this.displayNotebookModalRef = this.modalService.show(this.displayNotebookModal, { + backdrop: 'static', + }); + } + + closeDisplayNotebookModal() { + this.displayNotebookModalRef.hide(); + this.navigateToPreviousComponent(); + } + + navigateToPreviousComponent() { + let previousDirectory = this.router.url.split('/'); + previousDirectory.pop(); + this.router.navigate([previousDirectory.join('/')]) } loadNotebooks() { diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.html new file mode 100644 index 00000000..28553395 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.scss b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.scss new file mode 100644 index 00000000..312b3f1c --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.scss @@ -0,0 +1,78 @@ +.modal-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + animation: fadeIn 0.3s ease-in-out; + z-index: 4; +} + +.modal-content { + padding: 20px; + border-radius: 10px; + text-align: center; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + max-width: 400px; + width: 80%; + animation: slideIn 0.3s ease-in-out; +} + +.modal-content p { + font-size: 25px; + margin-bottom: 20px; +} + +button { + background-color: #0a090981; + border: none; + border-radius: 5px; + color: white; + padding: 10px 20px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.3s ease; + margin: 0 10px; + width: max-content; +} + +.button-css { + display: flex; + justify-content: space-evenly; +} + +button:hover { + background-color: #110a0acf; +} + +button:first-of-type { + background-color: #3769d4; +} + +button:first-of-type:hover { + background-color: #1541af; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes slideIn { + from { + transform: translateY(-20px); + } + + to { + transform: translateY(0); + } +} \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.spec.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.spec.ts new file mode 100644 index 00000000..91e33783 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SaveConfirmationModalComponent } from './save-confirmation-modal.component'; + +describe('SaveConfirmationModalComponent', () => { + let component: SaveConfirmationModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SaveConfirmationModalComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SaveConfirmationModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.ts new file mode 100644 index 00000000..c480d5cc --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-confirmation-modal/save-confirmation-modal.component.ts @@ -0,0 +1,23 @@ +import { Component, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'app-save-confirmation-modal', + templateUrl: './save-confirmation-modal.component.html', + styleUrls: ['./save-confirmation-modal.component.scss'] +}) +export class SaveConfirmationModalComponent { + @Output() confirmSave = new EventEmitter(); + @Output() cancelSave = new EventEmitter(); + + ngOnInit(): void { + console.log('SaveConfirmationModalComponent loaded'); + } + + onConfirmSave() { + this.confirmSave.emit(); + } + + onCancelSave() { + this.cancelSave.emit(); + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.html new file mode 100644 index 00000000..7461d04c --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.scss b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.scss new file mode 100644 index 00000000..312b3f1c --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.scss @@ -0,0 +1,78 @@ +.modal-background { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + animation: fadeIn 0.3s ease-in-out; + z-index: 4; +} + +.modal-content { + padding: 20px; + border-radius: 10px; + text-align: center; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + max-width: 400px; + width: 80%; + animation: slideIn 0.3s ease-in-out; +} + +.modal-content p { + font-size: 25px; + margin-bottom: 20px; +} + +button { + background-color: #0a090981; + border: none; + border-radius: 5px; + color: white; + padding: 10px 20px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.3s ease; + margin: 0 10px; + width: max-content; +} + +.button-css { + display: flex; + justify-content: space-evenly; +} + +button:hover { + background-color: #110a0acf; +} + +button:first-of-type { + background-color: #3769d4; +} + +button:first-of-type:hover { + background-color: #1541af; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes slideIn { + from { + transform: translateY(-20px); + } + + to { + transform: translateY(0); + } +} \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.spec.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.spec.ts new file mode 100644 index 00000000..26848b4c --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SaveNotebookModalComponent } from './save-notebook-modal.component'; + +describe('SaveNotebookModalComponent', () => { + let component: SaveNotebookModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SaveNotebookModalComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SaveNotebookModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.ts new file mode 100644 index 00000000..b29643e9 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/notebooks/save-notebook-modal/save-notebook-modal.component.ts @@ -0,0 +1,26 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +@Component({ + selector: 'app-save-notebook-modal', + templateUrl: './save-notebook-modal.component.html', + styleUrls: ['./save-notebook-modal.component.scss'] +}) +export class SaveNotebookModalComponent implements OnInit { + + @Input() loading: boolean; + @Output() saveNotebook = new EventEmitter(); + @Output() cancelSave = new EventEmitter(); + + ngOnInit(): void { + console.log('SaveNotebookModalComponent loaded'); + } + + onConfirmSave() { + this.saveNotebook.emit(); + } + + onCancelSave() { + this.cancelSave.emit(); + } + +} diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.html new file mode 100644 index 00000000..0eaca958 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.html @@ -0,0 +1,53 @@ + + + + + + diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.scss b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.spec.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.spec.ts new file mode 100644 index 00000000..6793b0c2 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProjectDisplayComponent } from './project-display.component'; + +describe('ProjectDisplayComponent', () => { + let component: ProjectDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ProjectDisplayComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProjectDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.ts new file mode 100644 index 00000000..6afe9358 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/project-display/project-display.component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit } from '@angular/core'; +import { EventEmitter, Input, Output } from '@angular/core'; +import { Project } from 'src/app/interfaces/project'; +import { ProjectService } from 'src/app/services/project.service'; + +@Component({ + selector: 'app-project-display', + templateUrl: './project-display.component.html', + styleUrls: ['./project-display.component.scss'] +}) +export class ProjectDisplayComponent implements OnInit { + + @Input() project : Project; + @Output() projectDeleted : EventEmitter = new EventEmitter(); + constructor( private projectService : ProjectService) { } + + ngOnInit(): void { + } + + deleteProject() { + this.projectService.deleteProject(this.project.projectID).subscribe(res => { + this.projectDeleted.emit(); + }) + } + +} diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/projects/projects.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/projects.component.html index 002cafdb..731856c4 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/components/projects/projects.component.html +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/projects.component.html @@ -13,6 +13,7 @@
All Projects
Visibility Date Created Last Updated + Actions @@ -23,6 +24,7 @@
All Projects
{{ p.visibility }} {{ p.dateCreated | date:'short' }} {{ p.lastUpdated | date:'short' }} + diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/projects/projects.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/projects.component.ts index 0141befe..ba71c967 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/components/projects/projects.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/projects/projects.component.ts @@ -16,15 +16,20 @@ export class ProjectsComponent implements OnInit { constructor(private projectService: ProjectService) { } ngOnInit() { + this.loadProjects(); + } + + loadProjects() { + this.loading = true; this.projectService.getProjectList().subscribe({ - next: result => { + next: (result) => { this.projects = result; this.loading = false; }, - error: () => { - this.error = 'Failed to load projects'; + error: (err) => { + this.error = 'Failed to load users'; this.loading = false; - } + }, }); } } diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.html new file mode 100644 index 00000000..d8bc70ff --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.html @@ -0,0 +1,53 @@ + + + + + + diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.scss b/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.spec.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.spec.ts new file mode 100644 index 00000000..ed0bc8f9 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserDisplayComponent } from './user-display.component'; + +describe('UserDisplayComponent', () => { + let component: UserDisplayComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UserDisplayComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UserDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.ts new file mode 100644 index 00000000..3608d852 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/users/user-display/user-display.component.ts @@ -0,0 +1,25 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { User } from 'src/app/interfaces/user'; +import { AccountService } from '../../../../services/account.service'; + +@Component({ + selector: 'app-user-display', + templateUrl: './user-display.component.html', + styleUrls: ['./user-display.component.scss'] +}) +export class UserDisplayComponent implements OnInit { + + @Input() user: User; + @Output() userDeleted : EventEmitter = new EventEmitter(); + constructor(private accountService: AccountService) { } + + ngOnInit(): void { + } + + deleteUser() { + this.accountService.deleteUser(this.user.id).subscribe(res => { + this.userDeleted.emit(); + }) + } + +} diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/users/users.component.html b/src/Analysim.Web/ClientApp/src/app/admin/components/users/users.component.html index 4dfe13c3..da2bb69c 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/components/users/users.component.html +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/users/users.component.html @@ -9,13 +9,17 @@
All Users
Username Email Date Created + Actions - {{u.userName}} - {{u.email}} - {{u.dateCreated | date:'short'}} + + {{ u.userName }} + + {{ u.email }} + {{ u.dateCreated | date : "short" }} + @@ -25,4 +29,4 @@
All Users
{{ error }}
- + \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/admin/components/users/users.component.ts b/src/Analysim.Web/ClientApp/src/app/admin/components/users/users.component.ts index ee2a5b8b..ec7924f0 100644 --- a/src/Analysim.Web/ClientApp/src/app/admin/components/users/users.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/admin/components/users/users.component.ts @@ -5,26 +5,31 @@ import { User } from '../../../interfaces/user'; @Component({ selector: 'app-users', templateUrl: './users.component.html', - styleUrls: ['./users.component.scss'] + styleUrls: ['./users.component.scss'], }) export class UsersComponent implements OnInit { users: User[] = []; loading = true; error: string = null; - constructor(private account: AccountService) { } + constructor(private account: AccountService) {} ngOnInit() { - this.account.getUserList() - .subscribe({ - next: result => { - this.users = result; - this.loading = false; - }, - error: err => { - this.error = 'Failed to load users'; - this.loading = false; - } - }); + this.loadUsers(); + } + + loadUsers() { + this.loading = true; + console.log('Loading users...'); + this.account.getUserList().subscribe({ + next: (result) => { + this.users = result; + this.loading = false; + }, + error: (err) => { + this.error = 'Failed to load users'; + this.loading = false; + }, + }); } } diff --git a/src/Analysim.Web/ClientApp/src/app/app-routing.module.ts b/src/Analysim.Web/ClientApp/src/app/app-routing.module.ts index ffa5fb42..8e7273a3 100644 --- a/src/Analysim.Web/ClientApp/src/app/app-routing.module.ts +++ b/src/Analysim.Web/ClientApp/src/app/app-routing.module.ts @@ -35,6 +35,7 @@ const routes: Routes = []; { path: 'profile/:username', component: ProfileComponent }, { path: 'project', loadChildren: () => import('./projects/projects.module').then(m => m.ProjectsModule) }, { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }, + { path: '404', component: NotFoundComponent }, { path: '**', component: NotFoundComponent } // todo: Add verify page routing and component ])], diff --git a/src/Analysim.Web/ClientApp/src/app/guards/admin.guard.ts b/src/Analysim.Web/ClientApp/src/app/guards/admin.guard.ts new file mode 100644 index 00000000..b0ee1d9f --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/guards/admin.guard.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { + CanActivate, + Router, +} from '@angular/router'; +import { firstValueFrom } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { AccountService } from '../services/account.service'; +import { User } from '../interfaces/user'; + +@Injectable({ providedIn: 'root' }) +export class AdminGuard implements CanActivate { + constructor( + private account: AccountService, + private router: Router + ) {} + + public async canActivate(): Promise { + const isLoggedIn = await firstValueFrom( + this.account.isLoggedIn.pipe(take(1)) + ); + if (!isLoggedIn) { + this.router.navigate(['/login']); + return false; + } + const user$ = await this.account.currentUser; + const user: User = await firstValueFrom( + user$.pipe(take(1)) + ); + if (!user) { + this.router.navigate(['/login']); + return false; + } + const isAdmin = await firstValueFrom( + this.account.getIsAdmin(user.userName).pipe(take(1)) + ); + if (!isAdmin) { + this.router.navigate(['/404']); + return false; + } + + return true; + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/navbar/navbar.component.html b/src/Analysim.Web/ClientApp/src/app/navbar/navbar.component.html index f4f5240e..e6e6133b 100644 --- a/src/Analysim.Web/ClientApp/src/app/navbar/navbar.component.html +++ b/src/Analysim.Web/ClientApp/src/app/navbar/navbar.component.html @@ -61,6 +61,11 @@