;
+
+ options: GridsterConfig = {
+ gridType: GridType.Fit,
+ compactType: CompactType.None,
+ margin: 1,
+ outerMargin: true,
+ outerMarginTop: null,
+ outerMarginRight: null,
+ outerMarginBottom: null,
+ outerMarginLeft: null,
+ useTransformPositioning: true,
+ mobileBreakpoint: 640,
+ minCols: 40,
+ maxCols: 40,
+ minRows: 20,
+ maxRows: 20,
+ minColWidth: 300,
+ maxItemCols: 100,
+ minItemCols: 1,
+ maxItemRows: 100,
+ minItemRows: 1,
+ maxItemArea: 2500,
+ minItemArea: 1,
+ defaultItemCols: 1,
+ defaultItemRows: 1,
+ // fixedColWidth: 105,
+ // fixedRowHeight: 105,
+ keepFixedHeightInMobile: false,
+ keepFixedWidthInMobile: false,
+ scrollSensitivity: 50,
+ scrollSpeed: 20,
+ enableEmptyCellClick: false,
+ enableEmptyCellContextMenu: false,
+ enableEmptyCellDrop: false,
+ enableEmptyCellDrag: false,
+ enableOccupiedCellDrop: false,
+ emptyCellDragMaxCols: 50,
+ emptyCellDragMaxRows: 50,
+ ignoreMarginInRow: false,
+ draggable: {
+ enabled: true,
+ },
+ resizable: {
+ enabled: true,
+ },
+ swap: true,
+ pushItems: true,
+ disablePushOnDrag: false,
+ disablePushOnResize: false,
+ pushDirections: { north: true, east: true, south: true, west: true },
+ pushResizeItems: false,
+ displayGrid: DisplayGrid.None,
+ disableWindowResize: false,
+ disableWarnings: false,
+ scrollToNewItems: false,
+ };
+
+ optionsEdit: GridsterConfig;
+ toolbarOptions: GridsterConfig;
+
+ constructor() {
+ this.dashboard = [];
+ this.optionsEdit = JSON.parse(JSON.stringify(this.options));
+ this.toolbarOptions = JSON.parse(JSON.stringify(this.options));
+ }
+
+ ngOnInit(): void {
+ this.optionsEdit = JSON.parse(JSON.stringify(this.options)); // seriously??? I cannot believe that's the only way to perform a deep copy of an object
+ this.optionsEdit.draggable = { enabled: true };
+ this.optionsEdit.resizable = { enabled: true };
+ this.optionsEdit.displayGrid = DisplayGrid.Always;
+ this.toolbarOptions.minCols = 40;
+ this.toolbarOptions.maxCols = 40;
+ this.toolbarOptions.minRows = 1;
+ this.toolbarOptions.maxRows = 1;
+
+ this.dashboard = [
+ { cols: 2, rows: 1, y: 0, x: 0 },
+ { cols: 2, rows: 2, y: 0, x: 2, hasContent: true },
+ { cols: 1, rows: 1, y: 0, x: 4 },
+ { cols: 1, rows: 1, y: 2, x: 5 },
+ { cols: 1, rows: 1, y: 1, x: 0 },
+ { cols: 1, rows: 1, y: 1, x: 0 },
+ {
+ cols: 2,
+ rows: 2,
+ y: 3,
+ x: 5,
+ minItemRows: 2,
+ minItemCols: 2,
+ label: 'Min rows & cols = 2',
+ },
+ {
+ cols: 2,
+ rows: 2,
+ y: 2,
+ x: 0,
+ maxItemRows: 2,
+ maxItemCols: 2,
+ label: 'Max rows & cols = 2',
+ },
+ {
+ cols: 2,
+ rows: 1,
+ y: 2,
+ x: 2,
+ dragEnabled: true,
+ resizeEnabled: true,
+ label: 'Drag&Resize Enabled',
+ },
+ {
+ cols: 1,
+ rows: 1,
+ y: 2,
+ x: 4,
+ dragEnabled: false,
+ resizeEnabled: false,
+ label: 'Drag&Resize Disabled',
+ },
+ { cols: 1, rows: 1, y: 2, x: 6 },
+ ];
+
+ console.log('DashboardComponent initialized');
+ }
+}
diff --git a/frontend/bec_atlas/src/app/device-box/device-box.component.html b/frontend/bec_atlas/src/app/device-box/device-box.component.html
new file mode 100644
index 0000000..6e13359
--- /dev/null
+++ b/frontend/bec_atlas/src/app/device-box/device-box.component.html
@@ -0,0 +1,9 @@
+
+
+ {{ device }}
+
+
+ {{ readback_signal() }}
+
+
+
\ No newline at end of file
diff --git a/frontend/bec_atlas/src/app/device-box/device-box.component.scss b/frontend/bec_atlas/src/app/device-box/device-box.component.scss
new file mode 100644
index 0000000..c7bda32
--- /dev/null
+++ b/frontend/bec_atlas/src/app/device-box/device-box.component.scss
@@ -0,0 +1,44 @@
+
+.device-box {
+ max-width: 100px;
+ max-height: 100px;
+}
+.center-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%; /* Ensures the mat-card-content takes the full height */
+ text-align: center; /* Centers text within the content */
+}
+
+mat-card {
+ width: 100%; /* Ensure card takes the full width of its container */
+ height: 100%; /* Ensure card takes the full height of its container */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--mat-sys-secondary-container);
+ color: var(--mat-sys-on-secondary-container);
+}
+
+mat-card.inner-card {
+ flex-grow: 1;
+ width: 100%;
+ height: auto; /* Adjust height as needed */
+ border-top-left-radius: 0px; /* Removes border radius */
+ border-top-right-radius: 0px; /* Removes border radius */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ color: var(--mat-sys-on-primary); /* Sets the text color */
+ background: var(--mat-sys-primary) /* Sets the background color */
+}
+
+mat-card-content {
+ text-align: center; /* Ensures text itself is centered */
+}
+
+mat-card-title {
+ font-size: 0.8em; /* Increases the font size of the title */
+}
\ No newline at end of file
diff --git a/frontend/bec_atlas/src/app/device-box/device-box.component.spec.ts b/frontend/bec_atlas/src/app/device-box/device-box.component.spec.ts
new file mode 100644
index 0000000..0992233
--- /dev/null
+++ b/frontend/bec_atlas/src/app/device-box/device-box.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DeviceBoxComponent } from './device-box.component';
+
+describe('DeviceBoxComponent', () => {
+ let component: DeviceBoxComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [DeviceBoxComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(DeviceBoxComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/bec_atlas/src/app/device-box/device-box.component.ts b/frontend/bec_atlas/src/app/device-box/device-box.component.ts
new file mode 100644
index 0000000..12c6bde
--- /dev/null
+++ b/frontend/bec_atlas/src/app/device-box/device-box.component.ts
@@ -0,0 +1,42 @@
+import { Component, computed, Input, Signal } from '@angular/core';
+import { RedisConnectorService } from '../core/redis-connector.service';
+import { MessageEndpoints } from '../core/redis_endpoints';
+import { MatCardModule } from '@angular/material/card';
+
+@Component({
+ selector: 'app-device-box',
+ imports: [MatCardModule],
+ templateUrl: './device-box.component.html',
+ styleUrl: './device-box.component.scss',
+})
+export class DeviceBoxComponent {
+ signal!: Signal;
+ readback_signal!: Signal;
+
+ @Input()
+ device!: string;
+
+ @Input()
+ signal_name!: string;
+
+ constructor(private redisConnector: RedisConnectorService) {}
+
+ ngOnInit(): void {
+ this.signal = this.redisConnector.register(
+ MessageEndpoints.device_readback(this.device)
+ );
+ this.readback_signal = computed(() => {
+ let data = this.signal();
+ if (!data) {
+ return 'N/A';
+ }
+ if (!data.data.signals[this.signal_name]) {
+ return 'N/A';
+ }
+ if (typeof data.data.signals[this.signal_name].value === 'number') {
+ return data.data.signals[this.signal_name].value.toFixed(2);
+ }
+ return data.data.signals[this.signal_name].value;
+ });
+ }
+}
diff --git a/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.html b/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.html
new file mode 100644
index 0000000..226e235
--- /dev/null
+++ b/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.html
@@ -0,0 +1,22 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.scss b/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.scss
new file mode 100644
index 0000000..5e5d7ee
--- /dev/null
+++ b/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.scss
@@ -0,0 +1,37 @@
+
+:host {
+ display: block;
+ height: 100vh; /* Full-screen height */
+ width: 100vw; /* Full-screen width */
+ overflow: hidden;
+ }
+
+ .grid-stack {
+ display: block;
+ overflow: hidden;
+ height: 100%;
+ min-height: 100% !important;
+ }
+
+ .grid-stack-item-content {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ // background: #007bff;
+ // background-color: #18bc9c;
+ color: rgb(24, 7, 7);
+ // border: 1px solid #ddd;
+ }
+ $columns: 20;
+ @function fixed($float) {
+ @return round($float * 1000) / 1000; // total 2+3 digits being %
+ }
+ .gs-#{$columns} > .grid-stack-item {
+
+ width: fixed(100% / $columns);
+
+ @for $i from 1 through $columns - 1 {
+ &[gs-x='#{$i}'] { left: fixed((100% / $columns) * $i); }
+ &[gs-w='#{$i+1}'] { width: fixed((100% / $columns) * ($i+1)); }
+ }
+ }
\ No newline at end of file
diff --git a/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.spec.ts b/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.spec.ts
new file mode 100644
index 0000000..0f441a7
--- /dev/null
+++ b/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GridstackTestComponent } from './gridstack-test.component';
+
+describe('GridstackTestComponent', () => {
+ let component: GridstackTestComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [GridstackTestComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(GridstackTestComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.ts b/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.ts
new file mode 100644
index 0000000..45b0e57
--- /dev/null
+++ b/frontend/bec_atlas/src/app/gridstack-test/gridstack-test.component.ts
@@ -0,0 +1,141 @@
+/**
+ * Example using Angular ngFor to loop through items and create DOM items
+ */
+
+import { CommonModule } from '@angular/common';
+import {
+ Component,
+ AfterViewInit,
+ Input,
+ ViewChildren,
+ QueryList,
+ ElementRef,
+} from '@angular/core';
+import {
+ GridItemHTMLElement,
+ GridStack,
+ GridStackNode,
+ GridStackWidget,
+ Utils,
+ GridStackOptions,
+} from 'gridstack';
+
+// unique ids sets for each item for correct ngFor updating
+let ids = 1;
+
+@Component({
+ selector: 'app-gridstack-test',
+ imports: [CommonModule],
+ templateUrl: './gridstack-test.component.html',
+ styleUrls: ['./gridstack-test.component.scss'],
+})
+export class GridStackTestComponent implements AfterViewInit {
+ /** list of HTML items that we track to know when the DOM has been updated to make/remove GS widgets */
+ @ViewChildren('gridStackItem') gridstackItems!: QueryList<
+ ElementRef
+ >;
+
+ /** set the items to display. */
+ @Input() public set items(list: GridStackWidget[]) {
+ this._items = list || [];
+ this._items.forEach((w) => (w.id = w.id || String(ids++))); // make sure a unique id is generated for correct ngFor loop update
+ }
+ public get items(): GridStackWidget[] {
+ return this._items;
+ }
+
+ private grid!: GridStack;
+ public _items!: GridStackWidget[];
+
+ constructor() {
+ this.items = [
+ { x: 0, y: 0 },
+ { x: 1, y: 1 },
+ { x: 2, y: 2 },
+ { x: 2, y: 3 },
+ ];
+ }
+
+ // wait until after DOM is ready to init gridstack - can't be ngOnInit() as angular ngFor needs to run first!
+ public ngAfterViewInit() {
+ const N_ROWS = 30;
+ this.grid = GridStack.init({
+ margin: 0,
+ float: true,
+ animate: true,
+ minRow: 4,
+ maxRow: N_ROWS,
+ cellHeight: 100 / N_ROWS,
+ cellHeightUnit: '%',
+ column: 20,
+ alwaysShowResizeHandle: true,
+ }).on('change added', (event: Event, nodes: GridStackNode[]) =>
+ this.onChange(nodes)
+ );
+
+ // sync initial actual valued rendered (in case init() had to merge conflicts)
+ this.onChange();
+
+ this.gridstackItems.changes.subscribe(() => {
+ const layout: GridStackWidget[] = [];
+ this.gridstackItems.forEach((ref) => {
+ const n =
+ ref.nativeElement.gridstackNode ||
+ this.grid.makeWidget(ref.nativeElement).gridstackNode;
+ if (n) layout.push(n);
+ });
+ this.grid.load(layout); // efficient that does diffs only
+ });
+ }
+
+ /** Optional: called when given widgets are changed (moved/resized/added) - update our list to match.
+ * Note this is not strictly necessary as demo works without this
+ */
+ public onChange(list = this.grid.engine.nodes) {
+ setTimeout(
+ () =>
+ // prevent new 'added' items from ExpressionChangedAfterItHasBeenCheckedError. TODO: find cleaner way to sync outside Angular change detection ?
+ list.forEach((n) => {
+ const item = this._items.find((i) => i.id === n.id);
+ if (item) Utils.copyPos(item, n);
+ }),
+ 0
+ );
+ }
+
+ /**
+ * CRUD operations
+ */
+ public add() {
+ // new array isn't required as Angular seem to detect changes to content
+ // this.items = [...this.items, { x:3, y:0, w:3, id:String(ids++) }];
+ this.items.push({ x: 3, y: 0, w: 3, id: String(ids++) });
+ }
+
+ public delete() {
+ this.items.pop();
+ }
+
+ public modify() {
+ // this will only update the DOM attr (from the ngFor loop in our template above)
+ // but not trigger gridstackItems.changes for GS to auto-update, so call GS update() instead
+ // this.items[0].w = 2;
+ const n = this.grid.engine.nodes[0];
+ if (n?.el) this.grid.update(n.el, { w: 3 });
+ }
+
+ public newLayout() {
+ this.items = [
+ // test updating existing and creating new one
+ { x: 0, y: 1, id: '1' },
+ { x: 1, y: 1, id: '2' },
+ // {x:2, y:1, id:3}, // delete item
+ { x: 3, y: 0, w: 3 }, // new item
+ ];
+ }
+
+ // ngFor unique node id to have correct match between our items used and GS
+ identify(index: number, w: GridStackWidget) {
+ return w.id;
+ }
+}
diff --git a/frontend/bec_atlas/src/index.html b/frontend/bec_atlas/src/index.html
new file mode 100644
index 0000000..0dfe90e
--- /dev/null
+++ b/frontend/bec_atlas/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ BecAtlas
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/bec_atlas/src/main.ts b/frontend/bec_atlas/src/main.ts
new file mode 100644
index 0000000..35b00f3
--- /dev/null
+++ b/frontend/bec_atlas/src/main.ts
@@ -0,0 +1,6 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+import { AppComponent } from './app/app.component';
+
+bootstrapApplication(AppComponent, appConfig)
+ .catch((err) => console.error(err));
diff --git a/frontend/bec_atlas/src/styles.scss b/frontend/bec_atlas/src/styles.scss
new file mode 100644
index 0000000..c5a36f7
--- /dev/null
+++ b/frontend/bec_atlas/src/styles.scss
@@ -0,0 +1,45 @@
+/* You can add global styles to this file, and also import other style files */
+@use '@angular/material' as mat;
+@import "gridstack/dist/gridstack.min.css";
+@import "gridstack/dist/gridstack-extra.min.css";
+
+// gridstack {
+// display: grid;
+// width: 100%; /* Ensure the grid uses the full width */
+// height: 100%; /* Ensure grid height is sufficient */
+// }
+
+// gridstack-item {
+// display: block;
+// }
+
+html, body { height: 100%; }
+body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
+
+
+
+html {
+ color-scheme: light dark;
+
+ // Light theme
+ @media (prefers-color-scheme: light) {
+ @include mat.theme((
+ color: mat.$violet-palette,
+ typography: Roboto,
+ density: 0
+ ), $overrides: (
+ primary-container: orange, // Light-specific override
+ ));
+ }
+
+ // Dark theme
+ @media (prefers-color-scheme: dark) {
+ @include mat.theme((
+ color: mat.$violet-palette,
+ typography: Roboto,
+ density: 0
+ ), $overrides: (
+ primary-container: darkorange, // Dark-specific override
+ ));
+ }
+ }
\ No newline at end of file
diff --git a/frontend/bec_atlas/tsconfig.app.json b/frontend/bec_atlas/tsconfig.app.json
new file mode 100644
index 0000000..3775b37
--- /dev/null
+++ b/frontend/bec_atlas/tsconfig.app.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts"
+ ],
+ "include": [
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/frontend/bec_atlas/tsconfig.json b/frontend/bec_atlas/tsconfig.json
new file mode 100644
index 0000000..5525117
--- /dev/null
+++ b/frontend/bec_atlas/tsconfig.json
@@ -0,0 +1,27 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./dist/out-tsc",
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "bundler",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022"
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/frontend/bec_atlas/tsconfig.spec.json b/frontend/bec_atlas/tsconfig.spec.json
new file mode 100644
index 0000000..5fb748d
--- /dev/null
+++ b/frontend/bec_atlas/tsconfig.spec.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}