feat(scan-table): add scan table component for scandata from mongodb

This commit is contained in:
2025-01-24 19:22:52 +01:00
committed by wakonig_k
parent f55adfe5c9
commit 7ade5dd03b
16 changed files with 377 additions and 8 deletions

View File

@ -27,7 +27,8 @@
{
"glob": "**/*",
"input": "public"
}
},
"src/assets"
],
"styles": [
"@angular/material/prebuilt-themes/cyan-orange.css",
@ -95,5 +96,8 @@
}
}
}
},
"cli": {
"analytics": false
}
}

View File

@ -19,6 +19,8 @@
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"angular-gridster2": "^19.0.0",
"angular-star-rating": "^7.0.0",
"css-star-rating": "^1.3.1",
"gridstack": "^11.1.2",
"gridstack-angular": "^0.6.0-dev",
"plotly.js": "^2.35.3",
@ -2665,6 +2667,31 @@
"node": ">=18"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@inquirer/checkbox": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.6.tgz",
@ -3203,6 +3230,40 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
"integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==",
"license": "BSD-3-Clause"
},
"node_modules/@lit-labs/virtualizer": {
"version": "2.0.15",
"resolved": "https://registry.npmjs.org/@lit-labs/virtualizer/-/virtualizer-2.0.15.tgz",
"integrity": "sha512-5sTsia0TX0RTsPKcOvH8UZ8n1f8q02R4iGX1RkjgdrxKcwHH6NFC2rIcpwKjftW85trloWguenDafGG0+7I8Tg==",
"license": "BSD-3-Clause",
"dependencies": {
"lit": "^3.2.0",
"tslib": "^2.0.3"
}
},
"node_modules/@lit/context": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.3.tgz",
"integrity": "sha512-Auh37F4S0PZM93HTDfZWs97mmzaQ7M3vnTc9YvxAGyP3UItSK/8Fs0vTOGT+njuvOwbKio/l8Cx/zWL4vkutpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^1.6.2 || ^2.0.0"
}
},
"node_modules/@lit/reactive-element": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz",
"integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0"
}
},
"node_modules/@lmdb/lmdb-darwin-arm64": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.2.tgz",
@ -5336,6 +5397,12 @@
"@types/geojson": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
@ -5681,6 +5748,21 @@
"rxjs": "^7.0.0"
}
},
"node_modules/angular-star-rating": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/angular-star-rating/-/angular-star-rating-7.0.0.tgz",
"integrity": "sha512-GO4BxTeo8IIhzb/Ynd+8TPIeGKL1vjB/E18UBzo3EPcWOHmpWG8HDf2ZCQ3G08yKAx/OwqnDnR2WHECh4Ca/MA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/core": ">=10.0.0",
"@angular/forms": ">=10.0.0",
"css-star-rating": ">=1.3.1",
"rxjs": ">=6.5.5"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@ -7477,6 +7559,12 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-star-rating": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/css-star-rating/-/css-star-rating-1.3.1.tgz",
"integrity": "sha512-AVBuyoqdahg0+1tY5zAqD8eMLjHlKu7VEIn+76YoJL/UhOiNFllnLg8nI5rWC2OA7EyyaPj36D/rxW2N+ec6PA==",
"license": "MIT"
},
"node_modules/css-system-font-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz",
@ -10247,6 +10335,21 @@
],
"license": "BSD-3-Clause"
},
"node_modules/igniteui-webcomponents": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/igniteui-webcomponents/-/igniteui-webcomponents-5.2.1.tgz",
"integrity": "sha512-0SJ1Xr9VhYbPGPStw4qLelB+umFqWAboEurQ/4yN1iq4rIqP3w3QaU13hcDm/kLrTL4M9UH++XTisUeII3Bppg==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@floating-ui/dom": "^1.6.0",
"@lit-labs/virtualizer": "^2.0.10",
"@lit/context": "^1.1.0",
"lit": "^3.2.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -12019,6 +12122,37 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/lit": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz",
"integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^2.0.4",
"lit-element": "^4.1.0",
"lit-html": "^3.2.0"
}
},
"node_modules/lit-element": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz",
"integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0",
"@lit/reactive-element": "^2.0.4",
"lit-html": "^3.2.0"
}
},
"node_modules/lit-html": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz",
"integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/lmdb": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.2.tgz",

