diff --git a/angular-client/.gitignore b/angular-client/.gitignore index 0711527e..5f773374 100644 --- a/angular-client/.gitignore +++ b/angular-client/.gitignore @@ -37,6 +37,9 @@ yarn-error.log testem.log /typings +# Environment variables +**/environment.prod.ts + # System files .DS_Store Thumbs.db diff --git a/angular-client/angular.json b/angular-client/angular.json index 80761011..2e611444 100644 --- a/angular-client/angular.json +++ b/angular-client/angular.json @@ -20,15 +20,15 @@ "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/assets"], "styles": ["@angular/material/prebuilt-themes/deeppurple-amber.css", "src/styles.css"], - "scripts": [] + "scripts": ["node_modules/apexcharts/dist/apexcharts.min.js"] }, "configurations": { "production": { "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "2mb", + "maximumError": "5mb" }, { "type": "anyComponentStyle", diff --git a/angular-client/package-lock.json b/angular-client/package-lock.json index 6e9f06e8..b47b03fb 100644 --- a/angular-client/package-lock.json +++ b/angular-client/package-lock.json @@ -21,6 +21,9 @@ "@angular/platform-browser": "^16.2.0", "@angular/platform-browser-dynamic": "^16.2.0", "@angular/router": "^16.2.0", + "apexcharts": "^3.44.0", + "ng-apexcharts": "^1.8.0", + "primeng": "^16.7.0", "rxjs": "~7.8.0", "socket.io-client": "^4.7.2", "tslib": "^2.3.0", @@ -28,6 +31,7 @@ }, "devDependencies": { "@types/jasmine": "~4.3.0", + "@types/node": "^20.10.4", "jasmine-core": "~4.6.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -3957,9 +3961,12 @@ "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==" }, "node_modules/@types/node": { - "version": "20.7.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.7.2.tgz", - "integrity": "sha512-RcdC3hOBOauLP+r/kRt27NrByYtDjsXyAuSbR87O6xpsvi763WI+5fbSIvYJrXnt9w4RuxhV6eAXfIs7aaf/FQ==" + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/qs": { "version": "6.9.8", @@ -4262,6 +4269,11 @@ "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -4496,6 +4508,20 @@ "node": ">= 8" } }, + "node_modules/apexcharts": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.44.0.tgz", + "integrity": "sha512-u7Xzrbcxc2yWznN78Jh5NMCYVAsWDfBjRl5ea++rVzFAqjU2hLz4RgKIFwYOBDRQtW1e/Qz8azJTqIJ1+Vu9Qg==", + "dependencies": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -8808,6 +8834,20 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/ng-apexcharts": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ng-apexcharts/-/ng-apexcharts-1.8.0.tgz", + "integrity": "sha512-NwJuMLHoLm52LSzM08RXV6oOOTyUYREAV53WHVGs+L2qi8UWbxCz19hX0kk+F/xFLEhhuiLegO3T1v30jLbKSQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0", + "apexcharts": "^3.41.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", @@ -9753,6 +9793,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/primeng": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-16.7.0.tgz", + "integrity": "sha512-WHL+V08jjH2d3u3KiCpiYocV4m6qDd8VSvm40aqt+ovp7KJyx6UAnM8etVdeDj8OoI+yFyEmr+cakcdHhKZNTw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^16.2.0", + "@angular/core": "^16.2.0", + "@angular/forms": "^16.2.0", + "rxjs": "^6.0.0 || ^7.8.1", + "zone.js": "~0.13.0" + } + }, "node_modules/proc-log": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", @@ -11040,6 +11095,89 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -11398,6 +11536,11 @@ "node": "*" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", diff --git a/angular-client/package.json b/angular-client/package.json index 3dc913fa..c912db72 100644 --- a/angular-client/package.json +++ b/angular-client/package.json @@ -5,26 +5,29 @@ "ng": "ng", "start": "ng serve", "build": "ng build", - "start:production": "ng serve --configuration production", + "start:production": "ng serve --configuration production --host 0.0.0.0", "install-dependencies": "npm install", "watch": "ng build --watch --configuration development", "test": "ng test" }, "private": true, "dependencies": { + "@angular-devkit/build-angular": "^16.2.4", "@angular/animations": "^16.2.0", "@angular/cdk": "^16.2.6", + "@angular/cli": "^16.2.4", "@angular/common": "^16.2.0", "@angular/compiler": "^16.2.0", + "@angular/compiler-cli": "^16.2.0", "@angular/core": "^16.2.0", "@angular/forms": "^16.2.0", "@angular/material": "^16.2.6", "@angular/platform-browser": "^16.2.0", "@angular/platform-browser-dynamic": "^16.2.0", "@angular/router": "^16.2.0", - "@angular-devkit/build-angular": "^16.2.4", - "@angular/cli": "^16.2.4", - "@angular/compiler-cli": "^16.2.0", + "apexcharts": "^3.44.0", + "ng-apexcharts": "^1.8.0", + "primeng": "^16.7.0", "rxjs": "~7.8.0", "socket.io-client": "^4.7.2", "tslib": "^2.3.0", @@ -32,6 +35,7 @@ }, "devDependencies": { "@types/jasmine": "~4.3.0", + "@types/node": "^20.10.4", "jasmine-core": "~4.6.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", diff --git a/angular-client/src/api/data.api.ts b/angular-client/src/api/data.api.ts new file mode 100644 index 00000000..3fc267d3 --- /dev/null +++ b/angular-client/src/api/data.api.ts @@ -0,0 +1,5 @@ +import { urls } from './urls'; + +export const getDataByDataTypeName = (dataTypeName: string): Promise => { + return fetch(urls.getDataByDataTypeName(dataTypeName)); +}; diff --git a/angular-client/src/api/node.api.ts b/angular-client/src/api/node.api.ts index 20befc3e..7a12bf6d 100644 --- a/angular-client/src/api/node.api.ts +++ b/angular-client/src/api/node.api.ts @@ -5,5 +5,5 @@ import { urls } from './urls'; * @returns A promise containing the response from the server */ export const getAllNodes = (): Promise => { - return fetch(urls.getAllNodes); + return fetch(urls.getAllNodes()); }; diff --git a/angular-client/src/api/run.api.ts b/angular-client/src/api/run.api.ts new file mode 100644 index 00000000..2b6398d1 --- /dev/null +++ b/angular-client/src/api/run.api.ts @@ -0,0 +1,18 @@ +import { urls } from './urls'; + +/** + * Fetches all runs from the server + * @returns A promise containing the response from the server + */ +export const getAllRuns = (): Promise => { + return fetch(urls.getAllRuns()); +}; + +/** + * Fetches the run with the given id + * @param id The id of the run to request + * @returns The requested run + */ +export const getRunById = (id: number): Promise => { + return fetch(urls.getRunById(id)); +}; diff --git a/angular-client/src/api/urls.ts b/angular-client/src/api/urls.ts index a5452d22..6e338a07 100644 --- a/angular-client/src/api/urls.ts +++ b/angular-client/src/api/urls.ts @@ -1,12 +1,27 @@ -const baseURL = 'http://localhost:8000'; +import { environment } from 'src/environment/environment'; + +const baseURL = environment.url; /* Nodes */ -const getAllNodes = `${baseURL}/nodes`; +const getAllNodes = () => `${baseURL}/nodes`; /* Systems */ -const getAllSystems = `${baseURL}/systems`; +const getAllSystems = () => `${baseURL}/systems`; + +/* Data */ +const getDataByDataTypeName = (dataTypeName: string) => `${baseURL}/data/${dataTypeName}`; + +/* Runs */ +const getRunById = (id: number) => `${baseURL}/runs/${id}`; +const getAllRuns = () => `${baseURL}/runs`; export const urls = { getAllNodes, - getAllSystems + + getAllSystems, + + getDataByDataTypeName, + + getAllRuns, + getRunById }; diff --git a/angular-client/src/app/app-routing.module.ts b/angular-client/src/app/app-routing.module.ts index d7ea4dbc..dcd263e4 100644 --- a/angular-client/src/app/app-routing.module.ts +++ b/angular-client/src/app/app-routing.module.ts @@ -1,7 +1,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import GraphPage from 'src/pages/graph-page/graph-page.component'; +import LandingPage from 'src/pages/landing-page/landing-page.component'; -const routes: Routes = []; +const routes: Routes = [ + { path: 'landing', component: LandingPage }, + { path: 'graph/:realTime/:runId', component: GraphPage }, + { path: '', redirectTo: '/landing', pathMatch: 'full' } +]; @NgModule({ imports: [RouterModule.forRoot(routes)], diff --git a/angular-client/src/app/app.module.ts b/angular-client/src/app/app.module.ts index 04299092..10252859 100644 --- a/angular-client/src/app/app.module.ts +++ b/angular-client/src/app/app.module.ts @@ -10,8 +10,10 @@ import { MatButtonModule } from '@angular/material/button'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatListModule } from '@angular/material/list'; +import { MatDialogModule } from '@angular/material/dialog'; +import { CarouselModule } from 'primeng/carousel'; import LandingPage from 'src/pages/landing-page/landing-page.component'; -import Sidebar from 'src/pages/graph-page/sidebar/sidebar.component'; +import GraphSidebar from 'src/pages/graph-page/graph-sidebar/graph-sidebar.component'; import SidebarCard from 'src/components/sidebar-card/sidebar-card.component'; import AppContext from './context/app-context.component'; import GraphPage from 'src/pages/graph-page/graph-page.component'; @@ -21,20 +23,39 @@ import ErrorPage from 'src/components/error-page/error-page.component'; import Header from 'src/components/header/header.component'; import LandingHeader from 'src/pages/landing-page/landing-header/landing-header'; import GraphHeader from 'src/pages/graph-page/graph-header/graph-header.component'; +import MoreDetails from 'src/components/more-details/more-details.component'; +import { History } from 'src/components/history-button/history.component'; +import { Carousel } from 'src/components/carousel/carousel.component'; +import { ButtonComponent } from 'src/components/argos-button/argos-button.component'; +import GraphInfo from 'src/pages/graph-page/graph-caption/graph-caption.component'; +import { NgApexchartsModule } from 'ng-apexcharts'; +import Graph from 'src/pages/graph-page/graph/graph.component'; +import LandingButtons from 'src/pages/landing-page/landing-buttons/landing-buttons.component'; +import GraphSidebarMobile from 'src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component'; +import GraphSidebarDesktop from 'src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component'; @NgModule({ declarations: [ AppContext, LandingPage, GraphPage, - Sidebar, + GraphSidebar, + GraphSidebarMobile, + GraphSidebarDesktop, SidebarCard, Typography, LoadingPage, ErrorPage, Header, LandingHeader, - GraphHeader + GraphHeader, + MoreDetails, + History, + Carousel, + ButtonComponent, + GraphInfo, + Graph, + LandingButtons ], imports: [ BrowserModule, @@ -47,7 +68,10 @@ import GraphHeader from 'src/pages/graph-page/graph-header/graph-header.componen MatButtonModule, MatToolbarModule, MatSidenavModule, - MatListModule + MatListModule, + MatDialogModule, + CarouselModule, + NgApexchartsModule ], providers: [], bootstrap: [AppContext] diff --git a/angular-client/src/app/context/app-context.component.html b/angular-client/src/app/context/app-context.component.html index bf52d122..67e7bd4c 100644 --- a/angular-client/src/app/context/app-context.component.html +++ b/angular-client/src/app/context/app-context.component.html @@ -1,4 +1 @@ - - - - + diff --git a/angular-client/src/app/context/app-context.component.ts b/angular-client/src/app/context/app-context.component.ts index fd7b8fd2..de0ad45a 100644 --- a/angular-client/src/app/context/app-context.component.ts +++ b/angular-client/src/app/context/app-context.component.ts @@ -1,9 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { io } from 'socket.io-client'; -import APIService from 'src/services/api.service'; -import { SocketService } from 'src/services/socket.service'; +import { environment } from 'src/environment/environment'; +import SocketService from 'src/services/socket.service'; import Storage from 'src/services/storage.service'; -import { DataValue } from 'src/utils/socket.utils'; /** * Container for the entire application, contains the socket service, API serivce, and storage service. @@ -13,13 +12,10 @@ import { DataValue } from 'src/utils/socket.utils'; templateUrl: './app-context.component.html' }) export default class AppContext implements OnInit { - title = 'angular-client'; - serverService = new APIService(); - storageMap = new Map(); - storage = new Storage(this.storageMap); - socket = io('http://localhost:8000'); + socket = io(environment.url); socketService = new SocketService(this.socket); - showLandingPage = false; + + constructor(private storage: Storage) {} ngOnInit(): void { this.socketService.receiveData(this.storage); diff --git a/angular-client/src/components/argos-button/argos-button.component.css b/angular-client/src/components/argos-button/argos-button.component.css new file mode 100644 index 00000000..73c9044f --- /dev/null +++ b/angular-client/src/components/argos-button/argos-button.component.css @@ -0,0 +1,14 @@ +.btn { + background-color: #f04346; + border: none; + color: white; + border-radius: 12px; + padding: 2px 20px; + text-align: center; + text-decoration: none; + font-size: 18px; + cursor: pointer; + + margin-bottom: 16px; + display: inline-block; +} diff --git a/angular-client/src/components/argos-button/argos-button.component.html b/angular-client/src/components/argos-button/argos-button.component.html new file mode 100644 index 00000000..4989f57b --- /dev/null +++ b/angular-client/src/components/argos-button/argos-button.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/angular-client/src/components/argos-button/argos-button.component.ts b/angular-client/src/components/argos-button/argos-button.component.ts new file mode 100644 index 00000000..b23a45af --- /dev/null +++ b/angular-client/src/components/argos-button/argos-button.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; + +/** + * Simple custom button component that does something on click + * Takes label and onClick function as inputs + * Currently has one set button style but can be expanded to have more customizable styles + */ +@Component({ + selector: 'argos-button', + templateUrl: './argos-button.component.html', + styleUrls: ['./argos-button.component.css'] +}) +export class ButtonComponent { + @Input() label!: string; + @Input() onClick!: () => void; +} diff --git a/angular-client/src/components/carousel/carousel.component.css b/angular-client/src/components/carousel/carousel.component.css new file mode 100644 index 00000000..73bdc7fb --- /dev/null +++ b/angular-client/src/components/carousel/carousel.component.css @@ -0,0 +1,66 @@ +/* Style for select run and close dialog buttons */ +.select-button-container { + width: 100%; + text-align: center; + margin-top: 8px; +} + +.run-select-button { + border-radius: 10px; + color: #262626; + background-color: white; + margin-top: 8px; + margin-bottom: 8px; +} + +.close-button { + background-color: #262626; + color: white; + position: absolute; + top: 0; + right: 0; + scale: 75%; +} + +/* Container that holds the carousel */ +.modal-container { + background-color: #262626; +} + +:host ::ng-deep .p-carousel-prev, +:host ::ng-deep .p-carousel-next { + background-color: #262626; + border-color: #262626; + color: white; + margin-bottom: 40px; +} + +.run-card { + display: flex; + flex-direction: column; + justify-content: space-between; + border: 2px solid white; + margin-left: 8px; + margin-right: 8px; +} + +/* should eventually be replaced with Grid component */ + +.row { + display: flex; + background-color: #262626; + color: #ffff; + width: 100%; +} + +.left { + margin-left: 5px; + text-align: left; + margin-right: auto; +} + +.right { + margin-left: auto; + margin-right: 5px; + text-align: right; +} diff --git a/angular-client/src/components/carousel/carousel.component.html b/angular-client/src/components/carousel/carousel.component.html new file mode 100644 index 00000000..b5050495 --- /dev/null +++ b/angular-client/src/components/carousel/carousel.component.html @@ -0,0 +1,34 @@ + diff --git a/angular-client/src/components/carousel/carousel.component.ts b/angular-client/src/components/carousel/carousel.component.ts new file mode 100644 index 00000000..e7a0c5dd --- /dev/null +++ b/angular-client/src/components/carousel/carousel.component.ts @@ -0,0 +1,42 @@ +import { Component, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { Run } from 'src/utils/types.utils'; + +// the data for the dialog, basically just all the runs +export interface DialogData { + runs: Run[]; +} + +@Component({ + selector: 'carousel', + templateUrl: 'carousel.component.html', + styleUrls: ['carousel.component.css'] +}) +export class Carousel { + runs: Run[]; + + responsiveOptions: any[] | undefined; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData, + public router: Router + ) { + this.runs = data.runs; + } + + onNoClick(): void { + this.dialogRef.close(); + } + + datePipe = (time: string) => { + const date = new Date(parseInt(time)); + return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()} - ${date.getHours()}:${date.getMinutes()}`; + }; + + selectRun = (run: Run) => { + this.router.navigate([`graph/false/${run.id}`]); + this.dialogRef.close(); + }; +} diff --git a/angular-client/src/components/history-button/history.component.html b/angular-client/src/components/history-button/history.component.html new file mode 100644 index 00000000..7e014c4b --- /dev/null +++ b/angular-client/src/components/history-button/history.component.html @@ -0,0 +1 @@ + diff --git a/angular-client/src/components/history-button/history.component.ts b/angular-client/src/components/history-button/history.component.ts new file mode 100644 index 00000000..0b87597d --- /dev/null +++ b/angular-client/src/components/history-button/history.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Run } from 'src/utils/types.utils'; +import { Carousel } from '../carousel/carousel.component'; +import { getAllRuns } from 'src/api/run.api'; +import APIService from 'src/services/api.service'; + +@Component({ + selector: 'history', + templateUrl: './history.component.html' +}) +export class History implements OnInit { + label!: string; + runs!: Run[]; + runsError?: Error; + runsIsLoading = true; + runsIsError = false; + + constructor( + public dialog: MatDialog, + private serverService: APIService + ) {} + + ngOnInit() { + const runsQueryResponse = this.serverService.query(() => getAllRuns()); + runsQueryResponse.isLoading.subscribe((isLoading: boolean) => { + this.runsIsLoading = isLoading; + }); + runsQueryResponse.error.subscribe((error: Error) => { + this.runsIsError = true; + this.runsError = error; + }); + runsQueryResponse.data.subscribe((data: Run[]) => { + this.runs = data; + }); + + this.label = 'Historical'; + } + + openDialog = () => { + this.dialog.open(Carousel, { + width: '550px', + data: { runs: this.runs }, + hasBackdrop: true, + backdropClass: 'dialog-background' + }); + }; +} diff --git a/angular-client/src/components/more-details/more-details.component.html b/angular-client/src/components/more-details/more-details.component.html new file mode 100644 index 00000000..f5e85d94 --- /dev/null +++ b/angular-client/src/components/more-details/more-details.component.html @@ -0,0 +1 @@ + diff --git a/angular-client/src/components/more-details/more-details.component.ts b/angular-client/src/components/more-details/more-details.component.ts new file mode 100644 index 00000000..4cd507d0 --- /dev/null +++ b/angular-client/src/components/more-details/more-details.component.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import Storage from 'src/services/storage.service'; + +@Component({ + selector: 'more-details', + templateUrl: './more-details.component.html' +}) +export default class MoreDetails { + label: string; + runId = this.storage.getCurrentRunId(); + + constructor( + private router: Router, + private storage: Storage + ) { + this.label = 'More Details'; + } + + goToGraph = () => { + this.router.navigate([`graph/true/${this.runId}`]); + }; +} diff --git a/angular-client/src/components/typography/typography.component.ts b/angular-client/src/components/typography/typography.component.ts index 426e9643..975a3092 100644 --- a/angular-client/src/components/typography/typography.component.ts +++ b/angular-client/src/components/typography/typography.component.ts @@ -18,7 +18,7 @@ import { StyleVariant } from 'src/utils/enumerations/StyleVariant'; export default class Typography implements OnInit { @Input() variant!: StyleVariant; @Input() content?: string | null; - @Input() additionalStyles?: string | null; + @Input() additionalStyles?: string; style!: string; ngOnInit(): void { diff --git a/angular-client/src/environment/environment.ts b/angular-client/src/environment/environment.ts new file mode 100644 index 00000000..b8bcf564 --- /dev/null +++ b/angular-client/src/environment/environment.ts @@ -0,0 +1,4 @@ +export const environment = { + production: false, + url: 'http://localhost:8000' +}; diff --git a/angular-client/src/pages/graph-page/graph-caption/graph-caption.component.css b/angular-client/src/pages/graph-page/graph-caption/graph-caption.component.css new file mode 100644 index 00000000..4b603d95 --- /dev/null +++ b/angular-client/src/pages/graph-page/graph-caption/graph-caption.component.css @@ -0,0 +1,21 @@ +.info-container { + font: small-caption; + width: 100%; + bottom: 0; + background-color: #323232; + border-radius: 10px; + display: flex; + flex-direction: row; + justify-content: space-between; + color: #ffffff; + padding-right: 8px; + padding-left: 8px; +} + +.left-info { + text-align: left; +} + +.right-info { + text-align: right; +} diff --git a/angular-client/src/pages/graph-page/graph-caption/graph-caption.component.html b/angular-client/src/pages/graph-page/graph-caption/graph-caption.component.html new file mode 100644 index 00000000..60f46b25 --- /dev/null +++ b/angular-client/src/pages/graph-page/graph-caption/graph-caption.component.html @@ -0,0 +1,20 @@ +
+
+
+ + +
+ + + +
+
+
+ +
+
+ + +
+
+
diff --git a/angular-client/src/pages/graph-page/graph-caption/graph-caption.component.ts b/angular-client/src/pages/graph-page/graph-caption/graph-caption.component.ts new file mode 100644 index 00000000..972d5a1e --- /dev/null +++ b/angular-client/src/pages/graph-page/graph-caption/graph-caption.component.ts @@ -0,0 +1,30 @@ +import { Component, Input } from '@angular/core'; +import { Subject } from 'rxjs'; +import { DataValue } from 'src/utils/socket.utils'; +import { DataType } from 'src/utils/types.utils'; + +@Component({ + selector: 'graph-caption', + styleUrls: ['./graph-caption.component.css'], + templateUrl: './graph-caption.component.html' +}) +export default class GraphInfo { + @Input() dataType!: Subject; + @Input() currentValue!: Subject; + @Input() currentDriver?: string; + @Input() currentSystem?: string; + @Input() currentLocation?: string; + dataTypeName?: string; + dataTypeUnit?: string; + value?: string | number; + + ngOnInit(): void { + this.dataType.subscribe((dataType: DataType) => { + this.dataTypeName = dataType.name; + this.dataTypeUnit = dataType.unit; + }); + this.currentValue.subscribe((value?: DataValue) => { + this.value = value?.value ?? 'No Values'; + }); + } +} diff --git a/angular-client/src/pages/graph-page/graph-header/graph-header.component.css b/angular-client/src/pages/graph-page/graph-header/graph-header.component.css index d6fed303..5a1871d3 100644 --- a/angular-client/src/pages/graph-page/graph-header/graph-header.component.css +++ b/angular-client/src/pages/graph-page/graph-header/graph-header.component.css @@ -1,3 +1,13 @@ .header-left { margin-left: 50px; } + +a { + text-decoration: none; +} + +@media screen and (max-width: 768px) { + .header-left { + margin-left: 0px; + } +} diff --git a/angular-client/src/pages/graph-page/graph-header/graph-header.component.html b/angular-client/src/pages/graph-page/graph-header/graph-header.component.html index 286f9720..e9b3f456 100644 --- a/angular-client/src/pages/graph-page/graph-header/graph-header.component.html +++ b/angular-client/src/pages/graph-page/graph-header/graph-header.component.html @@ -1,8 +1,10 @@
- + + +
- +
diff --git a/angular-client/src/pages/graph-page/graph-header/graph-header.component.ts b/angular-client/src/pages/graph-page/graph-header/graph-header.component.ts index 859666e6..42f35746 100644 --- a/angular-client/src/pages/graph-page/graph-header/graph-header.component.ts +++ b/angular-client/src/pages/graph-page/graph-header/graph-header.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, Input } from '@angular/core'; /** * Graph Header Component to display the graph page header. @@ -9,4 +9,7 @@ import { Component } from '@angular/core'; templateUrl: './graph-header.component.html', styleUrls: ['./graph-header.component.css'] }) -export default class GraphHeader {} +export default class GraphHeader { + @Input() realTime?: boolean; + @Input() runId?: number; +} 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 8605d266..3eb3ffb1 100644 --- a/angular-client/src/pages/graph-page/graph-page.component.css +++ b/angular-client/src/pages/graph-page/graph-page.component.css @@ -1,3 +1,71 @@ :host { max-height: inherit; } + +.container { + display: flex; + flex-flow: column; + height: 100%; +} + +.graph-header { + flex: 0 1 auto; +} + +.content-div { + flex: 1 1 auto; + display: flex; + flex-flow: row; + height: 100%; + width: 100%; +} + +.desktop-sidebar { + flex: 0 1 auto; +} + +.right-container { + flex: 1 1 auto; + display: flex; + flex-flow: column; + height: 100%; + overflow: hidden; + padding-right: 16px; +} + +.mobile-sidebar { + display: none; +} + +.graph { + flex: 1 1 auto; +} + +.graph-caption { + flex: 0 0 100px; + margin-bottom: 8px; + margin-right: 8px; + margin-left: 8px; +} + +@media (max-width: 768px) { + .content-div { + flex-flow: column; + } + + .desktop-sidebar { + display: none; + } + + .mobile-sidebar { + display: block; + } + + .right-container { + flex: 1 1 auto; + } + + .graph-caption { + flex: 0 0 auto; + } +} 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 2bd184a8..6aad3d19 100644 --- a/angular-client/src/pages/graph-page/graph-page.component.html +++ b/angular-client/src/pages/graph-page/graph-page.component.html @@ -1,12 +1,33 @@ -
- +
+
- -
+ +
+ +
+ +
+
- - +
+ +
+ +
+ + +
+ +
+
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 93d3ea92..236532eb 100644 --- a/angular-client/src/pages/graph-page/graph-page.component.ts +++ b/angular-client/src/pages/graph-page/graph-page.component.ts @@ -1,8 +1,13 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { getDataByDataTypeName } from 'src/api/data.api'; import { getAllNodes } from 'src/api/node.api'; +import { getRunById } from 'src/api/run.api'; import APIService from 'src/services/api.service'; import Storage from 'src/services/storage.service'; -import { Node } from 'src/utils/types.utils'; +import { DataValue } from 'src/utils/socket.utils'; +import { DataType, Node, Run } from 'src/utils/types.utils'; @Component({ selector: 'graph-page', @@ -10,14 +15,74 @@ import { Node } from 'src/utils/types.utils'; styleUrls: ['./graph-page.component.css'] }) export default class GraphPage implements OnInit { - @Input() serverService!: APIService; - @Input() storage!: Storage; + paramsError?: Error; + runId?: number; + realTime!: boolean; + nodes?: Node[]; nodesIsLoading = true; nodesIsError = false; nodesError?: Error; + run?: Run; + runIsLoading = true; + + selectedDataType: Subject = new Subject(); + selectedDataTypeValuesSubject: BehaviorSubject = new BehaviorSubject([]); + currentValue: Subject = new Subject(); + selectedDataTypeValuesIsLoading = false; + selectedDataTypeValuesIsError = false; + selectedDataTypeValuesError?: Error; + + constructor( + private serverService: APIService, + private storage: Storage, + private route: ActivatedRoute + ) {} + ngOnInit(): void { + this.parseParams(); + this.queryNodes(); + this.queryRuns(); + + this.setSelectedDataType = (dataType: DataType) => { + this.selectedDataType.next(dataType); + if (this.realTime) { + const key = JSON.stringify({ + name: dataType.name, + unit: dataType.unit + }); + const valuesSubject = this.storage.get(key); + if (valuesSubject) { + this.selectedDataTypeValuesSubject.next(valuesSubject.getValue()); + } else { + this.storage.set(key, this.selectedDataTypeValuesSubject); + } + } else { + this.selectedDataTypeValuesIsLoading = true; + this.selectedDataTypeValuesIsError = false; + this.selectedDataTypeValuesError = undefined; + + const dataQueryResponse = this.serverService.query(() => getDataByDataTypeName(dataType.name)); + dataQueryResponse.isLoading.subscribe((isLoading: boolean) => { + this.selectedDataTypeValuesIsLoading = isLoading; + }); + dataQueryResponse.error.subscribe((error: Error) => { + this.selectedDataTypeValuesIsError = true; + this.selectedDataTypeValuesError = error; + }); + dataQueryResponse.data.subscribe((data: DataValue[]) => { + this.selectedDataTypeValuesSubject.next(data); + this.currentValue.next(data.pop()); + }); + } + }; + } + + /** + * Queries the nodes from the server. + */ + private queryNodes() { const nodeQueryResponse = this.serverService.query(getAllNodes); nodeQueryResponse.isLoading.subscribe((isLoading: boolean) => { this.nodesIsLoading = isLoading; @@ -30,4 +95,52 @@ export default class GraphPage implements OnInit { this.nodes = data; }); } + + /** + * Queries the runs from the server. + */ + private queryRuns() { + if (!this.runId) { + return; + } + const runQueryResponse = this.serverService.query(() => getRunById(this.runId!)); + + runQueryResponse.isLoading.subscribe((isLoading: boolean) => { + this.runIsLoading = isLoading; + }); + + runQueryResponse.data.subscribe((run) => { + this.run = run; + }); + } + + private parseParams() { + const realtTime = this.route.snapshot.paramMap.get('realTime'); + if (realtTime) this.realTime = realtTime === 'true'; + else { + this.paramsError = new Error('No real time value provided'); + return; + } + const runId = this.route.snapshot.paramMap.get('runId'); + if (runId) { + if (runId === 'undefined' && this.realTime) { + this.paramsError = new Error('No Real Time Data Available'); + return; + } + this.runId = parseInt(runId); + if (isNaN(this.runId)) { + this.paramsError = new Error('Run Id must be a number'); + return; + } + } else { + this.paramsError = new Error('No run id provided'); + return; + } + } + + /** + * Sets the selected data type. + * @param dataType The data type to set. + */ + setSelectedDataType!: (dataType: DataType) => void; } diff --git a/angular-client/src/pages/graph-page/sidebar/sidebar.component.css b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.css similarity index 56% rename from angular-client/src/pages/graph-page/sidebar/sidebar.component.css rename to angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.css index 53d66732..3c53d9c3 100644 --- a/angular-client/src/pages/graph-page/sidebar/sidebar.component.css +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.css @@ -1,8 +1,6 @@ .background { width: 200px; margin-left: 16px; - margin-top: 0px; - margin-bottom: 0px; - height: 60%; + height: 80%; overflow: scroll; } diff --git a/angular-client/src/pages/graph-page/sidebar/sidebar.component.html b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.html similarity index 84% rename from angular-client/src/pages/graph-page/sidebar/sidebar.component.html rename to angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.html index 99026f42..b98f62a3 100644 --- a/angular-client/src/pages/graph-page/sidebar/sidebar.component.html +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.html @@ -1,10 +1,10 @@
- + - + diff --git a/angular-client/src/pages/graph-page/sidebar/sidebar.component.ts b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.ts similarity index 81% rename from angular-client/src/pages/graph-page/sidebar/sidebar.component.ts rename to angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.ts index f9d9d928..3e698b03 100644 --- a/angular-client/src/pages/graph-page/sidebar/sidebar.component.ts +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-desktop/graph-sidebar-desktop.component.ts @@ -1,6 +1,6 @@ import { animate, style, transition, trigger } from '@angular/animations'; import { Component, Input, OnInit } from '@angular/core'; -import { Node, NodeWithVisibilityToggle } from 'src/utils/types.utils'; +import { DataType, Node, NodeWithVisibilityToggle } from 'src/utils/types.utils'; /** * Sidebar component that displays the nodes and their data types. @@ -9,9 +9,9 @@ import { Node, NodeWithVisibilityToggle } from 'src/utils/types.utils'; * */ @Component({ - selector: 'sidebar', - templateUrl: './sidebar.component.html', - styleUrls: ['./sidebar.component.css'], + selector: 'graph-sidebar-desktop', + templateUrl: './graph-sidebar-desktop.component.html', + styleUrls: ['./graph-sidebar-desktop.component.css'], animations: [ trigger('toggleExpand', [ transition(':enter', [ @@ -42,9 +42,10 @@ import { Node, NodeWithVisibilityToggle } from 'src/utils/types.utils'; ]) ] }) -export default class Sidebar implements OnInit { +export default class GraphSidebarDesktop implements OnInit { @Input() nodes!: Node[]; nodesWithVisibilityToggle!: NodeWithVisibilityToggle[]; + @Input() selectDataType!: (dataType: DataType) => void; /** * Initializes the nodes with the visibility toggle. 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 new file mode 100644 index 00000000..9d0b71c1 --- /dev/null +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.css @@ -0,0 +1,30 @@ +.container { + display: flex; + flex-flow: row; + justify-content: flex-start; + width: fit-content; +} + +.subcontainer { + display: flex; + flex-flow: row; + width: fit-content; + justify-content: flex-start; +} + +.popup-tab { + height: fit-content; + bottom: 0px; + background-color: #1b1b1b; + position: fixed; + padding: 8px; + overflow: scroll; + max-width: 95%; + border-top-right-radius: 8px; + margin: 0px 0px 0px 8px; +} + +:host ::ng-deep sidebar-card .card { + height: 50px; + width: 100px; +} 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 new file mode 100644 index 00000000..f0f195bc --- /dev/null +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.html @@ -0,0 +1,25 @@ + diff --git a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.ts b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.ts new file mode 100644 index 00000000..101c35cd --- /dev/null +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar-mobile/graph-sidebar-mobile.component.ts @@ -0,0 +1,92 @@ +import { animate, style, transition, trigger } from '@angular/animations'; +import { Component, Input } from '@angular/core'; +import { DataType, Node, NodeWithVisibilityToggle } from 'src/utils/types.utils'; + +@Component({ + selector: 'graph-sidebar-mobile', + templateUrl: './graph-sidebar-mobile.component.html', + styleUrls: ['./graph-sidebar-mobile.component.css'], + animations: [ + trigger('toggleCardExpand', [ + transition(':enter', [ + style({ + width: 0, + opacity: 0 + }), + animate( + '400ms', + style({ + width: '*', + opacity: 1 + }) + ) + ]), + transition(':leave', [ + animate( + '400ms', + style({ + width: 0, + opacity: 0 + }) + ) + ]) + ]), + trigger('toggleSidebar', [ + transition(':enter', [ + style({ + height: 0, + opacity: 0 + }), + animate( + '400ms', + style({ + height: '*', + opacity: 1 + }) + ) + ]), + transition(':leave', [ + animate( + '400ms', + style({ + height: 0, + opacity: 0 + }) + ) + ]) + ]) + ] +}) +export default class GraphSidebarMobile { + @Input() nodes!: Node[]; + @Input() selectDataType!: (dataType: DataType) => void; + nodesWithVisibilityToggle!: NodeWithVisibilityToggle[]; + showSelection = false; + + /** + * Initializes the nodes with the visibility toggle. + */ + ngOnInit(): void { + this.nodesWithVisibilityToggle = this.nodes.map((node: Node) => { + return { + ...node, + dataTypesAreVisible: false + }; + }); + } + + /** + * Toggles Visibility whenever a node is selected + * @param node The node to toggle the visibility of the data types for. + */ + toggleDataTypeVisibility = (node: NodeWithVisibilityToggle) => { + node.dataTypesAreVisible = !node.dataTypesAreVisible; + }; + + /** + * Toggles the sidebar. + */ + toggleSidebar = () => { + this.showSelection = !this.showSelection; + }; +} diff --git a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar.component.css b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar.component.css new file mode 100644 index 00000000..e69de29b diff --git a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar.component.html b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar.component.html new file mode 100644 index 00000000..6dd57e58 --- /dev/null +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar.component.html @@ -0,0 +1,6 @@ +
+ +
+ + + diff --git a/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar.component.ts b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar.component.ts new file mode 100644 index 00000000..42849981 --- /dev/null +++ b/angular-client/src/pages/graph-page/graph-sidebar/graph-sidebar.component.ts @@ -0,0 +1,30 @@ +import { Component, HostListener, Input, OnInit } from '@angular/core'; +import { DataType, Node } from 'src/utils/types.utils'; + +/** + * Sidebar component wrapper that determines to display mobile or desktop sidebar. + * @param nodes The nodes to display on the sidebar. + * @param selectDataType The function to call when a data type is selected. + */ +@Component({ + selector: 'graph-sidebar', + templateUrl: './graph-sidebar.component.html', + styleUrls: ['./graph-sidebar.component.css'] +}) +export default class GraphSidebar implements OnInit { + @Input() nodes!: Node[]; + @Input() selectDataType!: (dataType: DataType) => void; + + isMobile!: boolean; + + mobileThreshold = 768; + + ngOnInit() { + this.isMobile = window.innerWidth <= this.mobileThreshold; + } + + @HostListener('window:resize', ['$event']) + onResize() { + this.isMobile = window.innerWidth <= this.mobileThreshold; + } +} diff --git a/angular-client/src/pages/graph-page/graph/graph.component.css b/angular-client/src/pages/graph-page/graph/graph.component.css new file mode 100644 index 00000000..9eb48353 --- /dev/null +++ b/angular-client/src/pages/graph-page/graph/graph.component.css @@ -0,0 +1,4 @@ +#chart-container { + width: 100%; + height: 100%; +} diff --git a/angular-client/src/pages/graph-page/graph/graph.component.html b/angular-client/src/pages/graph-page/graph/graph.component.html new file mode 100644 index 00000000..74e5b137 --- /dev/null +++ b/angular-client/src/pages/graph-page/graph/graph.component.html @@ -0,0 +1 @@ +
diff --git a/angular-client/src/pages/graph-page/graph/graph.component.ts b/angular-client/src/pages/graph-page/graph/graph.component.ts new file mode 100644 index 00000000..9ef6717c --- /dev/null +++ b/angular-client/src/pages/graph-page/graph/graph.component.ts @@ -0,0 +1,108 @@ +import { Component, Input, OnInit } from '@angular/core'; +import * as ApexCharts from 'apexcharts'; +import { + ApexAxisChartSeries, + ApexXAxis, + ApexDataLabels, + ApexChart, + ApexMarkers, + ApexGrid, + ApexTooltip, + ApexFill +} from 'ng-apexcharts'; +import { BehaviorSubject } from 'rxjs'; +import { DataValue } from 'src/utils/socket.utils'; + +type ChartOptions = { + series: ApexAxisChartSeries; + chart: ApexChart; + xaxis: ApexXAxis; + dataLabels: ApexDataLabels; + markers: ApexMarkers; + grid: ApexGrid; + tooltip: ApexTooltip; + fill: ApexFill; +}; + +@Component({ + selector: 'graph', + templateUrl: './graph.component.html', + styleUrls: ['./graph.component.css'] +}) +export default class Graph implements OnInit { + @Input() valuesSubject!: BehaviorSubject; + options!: ChartOptions; + chart!: ApexCharts; + + updateChart(values: DataValue[]) { + const mappedValues = values.map((value: DataValue) => [+value.time, +value.value]); + + const newSeries = [ + { + name: 'My-series', + data: mappedValues + } + ]; + + this.chart.updateSeries(newSeries); + } + + ngOnInit(): void { + this.valuesSubject.subscribe((values: DataValue[]) => { + this.updateChart(values); + }); + + const chartContainer = document.getElementById('chart-container'); + if (!chartContainer) { + console.log('Something went very wrong'); + return; + } + + this.options = { + series: [], + chart: { + id: 'graph', + type: 'area', + height: '100%', + zoom: { + autoScaleYaxis: true + } + }, + dataLabels: { + enabled: false + }, + markers: { + size: 0 + }, + xaxis: { + type: 'datetime', + tickAmount: 6 + }, + tooltip: { + x: { + //format by hours and minutes and seconds + format: 'M/d/yy, h:mm:ss' + } + }, + fill: { + type: 'gradient', + gradient: { + shadeIntensity: 1, + opacityFrom: 0.7, + opacityTo: 0.9, + stops: [0, 100] + } + }, + grid: { + show: false + } + }; + + //Weird rendering stuff with apex charts, view link to see why https://github.com/apexcharts/react-apexcharts/issues/187 + setTimeout(() => { + this.chart = new ApexCharts(chartContainer, this.options); + + this.chart.render(); + }, 0); + } +} diff --git a/angular-client/src/pages/landing-page/landing-buttons/landing-buttons.component.css b/angular-client/src/pages/landing-page/landing-buttons/landing-buttons.component.css new file mode 100644 index 00000000..922ce585 --- /dev/null +++ b/angular-client/src/pages/landing-page/landing-buttons/landing-buttons.component.css @@ -0,0 +1,7 @@ +.button-container { + display: flex; + flex-direction: row; + gap: 10px; + margin-top: 10px; + margin-left: 10px; +} diff --git a/angular-client/src/pages/landing-page/landing-buttons/landing-buttons.component.html b/angular-client/src/pages/landing-page/landing-buttons/landing-buttons.component.html new file mode 100644 index 00000000..4180c90c --- /dev/null +++ b/angular-client/src/pages/landing-page/landing-buttons/landing-buttons.component.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/angular-client/src/pages/landing-page/landing-buttons/landing-buttons.component.ts b/angular-client/src/pages/landing-page/landing-buttons/landing-buttons.component.ts new file mode 100644 index 00000000..67d0362e --- /dev/null +++ b/angular-client/src/pages/landing-page/landing-buttons/landing-buttons.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'landing-buttons', + templateUrl: './landing-buttons.component.html', + styleUrls: ['./landing-buttons.component.css'] +}) +export default class LandingButtons {} diff --git a/angular-client/src/pages/landing-page/landing-page.component.html b/angular-client/src/pages/landing-page/landing-page.component.html index 79cad371..e29a5893 100644 --- a/angular-client/src/pages/landing-page/landing-page.component.html +++ b/angular-client/src/pages/landing-page/landing-page.component.html @@ -1,3 +1,4 @@
+
diff --git a/angular-client/src/pages/landing-page/landing-page.component.ts b/angular-client/src/pages/landing-page/landing-page.component.ts index 519b6e06..e83ba7a3 100644 --- a/angular-client/src/pages/landing-page/landing-page.component.ts +++ b/angular-client/src/pages/landing-page/landing-page.component.ts @@ -1,10 +1,9 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import Storage from 'src/services/storage.service'; import { IdentifierDataType } from 'src/utils/enumerations/ImportantDataType'; /** * Container for the landing page, obtains data from the storage service. - * @param storage - The storage service to obtain data from. */ @Component({ selector: 'landing-page', @@ -12,14 +11,18 @@ import { IdentifierDataType } from 'src/utils/enumerations/ImportantDataType'; templateUrl: './landing-page.component.html' }) export default class LandingPage implements OnInit { - @Input() storage!: Storage; currentDriver!: string; currentLocation!: string; currentSystem!: string; + constructor(private storage: Storage) {} + ngOnInit() { - this.currentDriver = (this.storage.get(IdentifierDataType.DRIVER)?.[0].value as string) ?? 'No Driver Selected'; - this.currentLocation = (this.storage.get(IdentifierDataType.LOCATION)?.[0].value as string) ?? 'No Location Selected'; - this.currentSystem = (this.storage.get(IdentifierDataType.SYSTEM)?.[0].value as string) ?? 'No System Selected'; + this.currentDriver = + (this.storage.get(IdentifierDataType.DRIVER)?.getValue()[0].value as string) ?? 'No Driver Selected'; + this.currentLocation = + (this.storage.get(IdentifierDataType.LOCATION)?.getValue()[0].value as string) ?? 'No Location Selected'; + this.currentSystem = + (this.storage.get(IdentifierDataType.SYSTEM)?.getValue()[0].value as string) ?? 'No System Selected'; } } diff --git a/angular-client/src/services/api.service.ts b/angular-client/src/services/api.service.ts index c10b7536..531f72a7 100644 --- a/angular-client/src/services/api.service.ts +++ b/angular-client/src/services/api.service.ts @@ -1,6 +1,11 @@ +import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; import { QueryResponse } from 'src/utils/api.utils'; +/** + * Service for interacting with the api + */ +@Injectable({ providedIn: 'root' }) export default class APIService { /** * Function to query data from the api @@ -14,13 +19,20 @@ export default class APIService { const error = new Subject(); isLoading.next(true); isError.next(false); - apiCall() - .then((response) => this.handleErrors(response, error, isError)) - .then((response) => response.json() as Promise) - .then((resolvedData) => { - data.next(resolvedData); - isLoading.next(false); - }); + try { + apiCall() + .then((response) => this.handleErrors(response, error, isError)) + .then((response) => response.json() as Promise) + .then((resolvedData) => { + data.next(resolvedData); + isLoading.next(false); + }); + } catch (err) { + if (err instanceof Error) { + isError.next(true); + error.next(err); + } + } return { data, isLoading, diff --git a/angular-client/src/services/socket.service.ts b/angular-client/src/services/socket.service.ts index bcf29a08..2ee3a051 100644 --- a/angular-client/src/services/socket.service.ts +++ b/angular-client/src/services/socket.service.ts @@ -1,11 +1,12 @@ import { Socket } from 'socket.io-client'; -import { ServerData } from 'src/utils/socket.utils'; +import { DataValue, ServerData } from 'src/utils/socket.utils'; import Storage from './storage.service'; +import { BehaviorSubject } from 'rxjs'; /** * Service for interacting with the socket */ -export class SocketService { +export default class SocketService { private socket: Socket; /** @@ -27,9 +28,16 @@ export class SocketService { name: data.name, unit: data.unit }); - const value = storage.get(key); - const newValue = { value: data.value, timestamp: data.timestamp }; - value ? storage.set(key, value.concat(newValue)) : storage.set(key, [newValue]); + const valuesSubject = storage.get(key); + const newValue = { value: data.value, time: data.timestamp }; + if (valuesSubject) { + const value = valuesSubject.getValue(); + value.push(newValue); + valuesSubject.next(value); + } else { + const newValuesSubject = new BehaviorSubject([newValue]); + storage.set(key, newValuesSubject); + } } catch (error) { if (error instanceof Error) this.sendError(error.message); } diff --git a/angular-client/src/services/storage.service.ts b/angular-client/src/services/storage.service.ts index 597f65f1..4d9372cb 100644 --- a/angular-client/src/services/storage.service.ts +++ b/angular-client/src/services/storage.service.ts @@ -1,20 +1,32 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; import { DataValue, StorageMap } from 'src/utils/socket.utils'; /** * Service for interacting with the storage */ +@Injectable({ providedIn: 'root' }) export default class Storage { private storage: StorageMap; + private currentRunId?: number; - constructor(storage: StorageMap) { - this.storage = storage; + constructor() { + this.storage = new Map>(); } - public get(key: string): DataValue[] | undefined { + public get(key: string): BehaviorSubject | undefined { return this.storage.get(key); } - public set(key: string, value: any): void { + public set(key: string, value: BehaviorSubject): void { this.storage.set(key, value); } + + public getCurrentRunId(): number | undefined { + return this.currentRunId; + } + + public setCurrentRunId(runId: number) { + this.currentRunId = runId; + } } diff --git a/angular-client/src/services/theme.service.ts b/angular-client/src/services/theme.service.ts index 9de6bb42..cd586719 100644 --- a/angular-client/src/services/theme.service.ts +++ b/angular-client/src/services/theme.service.ts @@ -9,6 +9,6 @@ export default class Theme { static readonly boldedText: string = this.textStyle + 'font-weight: bold; '; static readonly HEADER: string = this.boldedText + 'fontSize: 20px;'; static readonly SUBHEADER: string = this.textStyle + 'fontSize: 16px;'; - static readonly XXLARGEHEADER: string = this.boldedText + 'font-size: 7.5rem; padding-bottom: 60px;'; + static readonly XXLARGEHEADER: string = this.boldedText + 'font-size: 7.5rem; margin: 0; padding: 0;'; static readonly LARGEHEADER: string = this.boldedText + 'font-size: xx-large;'; } diff --git a/angular-client/src/utils/socket.utils.ts b/angular-client/src/utils/socket.utils.ts index 410d049c..5a004836 100644 --- a/angular-client/src/utils/socket.utils.ts +++ b/angular-client/src/utils/socket.utils.ts @@ -1,20 +1,23 @@ +import { BehaviorSubject } from 'rxjs'; + /** * The storage system for the data received from the server */ -export type StorageMap = Map; +export type StorageMap = Map>; /** * The value of a data point */ export type DataValue = { value: string | number; - timestamp: number; + time: number; }; /** * The format of a message sent from the server */ export type ServerData = { + runId: string; name: string; unit: string; value: number; diff --git a/angular-client/src/utils/types.utils.ts b/angular-client/src/utils/types.utils.ts index ffe25a50..3459d65c 100644 --- a/angular-client/src/utils/types.utils.ts +++ b/angular-client/src/utils/types.utils.ts @@ -20,3 +20,14 @@ export type DataType = { name: string; unit: string; }; + +/** + * Frontend type of a Run + */ +export type Run = { + id: number; + locationName: string; + driverName: string; + systemName: string; + time: number; +}; diff --git a/angular-client/tsconfig.app.json b/angular-client/tsconfig.app.json index 374cc9d2..9161a71d 100644 --- a/angular-client/tsconfig.app.json +++ b/angular-client/tsconfig.app.json @@ -3,7 +3,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", - "types": [] + "types": ["node"] }, "files": [ "src/main.ts" diff --git a/docker-compose.yml b/docker-compose.yml index 21d2c713..1db8bee8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: scylla-server: container_name: scylla-server restart: unless-stopped - image: scylla-server-prod:1.0.0 + image: mckeep/scylla-server-prod:1.0.0 build: context: scylla-server target: production @@ -17,13 +17,13 @@ services: client: container_name: client restart: unless-stopped - image: client-prod:1.0.0 + image: mckeep/client-prod:1.0.0 build: context: angular-client target: production dockerfile: Dockerfile ports: - - 3000:3000 + - 4200:4200 networks: - shared-network depends_on: diff --git a/scylla-server/.gitignore b/scylla-server/.gitignore index b3b5402a..c3020db5 100644 --- a/scylla-server/.gitignore +++ b/scylla-server/.gitignore @@ -2,4 +2,5 @@ /node_modules # production -/dist \ No newline at end of file +/dist + diff --git a/scylla-server/src/controllers/data.controller.ts b/scylla-server/src/controllers/data.controller.ts index 4f26d551..9ecbf4bb 100644 --- a/scylla-server/src/controllers/data.controller.ts +++ b/scylla-server/src/controllers/data.controller.ts @@ -7,7 +7,8 @@ import DataService from '../odyssey-base/src/services/data.services'; export default class DataController { static async getDataByDataTypeName(req: Request, res: Response, next: NextFunction) { try { - const dataByDataTypeName = await DataService.getDataByDataTypeName(req.body.dataTypeName); + const { dataTypeName } = req.params; + const dataByDataTypeName = await DataService.getDataByDataTypeName(dataTypeName); res.status(200).json(dataByDataTypeName); } catch (error: unknown) { next(error); diff --git a/scylla-server/src/controllers/node.controller.ts b/scylla-server/src/controllers/node.controller.ts index 1d4a2312..15c969ad 100644 --- a/scylla-server/src/controllers/node.controller.ts +++ b/scylla-server/src/controllers/node.controller.ts @@ -8,7 +8,6 @@ export default class NodeController { static async getAllNodes(req: Request, res: Response, next: NextFunction) { try { const allNodes = await NodeService.getAllNodes(); - console.log('allNodes: ', allNodes); res.status(200).json(allNodes); } catch (error: unknown) { next(error); diff --git a/scylla-server/src/controllers/run.controller.ts b/scylla-server/src/controllers/run.controller.ts index 69cdfe7e..b92f4d9b 100644 --- a/scylla-server/src/controllers/run.controller.ts +++ b/scylla-server/src/controllers/run.controller.ts @@ -5,7 +5,7 @@ import RunService from '../odyssey-base/src/services/runs.services'; * Controller to manage Run requests and responses */ export default class RunController { - static async getAllRuns(req: Request, res: Response, next: NextFunction) { + static async getAllRuns(_req: Request, res: Response, next: NextFunction) { try { const allRuns = await RunService.getAllRuns(); res.status(200).json(allRuns); @@ -13,4 +13,14 @@ export default class RunController { next(error); } } + + static async getRunById(req: Request, res: Response, next: NextFunction) { + try { + const runId = parseInt(req.params.id); + const run = await RunService.getRunById(runId); + res.status(200).json(run); + } catch (error: unknown) { + next(error); + } + } } diff --git a/scylla-server/src/index.ts b/scylla-server/src/index.ts index 52fd3fe3..c8738e5a 100644 --- a/scylla-server/src/index.ts +++ b/scylla-server/src/index.ts @@ -55,21 +55,20 @@ serverSocket.on('connection', (socket: Socket) => { serverProxy.configure(); }); -// TODO: Get host/port from DNC -const host = 'localhost'; -const mqttPort = '8080'; -const clientId = `mqtt_${Math.random().toString(16).slice(3)}`; +if (process.env.PROD === 'true') { + const host = process.env.PROD_SIREN_HOST_URL; + const mqttPort = '1883'; + const clientId = `Scylla-Server`; -const connectUrl = `mqtt://${host}:${mqttPort}`; + const connectUrl = `mqtt://${host}:${mqttPort}`; -const connection = connect(connectUrl, { - clientId, - clean: true, - connectTimeout: 4000, - username: 'scylla-server', - password: 'public', - reconnectPeriod: 1000 -}); + const connection = connect(connectUrl, { + clientId, + clean: true, + connectTimeout: 4000, + reconnectPeriod: 1000 + }); -const proxyClient = new ProxyClient(connection); -proxyClient.configure(); + const proxyClient = new ProxyClient(connection); + proxyClient.configure(); +} diff --git a/scylla-server/src/odyssey-base b/scylla-server/src/odyssey-base index af05d452..49eb83ab 160000 --- a/scylla-server/src/odyssey-base +++ b/scylla-server/src/odyssey-base @@ -1 +1 @@ -Subproject commit af05d452e054f0cfed1ba69b510d4188d3046456 +Subproject commit 49eb83ab586d0cb0d21d675a4786cfbaff5a9bc3 diff --git a/scylla-server/src/proxy/proxy-client.ts b/scylla-server/src/proxy/proxy-client.ts index 41032bef..ee34c153 100644 --- a/scylla-server/src/proxy/proxy-client.ts +++ b/scylla-server/src/proxy/proxy-client.ts @@ -23,7 +23,7 @@ export default class ProxyClient { * @param topics The topics to subscribe to */ private subscribeToTopics = (topics: Topic[]) => { - this.connection.subscribe(topics.map((topic) => topic.toString())); + this.connection.subscribe(topics.map((topic) => topic.valueOf())); }; /** @@ -39,7 +39,7 @@ export default class ProxyClient { */ private handleOpen = (packet: IConnackPacket) => { console.log('Connected to Siren', packet.properties); - this.subscribeToTopics(Object.values(Topic)); + this.subscribeToTopics([Topic.ALL]); }; /** @@ -47,7 +47,10 @@ export default class ProxyClient { * @param topic The topic the message was received on * @param message The message received from Siren */ - private handleMessage = (topic: string, payload: Buffer) => {}; + private handleMessage = (topic: string, payload: Buffer) => { + //TODO: Handle the message + console.log('Received Message: ', topic, payload.toString()); + }; /** * Handles receiving data from the car and: diff --git a/scylla-server/src/routes/data.routes.ts b/scylla-server/src/routes/data.routes.ts index fea2244e..ee0a0552 100644 --- a/scylla-server/src/routes/data.routes.ts +++ b/scylla-server/src/routes/data.routes.ts @@ -3,6 +3,6 @@ import DataController from '../controllers/data.controller'; const dataRouter = Router(); -dataRouter.get('/', DataController.getDataByDataTypeName); +dataRouter.get('/:dataTypeName', DataController.getDataByDataTypeName); export default dataRouter; diff --git a/scylla-server/src/routes/run.routes.ts b/scylla-server/src/routes/run.routes.ts index e5581570..73b07be5 100644 --- a/scylla-server/src/routes/run.routes.ts +++ b/scylla-server/src/routes/run.routes.ts @@ -5,4 +5,6 @@ const runRouter = Router(); runRouter.get('/', RunController.getAllRuns); +runRouter.get('/:id', RunController.getRunById); + export default runRouter;