Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 },
Expand Down
21 changes: 18 additions & 3 deletions src/Analysim.Web/ClientApp/src/app/admin/admin.module.ts
Original file line number Diff line number Diff line change
@@ -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 { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<td>
<button class="btn btn-sm btn-outline-primary mr-2" (click)="preview()">
Preview
</button>
<button
type="button"
class="btn btn-secondary btn-sm"
data-toggle="modal"
[attr.data-target]="'#deleteModal' + dataset.blobFileID"
>
<i class="fa fa-trash mr-1"></i>
</button>
</td>

<!--Delete User Modal-->
<div
class="modal fade"
[id]="'deleteModal' + dataset.blobFileID"
tabindex="-1"
role="dialog"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Delete Window</h5>
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div>Are you sure you want to delete this dataset ?</div>
<div>
<span> {{ dataset.name }} </span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn" data-dismiss="modal">Close</button>
<button
type="button"
class="btn"
(click)="deleteDataset()"
data-dismiss="modal"
>
Delete
</button>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -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<DatasetActionsComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DatasetActionsComponent ]
})
.compileComponents();

fixture = TestBed.createComponent(DatasetActionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -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<any> = new EventEmitter<any>();
@Output() datasetPreview: EventEmitter<any> = new EventEmitter<any>();
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();
})
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
<!-- src/app/admin/admin-datasets/admin-datasets.component.html -->
<div class="card">
<div class="card-header">
<h5>All Datasets</h5>
</div>
<div class="card-body p-0">
<table class="table mb-0" *ngIf="!loading && !error">
<thead class="thead-light">
<tr>
<th>Name</th>
<th>Directory</th>
<th>Size</th>
<th>Date Created</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let d of datasets">
<td>{{d.name}}{{d.extension}}</td>
<td>{{d.directory || '/'}}</td>
<td>{{d.size | number}} bytes</td>
<td>{{d.dateCreated | date:'short'}}</td>
</tr>
</tbody>
</table>
<!-- src/app/admin/components/datasets/admin-datasets.component.html -->
<div class="card p-3">
<h4>All Datasets</h4>
<div *ngIf="loading" class="alert alert-info">Loading…</div>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>

<div class="p-3" *ngIf="loading">
<div class="spinner-border spinner-border-sm mr-2"></div>Loading…
</div>
<div class="p-3 text-danger" *ngIf="error">{{ error }}</div>
</div>
<table class="table table-striped" *ngIf="!loading && !error">
<thead>
<tr>
<th>Name</th>
<th>Directory</th>
<th>Size (bytes)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let ds of datasets">
<td>{{ ds.name + ds.extension }}</td>
<td>{{ ds.directory }}</td>
<td>{{ ds.size }}</td>
<app-dataset-actions [dataset]="ds" (datasetPreview)="preview($event)" (datasetDeleted)="loadDatasets()"></app-dataset-actions>
</tr>
</tbody>
</table>
</div>

<ng-template #previewModal>
<div class="modal-header">
<h5 class="modal-title">Preview: {{ selectedDataset?.name + selectedDataset?.extension }}</h5>
<button type="button" class="close" (click)="closePreview()">&times;</button>
</div>
<div class="modal-body">
<app-csvdata-browser
[csvFile]="selectedDataset!"
style="display: block;"
></app-csvdata-browser>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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<any>;
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div #notebookWindow class="modal-content"
style="width: 100vw;height: 100vh;top: 0; left: 0;margin: 0;position: fixed; overflow: auto;">
<div class="header">
<h5 class="modal-title">Notebook</h5>
<div style="display: flex; gap: 15px;">
<div class="commit" (click)="saveNotebook()" *ngIf="!isLoading">commit changes</div>
<button type="button" #closebutton class="close" data-dismiss="modal"
(click)="closeNotebook()">&times;</button>
</div>
</div>
<div id="jupyterlite-container" *ngIf="notebook.type === 'new'">
<div *ngIf="isLoading" class="spinner-container">
<div class="spinner-border" role="status">
</div>
<div>Loading Notebook...</div>
</div>
<iframe #jupyterFrame [src]="jupyterFrameSrc" width="100%" style="height: 100vh;"></iframe>
</div>
<div #observablehqPanel class="m-3" *ngIf="notebook.type === 'observable'">
<div id="notebook"></div>
</div>
<app-save-confirmation-modal *ngIf="showSaveWarningModal" (confirmSave)="onConfirmSave()"
(cancelSave)="onCancelSave()"></app-save-confirmation-modal>
<app-save-notebook-modal *ngIf="showSaveNotebookModal" (saveNotebook)="onConfirmSaveNotebook()"
(cancelSave)="onCancelSaveNotebook()" [loading]="commitChangesLoading"></app-save-notebook-modal>
</div>
Loading