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 @@
+
+
+ Preview
+
+
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to delete this dataset ?
+
+ {{ dataset.name }}
+
+
+
+
+
+
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 @@
-
-
-
-
-
-
-
- Name
- Directory
- Size
- Date Created
-
-
-
-
- {{d.name}}{{d.extension}}
- {{d.directory || '/'}}
- {{d.size | number}} bytes
- {{d.dateCreated | date:'short'}}
-
-
-
+
+
+
All Datasets
+
Loading…
+
{{ error }}
-
-
{{ error }}
-
+
+
+
+ Name
+ Directory
+ Size (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 @@
+
+
+
+
+
+
+
Loading Notebook...
+
+
+
+
+
+
+
\ 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!
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to delete this item ?
+
+ {{notebook.name}}{{notebook.extension}} version v{{selectedVersion}}
+
+ {{notebook.name}}{{notebook.extension}}
+ {{notebook.name}} folder
+
+
+
+
+
+
\ 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 @@
+
+
+
Please make sure to save all the files before leaving !!
+
+ Already Saved
+ Cancel
+
+
+
\ 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 @@
+
+
+
Please make sure to save the notebook by pressing ctrl + S before committing changes !!
+
+
+ commit changes
+ committing..
+
+ Cancel
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to delete this project ?
+
+ {{ project.name }}
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to delete this user ?
+
+ {{ user.userName }}
+
+
+
+
+
+
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 @@