View File

@ -21,6 +21,8 @@
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"angular-gridster2": "^19.0.0",
"angular-star-rating": "^7.0.0",
"css-star-rating": "^1.3.1",
"gridstack": "^11.1.2",
"gridstack-angular": "^0.6.0-dev",
"plotly.js": "^2.35.3",

View File

@ -1,6 +1,7 @@
import {
APP_INITIALIZER,
ApplicationConfig,
importProvidersFrom,
provideZoneChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';
@ -14,6 +15,7 @@ import {
withInterceptorsFromDi,
} from '@angular/common/http';
import { AuthInterceptor } from './core/auth.interceptor';
import { StarRatingModule } from 'angular-star-rating';
const appConfigInitializerFn = (appConfig: AppConfigService) => {
return () => appConfig.loadAppConfig();
@ -37,5 +39,6 @@ export const appConfig: ApplicationConfig = {
useClass: AuthInterceptor,
multi: true,
},
importProvidersFrom(StarRatingModule.forRoot()),
],
};

View File

@ -0,0 +1,20 @@
export interface ScanDataResponse {
scan_id?: string;
scan_number?: number;
status?: "open" | "paused" | "aborted" | "halted" | "closed";
session_id?: string;
num_points?: number;
scan_name?: string;
scan_type?: "step" | "fly";
dataset_number?: number;
scan_report_devices?: string[];
user_metadata?: { [key: string]: any };
readout_priority?: { [key: "monitored" | "baseline" | "async" | "continuous" | "on_request" | string]: string[] };
scan_parameters?: { [key: "exp_time" | "frames_per_trigger" | "settling_time" | "readout_time" | string]: any };
request_inputs?: { [key: "arg_bundle" | "inputs" | "kwargs" | string]: any };
info?: { [key: string]: any };
timestamp?: number;
user_data?: { [key: string]: any };
user_rating?: number;
}

View File

@ -27,7 +27,7 @@ export class RedisConnectorService {
auth: {
user: 'john_doe',
token: '1234',
deployment: '678aa8d4875568640bd92176',
deployment: '678f5e85434914d3e0e2bb3f',
},
});

View File

@ -1,6 +1,7 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { inject, Injectable } from '@angular/core';
import { ServerSettingsService } from '../server-settings.service';
import { ScanDataResponse } from './model/scan-data';
@Injectable({
providedIn: 'root',
@ -73,3 +74,37 @@ export class AuthDataService extends RemoteDataService {
);
}
}
@Injectable({
providedIn: 'root',
})
export class ScanDataService extends RemoteDataService {
/**
* Method for getting the scan data
* @param sessionId Unique identifier for the session
* @param offset Pagination offset (default = 0)
* @param limit Number of records to retrieve (default = 100)
* @param fields List of fields to include in the response
* @param includeUserData Whether to include user-related data
* @param sort Sort order for the records as a dictionary
* @returns response from the server with the scan data
* @throws HttpErrorResponse if the request fails
* @throws TimeoutError if the request takes too long
*/
getScanData(sessionId: string, offset:number = 0, limit:number = 100, fields: Array<string> |null = null, includeUserData: boolean = false, sort: { [key: string]: number } | null = null) {
let headers = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json; charset=utf-8');
return this.get<Array<ScanDataResponse>>(
"scans/session",
{
session_id : sessionId,
offset : offset.toString(),
limit : limit.toString(),
fields: fields ? fields.join(',') : "",
sort: sort ? JSON.stringify(sort) : "",
includeUserData: includeUserData.toString()
},
headers
)
}
}

View File

@ -23,9 +23,10 @@
<mat-sidenav-content>
<div class="content">
<app-device-box [device]="'samx'" [signal_name]="'samx'"></app-device-box>
<!-- <app-device-box [device]="'samx'" [signal_name]="'samx'"></app-device-box>
<app-device-box [device]="'samy'" [signal_name]="'samy'"></app-device-box>
<app-queue-table></app-queue-table>
<app-queue-table></app-queue-table> -->
<app-scan-table></app-scan-table>
</div>
</mat-sidenav-content>
</mat-sidenav-container>

