diff --git a/.gitignore b/.gitignore index b767049c..2b4e95f5 100644 --- a/.gitignore +++ b/.gitignore @@ -104,4 +104,9 @@ iOSInjectionProject/ # user compose override compose.override.yml **/.DS_Store -**/.vs \ No newline at end of file +**/.vs + +# AI tooling +.playwright-mcp/ +.wolf/ +.claude/ diff --git a/angular-client/src/components/argos-button/argos-button.component.css b/angular-client/src/components/argos-button/argos-button.component.css index 2975c4cb..f56ec9a9 100644 --- a/angular-client/src/components/argos-button/argos-button.component.css +++ b/angular-client/src/components/argos-button/argos-button.component.css @@ -23,3 +23,11 @@ transition-duration: 0.2s; background-color: #f04346b2; } + +@media (max-width: 768px) { + .btn { + padding: 6px 8px; + font-size: 13px; + min-height: 44px; + } +} diff --git a/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.css b/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.css index dfb3ee6b..22ccd7a3 100644 --- a/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.css +++ b/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.css @@ -7,3 +7,31 @@ height: 100%; margin: 0px 5px 0px 15px; } + +.general-buttons-row { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +@media (max-width: 768px) { + :host { + margin: 0; + width: 100%; + } + + .general-buttons-row { + gap: 6px; + width: 100%; + } + + :host ::ng-deep select-dropdown { + min-width: 130px; + } + + :host ::ng-deep select-dropdown .p-select { + min-width: 130px; + } +} diff --git a/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.html b/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.html index 75707249..8818f7e3 100644 --- a/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.html +++ b/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.html @@ -1,4 +1,4 @@ - +
@if (historicalOn()) { @@ -13,4 +13,4 @@ [placeholder]="selectorConfig().placeholder" [defaultValue]="selectorConfig().defaultValue" /> - +
diff --git a/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.ts b/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.ts index 304ac5b6..eee4236e 100644 --- a/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.ts +++ b/angular-client/src/pages/graph-page/graph-caption/general-buttons/general-buttons.component.ts @@ -5,14 +5,13 @@ import { Run } from 'src/utils/types.utils'; import { RunSelectorComponent } from '../run-selector/run-selector.component'; import { ButtonComponent } from '../../../../components/argos-button/argos-button.component'; import { SelectDropdownComponent } from '../../../../components/select-dropdown/select-dropdown.component'; -import HStackComponent from 'src/components/hstack/hstack.component'; @Component({ selector: 'general-buttons', templateUrl: './general-buttons.component.html', styleUrl: './general-buttons.component.css', standalone: true, - imports: [RunSelectorComponent, ButtonComponent, SelectDropdownComponent, HStackComponent] + imports: [RunSelectorComponent, ButtonComponent, SelectDropdownComponent] }) export class GeneralButtonsComponent { historicalOn = input(false); diff --git a/angular-client/src/pages/graph-page/graph-page.component.css b/angular-client/src/pages/graph-page/graph-page.component.css index 94c4d4fe..dec79f0b 100644 --- a/angular-client/src/pages/graph-page/graph-page.component.css +++ b/angular-client/src/pages/graph-page/graph-page.component.css @@ -7,6 +7,45 @@ flex-flow: column; } +/* Toolbar: replaces the old inline-styled hstack */ +.toolbar { + display: flex; + align-items: start; + justify-content: space-between; + align-self: flex-start; + width: 100%; +} + +.toolbar-left { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px; +} + +.toolbar-group { + display: flex; + align-items: center; + gap: 5px; +} + +.config-group { + margin-left: 15px; +} + +.input-label { + color: #fff; + font-size: 12px; + white-space: nowrap; +} + +.toolbar-header { + margin-top: 5px; + margin-bottom: 0; + padding-right: 15px; + flex-shrink: 0; +} + .graph-header { flex: 0 1 auto; } @@ -35,6 +74,10 @@ display: none; } +.mobile-value-strip { + display: none; +} + .graph { flex: 1 1 auto; } @@ -63,7 +106,36 @@ width: 100% !important; } +/* ======================== Mobile ======================== */ @media (max-width: 768px) { + .toolbar { + flex-direction: column; + align-items: stretch; + gap: 6px; + padding: 4px 8px; + } + + .toolbar-left { + flex-direction: column; + align-items: stretch; + gap: 6px; + } + + .toolbar-group { + flex-wrap: wrap; + gap: 6px; + } + + .config-group { + margin-left: 0; + } + + .toolbar-header { + padding-right: 0; + text-align: center; + margin-top: 2px; + } + .content-div { flex-flow: column; } @@ -76,11 +148,32 @@ display: block; } + .mobile-value-strip { + display: block; + } + .right-container { - flex: 1 1 auto; + height: 50vh; + min-height: 250px; + padding-right: 8px; + padding-left: 8px; } .graph-caption { flex: 0 0 auto; } + + ::ng-deep .time-range-input { + width: 70px !important; + } + + ::ng-deep .y-axis-input { + width: 55px !important; + } + + ::ng-deep .p-select-label, + ::ng-deep .p-select-label.p-placeholder { + width: auto !important; + min-width: 100px; + } } diff --git a/angular-client/src/pages/graph-page/graph-page.component.html b/angular-client/src/pages/graph-page/graph-page.component.html index 126fa1b3..3e604149 100644 --- a/angular-client/src/pages/graph-page/graph-page.component.html +++ b/angular-client/src/pages/graph-page/graph-page.component.html @@ -9,36 +9,37 @@ } @else {
- - - - - @if (realTime === true) { +
+
+
- } + + @if (realTime === true) { + + } +
-
- @if (this.realTime === true) { + @if (this.realTime === true) { +
- - + - } +
+ } - +
+ - +
- @if (onFaultPage) { - - } @else { - - } - +
+ @if (onFaultPage) { + + } @else { + + } +
+
+ + +
- - + @if (selectedDataTypes.length > 0) { + + }
@if (this.showSideBar) { - }
@@ -112,12 +111,6 @@ [graphConfig]="this.graphConfig" />
-
diff --git a/angular-client/src/pages/graph-page/graph-page.component.ts b/angular-client/src/pages/graph-page/graph-page.component.ts index 205b319f..223c7770 100644 --- a/angular-client/src/pages/graph-page/graph-page.component.ts +++ b/angular-client/src/pages/graph-page/graph-page.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { MessageService } from 'primeng/api'; import { BehaviorSubject, Subscription } from 'rxjs'; @@ -17,8 +17,8 @@ import { ButtonComponent } from '../../components/argos-button/argos-button.comp import { FaultButtonsComponent } from './graph-caption/fault-buttons/fault-buttons.component'; import { GeneralButtonsComponent } from './graph-caption/general-buttons/general-buttons.component'; import GraphSidebarComponent from './graph-sidebar/graph-sidebar.component'; -import HStackComponent from 'src/components/hstack/hstack.component'; import CustomGraphComponent from './graph/graph.component'; +import LiveValueStripComponent from './live-value-strip/live-value-strip.component'; import LoadingPageComponent from 'src/components/loading-page/loading-page.component'; import ErrorPageComponent from 'src/components/error-page/error-page.component'; import TypographyComponent from '../../components/typography/typography.component'; @@ -37,8 +37,8 @@ import { FormsModule } from '@angular/forms'; FaultButtonsComponent, GeneralButtonsComponent, GraphSidebarComponent, - HStackComponent, CustomGraphComponent, + LiveValueStripComponent, TypographyComponent, InputNumberModule, FormsModule @@ -52,6 +52,7 @@ export default class GraphPageComponent implements OnInit, OnDestroy { private topicSelectionService = inject(TopicSelectionService); private router = inject(Router); // for fault page navigation private route = inject(ActivatedRoute); + private cdr = inject(ChangeDetectorRef); // keep track of the subscriptions, that way we cancel all subs anywhere anytime subscriptions: Subscription[] = []; @@ -350,6 +351,7 @@ export default class GraphPageComponent implements OnInit, OnDestroy { this.persistentSubscriptions.push( dataTypesQueryResponse.isLoading.subscribe((isLoading: boolean) => { this.dataTypesIsLoading = isLoading; + this.cdr.detectChanges(); }) ); this.persistentSubscriptions.push( diff --git a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.html b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.html index a147985a..9da9b8a4 100644 --- a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.html +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.html @@ -9,7 +9,7 @@ styleClass="w-full md:w-[30rem]" (onNodeSelect)="nodeSelect($event)" (onNodeUnselect)="onNodeUnselect($event)" - [(selection)]="this.selectedNodes" + [(selection)]="selectedNodes" [filter]="true" filterPlaceholder="Select Data Type" styleClass="w-full" @@ -19,16 +19,11 @@ selectionMode="multiple" > -
-
- - @if (node.children.length === 0) { - - } -
+
+ + @if (node.children?.length === 0) { + + }
diff --git a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.ts b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.ts index b4821d60..b1610788 100644 --- a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.ts +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.ts @@ -1,153 +1,67 @@ +import { AsyncPipe } from '@angular/common'; import { Component, OnInit, OnDestroy, input, inject } from '@angular/core'; -import { DataType, Node } from 'src/utils/types.utils'; -import { FormControl, FormGroup } from '@angular/forms'; -import { BehaviorSubject, debounceTime, Subscription } from 'rxjs'; +import { Subscription } from 'rxjs'; import { TreeNode, PrimeTemplate } from 'primeng/api'; -import Storage from 'src/services/storage.service'; -import { decimalPipe } from 'src/utils/pipes.utils'; import { TreeNodeSelectEvent, TreeNodeUnSelectEvent, Tree } from 'primeng/tree'; -import { dataTypeNamePipe, dataTypesToNodes } from 'src/utils/dataTypes.utils'; +import Storage from 'src/services/storage.service'; +import { dataTypesToNodes } from 'src/utils/dataTypes.utils'; +import { mapNodesToTreeNodes, findSelectedTreeNodes, TreeNodeData } from 'src/utils/tree.utils'; +import { DataType } from 'src/utils/types.utils'; +import { TopicSelectionService } from 'src/services/topic-selection.service'; import { ButtonComponent } from '../../../../components/argos-button/argos-button.component'; import TypographyComponent from 'src/components/typography/typography.component'; -import { TopicSelectionService } from 'src/services/topic-selection.service'; -/** - * Sidebar component that displays the nodes and their data types. - * @param nodes The nodes to display. - * Has animations for when a node is selected to collapse and expand the associated datatypes - * - */ @Component({ selector: 'graph-sidebar-desktop', templateUrl: './graph-sidebar-desktop.component.html', styleUrls: ['./graph-sidebar-desktop.component.css'], standalone: true, - imports: [ButtonComponent, Tree, PrimeTemplate, TypographyComponent] + imports: [AsyncPipe, ButtonComponent, Tree, PrimeTemplate, TypographyComponent] }) export default class GraphSidebarDesktopComponent implements OnInit, OnDestroy { private topicSelectionService = inject(TopicSelectionService); private storage = inject(Storage); - dataTypes = input([]); - nodes: Node[] = []; - - filterForm: FormGroup = new FormGroup({ - searchFilter: new FormControl('') - }); - filterFormSubsription!: Subscription; - searchFilter: string = ''; - treeNodes: TreeNode[] = []; - selectedNodes?: TreeNode[]; // still needed for p-tree binding - treeInitialized = false; + dataTypes = input([]); + treeNodes: TreeNode[] = []; + selectedNodes?: TreeNode[]; - // Local list of selected DataTypes - private selectedDataTypesList: DataType[] = []; + private storageSubscriptions: Subscription[] = []; - /** - * Initializes the nodes with the visibility toggle. - */ ngOnInit(): void { - this.nodes = dataTypesToNodes(this.dataTypes()); - - // Callback to update search regex (debounced at 300 ms) - this.filterFormSubsription = this.filterForm.valueChanges.pipe(debounceTime(300)).subscribe((changes) => { - this.searchFilter = changes.searchFilter; - }); - - const mapToTreeNode = (node: Node): TreeNode => { - const displayValue = new BehaviorSubject('N/A'); - this.storage.get(node.topicName.slice(0, -1)).subscribe((value) => { - displayValue.next(decimalPipe(value.values[0], 3).toFixed(3) + value.unit); - }); - return { - label: node.name, - data: { ...node, displayValue }, - key: node.topicName, - children: node.nodes.value.map(mapToTreeNode), - selectable: node.nodes.value.length === 0 - }; - }; - - this.treeNodes = this.nodes.map(mapToTreeNode); - // Helper function to find selected nodes in the tree and expand their parent nodes - const findSelectedNodes = (nodes: TreeNode[]): TreeNode[] => { - const selected: TreeNode[] = []; - - // Map to track if a node contains selected children - const containsSelectedNode = new Map, boolean>(); - - // First pass: find all selected nodes - const findSelected = (nodes: TreeNode[], parents: TreeNode[] = []): void => { - for (const node of nodes) { - // Check if this is a leaf node and matches a selected data type - if (node.selectable && node.data?.dataType && this.topicSelectionService.isSelected(node.data.dataType)) { - selected.push(node); - - // Mark all parents as containing selected nodes - parents.forEach((parent) => containsSelectedNode.set(parent, true)); - } - - // Continue searching children - if (node.children && node.children.length > 0) { - findSelected(node.children as TreeNode[], [...parents, node]); - } - } - }; - - // Find selected nodes and track their parents - findSelected(nodes); - - // Expand all parent nodes that contain selected children - containsSelectedNode.forEach((hasSelectedChild, node) => { - if (hasSelectedChild) { - node.expanded = true; - } - }); - - return selected; - }; - - this.selectedNodes = findSelectedNodes(this.treeNodes); + const nodes = dataTypesToNodes(this.dataTypes()); + this.treeNodes = mapNodesToTreeNodes(nodes, this.storage, this.storageSubscriptions); + this.selectedNodes = findSelectedTreeNodes(this.treeNodes, this.topicSelectionService); } ngOnDestroy(): void { - this.filterFormSubsription.unsubscribe(); + this.storageSubscriptions.forEach((sub) => sub.unsubscribe()); } clearSelections = () => { this.treeNodes.forEach((n) => (n.expanded = false)); - this.selectedDataTypesList = []; this.topicSelectionService.clearSelection(); this.selectedNodes = undefined; }; - transformDataTypeName(dataTypeName: string) { - return dataTypeNamePipe(dataTypeName); - } - nodeSelect(event: TreeNodeSelectEvent) { const dt = event.node.data?.dataType; - if (dt && !this.selectedDataTypesList.includes(dt)) { - this.selectedDataTypesList.push(dt); + if (dt) { this.topicSelectionService.addDataType(dt); } } - // this is so awesome and made by chat onRowClick(evt: MouseEvent, node: TreeNode) { if (node.children?.length !== 0) { - // parent row evt.preventDefault(); - evt.stopPropagation(); // keep selection engine out - node.expanded = !node.expanded; // toggle expanded state + evt.stopPropagation(); + node.expanded = !node.expanded; } - /* leaf rows fall through → normal multi‑selection behaviour */ } onNodeUnselect(event: TreeNodeUnSelectEvent) { const dt = event.node.data?.dataType; if (dt) { - this.selectedDataTypesList = this.selectedDataTypesList.filter((x) => x !== dt); this.topicSelectionService.removeDataType(dt); } } diff --git a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.css b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.css index b80f7e7c..9d760e0e 100644 --- a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.css +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.css @@ -1,32 +1,47 @@ -.popup-tab { - height: fit-content; - bottom: 0px; - background-color: #1b1b1b; +.toggle-btn { position: fixed; - padding: 8px; - overflow: scroll; - max-width: 95%; - border-top-right-radius: 8px; - margin: 0px 0px 0px 8px; + bottom: calc(16px + env(safe-area-inset-bottom, 0px)); + right: calc(16px + env(safe-area-inset-right, 0px)); + z-index: 5; + padding: 12px 20px; + border: none; + border-radius: 8px; + background-color: #ef4343; + color: #fff; + font-size: 14px; + font-weight: 600; + font-family: inherit; + cursor: pointer; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + min-height: 44px; } -.container { +.toggle-btn:active { + background-color: #d63535; +} + +.sidebar-content { display: flex; - flex-flow: row; - justify-content: flex-start; - width: fit-content; - align-items: center; - gap: 16px; + flex-direction: column; + gap: 8px; + height: 100%; } -.subcontainer { +.tree-node-container { display: flex; - flex-flow: row; - width: fit-content; - justify-content: flex-start; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: 44px; +} + +:host { + --p-tree-background: transparent !important; + --p-tree-node-hover-color: #ef4343; + --p-tree-node-selected-background: #ef434355; + --p-tree-node-focus-ring-color: #ef4343; } -:host ::ng-deep sidebar-card .card { - height: 50px; - width: 100px; +::ng-deep .p-tree-node-label { + width: 100%; } diff --git a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.html b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.html index 88005e66..7c26ed8c 100644 --- a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.html +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.html @@ -1,16 +1,37 @@ -