View File

@ -44,6 +44,6 @@ mat-sidenav {
color: var(--mat-sys-on-primary);
}
mat-sidenav-content {
margin-left: 0px;
}
// mat-sidenav-content {
// margin-left: 0px;
// }

View File

@ -6,6 +6,7 @@ import { MatIconModule } from '@angular/material/icon';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { ScanTableComponent } from '../scan-table/scan-table.component';
@Component({
selector: 'app-dashboard',
@ -16,6 +17,7 @@ import { MatButtonModule } from '@angular/material/button';
MatSidenavModule,
MatIconModule,
MatButtonModule,
ScanTableComponent,
],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.scss',

View File

@ -0,0 +1,39 @@
<div class = "button-container">
<button mat-raised-button (click)="ngOnInit()">Refresh</button>
</div>
<div class="table-container">
<mat-card>
<mat-toolbar color="primary"> ScanData Table </mat-toolbar>
<!-- <table mat-table *ngIf="tableData | async as data" [dataSource]="data" class="mat-elevation-z8"> -->
<table mat-table *ngIf="tableData() as data" [dataSource]="data" class="mat-elevation-z8">
<!-- Create table data dynamically -->
<ng-container *ngFor="let column of displayedColumns" [matColumnDef]="column">
<th mat-header-cell *matHeaderCellDef> {{ column | titlecase }} </th>
<td mat-cell *matCellDef="let element">
<ng-container *ngIf="column === 'timestamp'; else userRating">
{{ element[column] * 1000| date :'HH:mm:ss'}}
<br>
{{ element[column] * 1000 | date :'dd/MM/yyyy'}}
</ng-container>
<ng-template #userRating>
<div *ngIf="column === 'user_rating'; else showData">
<star-rating [starType]="'svg'" [rating]="element[column]"></star-rating>
</div>
</ng-template>
<ng-template #showData>
<p> {{ element[column] }}</p>
</ng-template>
</ng-container>
<!-- Header Row -->
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<!-- Data Rows -->
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</mat-card>
<mat-paginator [pageSize]=10 [pageSizeOptions]="[5, 10, 25, 100]" aria-label="Select page"></mat-paginator>
</div>

View File

@ -0,0 +1,13 @@
.button-container {
display: flex;
justify-content: space-between;
padding-top: 16px;
padding-left: 16px;
padding-bottom: 16px;
}
.table-container{
width: auto;
}
.mat-mdc-row:hover {
background-color: var(--mat-sys-secondary-container);
}

View File

@ -0,0 +1,91 @@
import { ChangeDetectionStrategy, Component, WritableSignal, signal, ViewChild } from '@angular/core';
import { ScanDataService } from '../core/remote-data.service';
import { ScanDataResponse } from '../core/model/scan-data';
import { firstValueFrom } from 'rxjs';
import { CommonModule } from '@angular/common';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { StarRatingModule } from 'angular-star-rating';
@Component({
selector: 'app-scan-table',
standalone: true,
imports: [CommonModule, MatPaginator, MatTableModule, MatIconModule, MatToolbarModule, MatCardModule, MatButtonModule, MatPaginatorModule, StarRatingModule],
templateUrl: './scan-table.component.html',
styleUrl: './scan-table.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ScanTableComponent {
tableData: WritableSignal<ScanDataResponse[]> = signal([]); // Initialize as empty array
limit: number = 100;
offset: number = 0;
displayedColumns: string[] = ['scan_number', 'status', 'num_points', 'scan_name', 'scan_type', 'dataset_number', 'timestamp', 'user_rating'];
ignoredEntries: string[] = ['scan_report_devices', 'user_metadata', 'readout_priority', 'scan_parameters', 'request_inputs', 'info'];
@ViewChild(MatPaginatorModule) paginator!: MatPaginatorModule;
constructor(private scanData: ScanDataService) {}
ngOnInit(): void {
let sessionId = '6793628df62026a414d9338e';
this.updateTableData(sessionId);
}
async updateTableData(sessionId: string) {
let data = await firstValueFrom(
this.scanData.getScanData(sessionId, this.offset, this.limit, this.displayedColumns, false, { scan_number: -1 })
);
console.log('Received data: ', data);
for (const entry of data) {
if (entry.user_data && entry.user_data['user_rating'])
{
entry.user_rating = entry.user_data['user_rating'];
}
}
console.log('Received data: ', data);
this.tableData.set(data); // Update the signal value
}
}
// @Component({
// selector: 'app-scan-table',
// standalone: true,
// imports: [CommonModule, MatPaginator, MatTableModule, MatIconModule, MatToolbarModule, MatCardModule, MatButtonModule, MatPaginatorModule],
// templateUrl: './scan-table.component.html',
// styleUrl: './scan-table.component.scss',
// changeDetection: ChangeDetectionStrategy.OnPush,
// })
// export class ScanTableComponent {
// tableData!: Promise<ScanDataResponse[]>;
// limit: number = 100;
// offset: number = 0;
// displayedColumns: string[] = ['scan_number', 'status', "num_points", "scan_name", "scan_type", "dataset_number", "timestamp", "user_rating"];
// ignoredEntries: string[] = ["scan_report_devices", "user_metadata", "readout_priority", "scan_parameters", "request_inputs", "info"];
// // , "scan_name", "scan_type"];
// @ViewChild(MatPaginatorModule) paginator!: MatPaginatorModule;
// constructor( private scanData: ScanDataService) {
// }
// ngOnInit(): void {
// let sessionId = "6793628df62026a414d9338e";
// this.tableData = this.updateTableData(sessionId = "6793628df62026a414d9338e");
// // this.tableData = this.scanData.getScanData(sessionId = "6793628df62026a414d9338e");
// }
// async updateTableData(sessionId: string, offset: number = 0, limit: number = 100) {
// const data = await firstValueFrom(this.scanData.getScanData(sessionId=sessionId, offset=this.offset, limit=this.limit, this.displayedColumns, false, {"scan_number": -1}));
// console.log("Received data: ", data);
// // this.tableData = data;
// return data;
// }
// }

View File

@ -0,0 +1,24 @@
<svg style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<symbol id="star-empty" viewBox="0 0 34 32">
<title>star-empty</title>
<path class="path-star-empty"
d="M33.412 12.395l-11.842-1.021-4.628-10.904-4.628 10.92-11.842 1.005 8.993 7.791-2.701 11.579 10.179-6.144 10.179 6.144-2.685-11.579 8.976-7.791zM16.941 22.541l-6.193 3.739 1.647-7.049-5.468-4.744 7.214-0.626 2.8-6.638 2.816 6.654 7.214 0.626-5.468 4.744 1.647 7.049-6.209-3.755z"/>
</symbol>
<symbol id="star-half" viewBox="0 0 34 32">
<title>star-half</title>
<path class="path-star-half"
d="M 33.412,12.395 21.57,11.374 16.942,0.47 12.314,11.39 0.472,12.395 9.465,20.186 6.764,31.765 16.943,25.621 27.122,31.765 24.437,20.186 33.413,12.395 Z M 16.941,22.541 c 0,0 -0.297971,-14.6455833 0,-15.318 l 2.816,6.654 7.214,0.626 -5.468,4.744 1.647,7.049 z"/>
</symbol>
<symbol id="star-filled" viewBox="0 0 34 32">
<title>star-filled</title>
<path class="path-star-filled"
d="M16.941 25.621l10.179 6.144-2.701-11.579 8.993-7.791-11.842-1.005-4.628-10.92-4.628 10.92-11.842 1.005 8.993 7.791-2.701 11.579z"/>
</symbol>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -2,6 +2,7 @@
@use "@angular/material" as mat;
@import "gridstack/dist/gridstack.min.css";
@import "gridstack/dist/gridstack-extra.min.css";
@import 'css-star-rating/scss/star-rating';
// gridstack {
// display: grid;