diff --git a/gs/frontend/aro/package-lock.json b/gs/frontend/aro/package-lock.json index 304f71b11..f5b4193a7 100644 --- a/gs/frontend/aro/package-lock.json +++ b/gs/frontend/aro/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.13", "@tanstack/react-query": "^5.87.4", + "@tanstack/react-table": "^8.21.3", "@types/react-router-dom": "^5.3.3", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -28,7 +29,7 @@ "globals": "^16.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", - "vite": "^7.1.2" + "vite": "^7.1.5" } }, "node_modules/@babel/code-frame": { @@ -1598,6 +1599,39 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3928,7 +3962,6 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", - "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/gs/frontend/aro/package.json b/gs/frontend/aro/package.json index 631584353..d68242921 100644 --- a/gs/frontend/aro/package.json +++ b/gs/frontend/aro/package.json @@ -12,6 +12,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.13", "@tanstack/react-query": "^5.87.4", + "@tanstack/react-table": "^8.21.3", "@types/react-router-dom": "^5.3.3", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -30,6 +31,6 @@ "globals": "^16.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", - "vite": "^7.1.2" + "vite": "^7.1.5" } } diff --git a/gs/frontend/aro/src/new-request/new-request-form.tsx b/gs/frontend/aro/src/new-request/new-request-form.tsx index 5ac6f658a..9c6a83158 100644 --- a/gs/frontend/aro/src/new-request/new-request-form.tsx +++ b/gs/frontend/aro/src/new-request/new-request-form.tsx @@ -1,5 +1,6 @@ import "./new-request-form.css"; import { type ChangeEvent, useState } from "react"; +import React from "react"; const NewRequestForm = () => { const [latitude, setLatitude] = useState(0); @@ -11,16 +12,42 @@ const NewRequestForm = () => { // TODO: Show a map centered at latitude / longitude. }); - const handleSubmit = () => { - // TODO: Use the proper type for this - const submission = { - latitude, - longitude, - }; - // TODO: Submit form to backend - console.log(submission); + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + try { + const submission = { + latitude, + longitude, - alert("Thanks for submitting!"); + }; + + const response = await fetch('http://localhost:5000/aro-request', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // NOTE TO REVIEWERS: + // Add authentication headers when implemented + // 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(submission), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('Request submitted successfully:', result); + + alert("Request submitted successfully! You can view it in your requests list."); + + + + } catch (error) { + console.error('Error submitting request:', error); + alert("Error submitting request. Please try again."); + } }; const handleLatitudeChange = (event: ChangeEvent) => { diff --git a/gs/frontend/aro/src/profile/profile-api.ts b/gs/frontend/aro/src/profile/profile-api.ts index 1026dd601..eb8bb8819 100644 --- a/gs/frontend/aro/src/profile/profile-api.ts +++ b/gs/frontend/aro/src/profile/profile-api.ts @@ -4,13 +4,26 @@ import type { ProfileData } from "./profile-data.ts"; * @brief Gets the profile info of the current user */ export const getProfile = async (): Promise => { - // This is a mock implementation of an API call - return { - name: "John Doe", - email: "john.doe@gmail.com", - call_sign: "VAYPO", - phone: "1234567890", - }; + try { + const response = await fetch('http://localhost:5000/user/profile', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + // Add authentication headers here when implemented + // 'Authorization': `Bearer ${token}` + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching profile:', error); + throw error; + } }; /** @@ -18,6 +31,22 @@ export const getProfile = async (): Promise => { * @param data The new profile data */ export const updateProfile = async (data: ProfileData): Promise => { - // This is a mock implementation of an API call - console.log(data); + try { + const response = await fetch('http://localhost:5000/user/profile', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + // Add authentication headers here when implemented + // 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (error) { + console.error('Error updating profile:', error); + throw error; + } }; diff --git a/gs/frontend/aro/src/requests/request-item-data.ts b/gs/frontend/aro/src/requests/request-item-data.ts index f65cd84d7..c536ba8b2 100644 --- a/gs/frontend/aro/src/requests/request-item-data.ts +++ b/gs/frontend/aro/src/requests/request-item-data.ts @@ -1,12 +1,23 @@ + +export type AROCommandStatus = + | 'pending' + | 'scheduled' + | 'taken' + | 'cancelled' + | 'failed' + | 'completed'; + export interface RequestItemData { id: number; - latitude: number; - longitude: number; - status: string; + aro_id?: number; + latitude: number; + longitude: number; + status: AROCommandStatus; created_on: Date; request_sent_to_obc_on: Date | null; pic_taken_on: Date | null; pic_transmitted_on: Date | null; - cancellable_after: Date; + packet_id?: number | null; + cancellable_after: Date; // Add more properties as needed } diff --git a/gs/frontend/aro/src/requests/requests-api.ts b/gs/frontend/aro/src/requests/requests-api.ts index 135ed5efc..96abb2457 100644 --- a/gs/frontend/aro/src/requests/requests-api.ts +++ b/gs/frontend/aro/src/requests/requests-api.ts @@ -1,28 +1,27 @@ import type { RequestItemData } from "./request-item-data.ts"; export const getRequestItems = async (): Promise => { - return [ - { - id: 1, - status: "Pending", - longitude: 100, - latitude: 80, - created_on: new Date(2024, 10), - cancellable_after: new Date(2025, 12), - request_sent_to_obc_on: null, - pic_transmitted_on: null, - pic_taken_on: null, - }, - { - id: 2, - status: "Pending", - longitude: 120, - latitude: 80, - created_on: new Date(2024, 10), - cancellable_after: new Date(2025, 12), - request_sent_to_obc_on: null, - pic_transmitted_on: null, - pic_taken_on: null, - }, - ]; + try { + const response = await fetch('http://localhost:5000/aro-request'); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + + return data.map((item: any) => ({ + ...item, + status: item.status.toLowerCase(), + created_on: new Date(item.created_on), + request_sent_to_obc_on: item.request_sent_to_obc_on ? new Date(item.request_sent_to_obc_on) : null, + pic_taken_on: item.pic_taken_on ? new Date(item.pic_taken_on) : null, + pic_transmitted_on: item.pic_transmitted_on ? new Date(item.pic_transmitted_on) : null, + cancellable_after: item.cancellable_after ? new Date(item.cancellable_after) : new Date(), + })); + } catch (error) { + console.error('Error fetching ARO requests:', error); + throw error; + } }; diff --git a/gs/frontend/aro/src/requests/requests.tsx b/gs/frontend/aro/src/requests/requests.tsx index a89db15e9..cb4573540 100644 --- a/gs/frontend/aro/src/requests/requests.tsx +++ b/gs/frontend/aro/src/requests/requests.tsx @@ -1,11 +1,23 @@ -import { type MouseEvent, useEffect, useState } from "react"; -import type { RequestItemData } from "./request-item-data.ts"; -import RequestItem from "./request-item.tsx"; +import { type MouseEvent, useEffect, useState, useMemo } from "react"; +import type { RequestItemData, AROCommandStatus } from "./request-item-data.ts"; import { getRequestItems } from "./requests-api.ts"; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getFilteredRowModel, + createColumnHelper, + flexRender, + type SortingState, + type ColumnFiltersState +} from "@tanstack/react-table"; const Requests = () => { // TODO: Switch to using react-query const [data, setData] = useState([]); + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + useEffect(() => { const getRequestItemsRegular = async () => { const response = await getRequestItems(); @@ -25,38 +37,149 @@ const Requests = () => { }; }; + const columnHelper = createColumnHelper(); + + const columns = useMemo(() => [ + columnHelper.accessor("id", { + header: "ID", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("latitude", { + header: "Latitude", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("longitude", { + header: "Longitude", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("status", { + header: "Status", + cell: (info) => { + const status = info.getValue(); + + const statusDisplay = status.charAt(0).toUpperCase() + status.slice(1); + const getStatusColor = (status: AROCommandStatus) => { + switch (status) { + case 'pending': return '#ffc107'; + case 'scheduled': return '#007bff'; + case 'taken': return '#28a745'; + case 'cancelled': return '#6c757d'; + case 'failed': return '#dc3545'; + case 'completed': return '#28a745'; + default: return '#000'; + } + }; + return ( + + {statusDisplay} + + ); + }, + }), + columnHelper.accessor("created_on", { + header: "Created On", + cell: (info) => info.getValue().toString(), + }), + columnHelper.accessor("request_sent_to_obc_on", { + header: "Request Sent to OBC On", + cell: (info) => info.getValue()?.toString() || "", + }), + columnHelper.accessor("pic_taken_on", { + header: "Picture Taken On", + cell: (info) => info.getValue()?.toString() || "", + }), + columnHelper.accessor("pic_transmitted_on", { + header: "Picture transmitted On", + cell: (info) => info.getValue()?.toString() || "", + }), + columnHelper.display({ + id: "downloadPacket", + header: "Download Packet", + cell: ({ row }) => ( + + ), + }), + columnHelper.display({ + id: "cancelRequest", + header: "Cancel Request", + cell: ({ row }) => ( + + ), + }), + ], []); + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnFilters, + }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + }); + if (data.length === 0) { return
You do not have any request created.
; } return ( - - - - - - - - - - - - - - - - - {data.map((item: RequestItemData, key: number) => { - console.log(item); - return ( - - {" "} - +
+
+ table.getColumn("status")?.setFilterValue(e.target.value)} + style={{ marginRight: "1rem" }} + /> +
+
IDLatitudeLongitudeStatusCreated OnRequest Sent to OBC OnPicture Taken OnPicture transmitted OnDownload PacketCancel Request
+ + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} - ); - })} - -
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {header.column.getIsSorted() === "asc" ? " ↑" : ""} + {header.column.getIsSorted() === "desc" ? " ↓ " : ""} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ))} + + + ); }; diff --git a/gs/frontend/aro/src/shared-types.ts b/gs/frontend/aro/src/shared-types.ts new file mode 100644 index 000000000..cf2302c1c --- /dev/null +++ b/gs/frontend/aro/src/shared-types.ts @@ -0,0 +1,119 @@ +// Shared types based on database schema for ARO and MCC frontends + +// Enums matching database schema +export type AROCommandStatus = + | 'pending' + | 'scheduled' + | 'taken' + | 'cancelled' + | 'failed' + | 'completed'; + +export type CommandStatus = + | 'pending' + | 'scheduled' + | 'ongoing' + | 'cancelled' + | 'failed' + | 'completed'; + +export type SessionStatus = + | 'pending' + | 'scheduled' + | 'ongoing' + | 'completed'; + +export type MainPacketType = 'uplink' | 'downlink'; + + +export interface ARORequest { + id: number; + aro_id: number; + latitude: number; + longitude: number; + status: AROCommandStatus; + created_on: string; + request_sent_to_obc_on: string | null; + pic_taken_on: string | null; + pic_transmitted_on: string | null; + packet_id: number | null; +} + +export interface Session { + id: number; + start_time: string; + end_time: string | null; + status: SessionStatus; +} + +export interface Packet { + id: number; + session_id: number; + raw_data: string; + type: MainPacketType; + subtype: string; // enum - TODO: Define specific values when known + payload_data: string; + created_on: string; + offset: number; +} + +export interface Command { + id: number; + status: CommandStatus; + type: number; // References master.commands.id + params: string; +} + +export interface Telemetry { + id: number; + type: number; // References master.telemetry.id + value: string; +} + +// Master table interfaces +export interface MasterCommand { + id: number; + name: string; + params: string; + format: string; + data_size: number; + total_size: number; +} + +export interface MasterTelemetry { + id: number; + name: string; + format: string; + data_size: number; + total_size: number; +} + +// Extended interfaces with joined data (for frontend display) +export interface TelemetryWithMaster extends Telemetry { + telemetry_name?: string; + telemetry_format?: string; + created_on?: string; +} + +export interface CommandWithMaster extends Command { + command_name?: string; + command_format?: string; + created_on?: string; +} + +// User-related interfaces +export interface UserData { + id: number; + call_sign: string; + first_name: string; + last_name: string | null; + phone_number: string | null; +} + +export interface UserLogin { + id: number; + email: string; + created_on: string; + user_data_id: number; + email_verification_token: string | null; +} diff --git a/gs/frontend/mcc/package-lock.json b/gs/frontend/mcc/package-lock.json index 9b91a730a..8b6380a4e 100644 --- a/gs/frontend/mcc/package-lock.json +++ b/gs/frontend/mcc/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.13", + "@tanstack/react-table": "^8.21.3", "bootstrap": "^5.3.8", "react": "^19.1.1", "react-bootstrap": "^2.10.10", @@ -28,7 +29,7 @@ "globals": "^16.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", - "vite": "^7.1.2" + "vite": "^7.1.5" } }, "node_modules/@babel/code-frame": { @@ -1669,6 +1670,39 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/gs/frontend/mcc/package.json b/gs/frontend/mcc/package.json index ccaa1b02e..b50db700c 100644 --- a/gs/frontend/mcc/package.json +++ b/gs/frontend/mcc/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.13", + "@tanstack/react-table": "^8.21.3", "bootstrap": "^5.3.8", "react": "^19.1.1", "react-bootstrap": "^2.10.10", @@ -30,6 +31,6 @@ "globals": "^16.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.39.1", - "vite": "^7.1.2" + "vite": "^7.1.5" } } diff --git a/gs/frontend/mcc/src/aro-requests/aro-requests.tsx b/gs/frontend/mcc/src/aro-requests/aro-requests.tsx index a3887d106..f01270dac 100644 --- a/gs/frontend/mcc/src/aro-requests/aro-requests.tsx +++ b/gs/frontend/mcc/src/aro-requests/aro-requests.tsx @@ -1,28 +1,37 @@ import Table from "react-bootstrap/Table"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getFilteredRowModel, + createColumnHelper, + flexRender, + type SortingState, + type ColumnFiltersState +} from "@tanstack/react-table"; + + +import { type AROCommandStatus } from "../shared-types.ts"; type AROItemProps = { id: number; + aro_id?: number; latitude: number; - longitude: number; - status: string; + longitude: number; + status: AROCommandStatus; + created_on?: string; + request_sent_to_obc_on?: string | null; + pic_taken_on?: string | null; + pic_transmitted_on?: string | null; + packet_id?: number | null; }; -function AROItem({ id, latitude, longitude, status }: AROItemProps) { - return ( - <> - - {id} - {longitude} - {latitude} - {status} - - - ); -} function ARORequests() { const [loading, setLoading] = useState(true); const [aroRequests, setARORequests] = useState([]); + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); useEffect(() => { fetch("http://localhost:5000/aro-request") @@ -33,37 +42,118 @@ function ARORequests() { }); }, []); + const columnHelper = createColumnHelper(); + + const columns = useMemo(() => [ + columnHelper.accessor("id", { + header: "#", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("latitude", { + header: "Latitude", + cell: (info) => info.getValue().toFixed(6), + }), + columnHelper.accessor("longitude", { + header: "Longitude", + cell: (info) => info.getValue().toFixed(6), + }), + columnHelper.accessor("status", { + header: "Status", + cell: (info) => { + const status = info.getValue(); + + const statusDisplay = status.charAt(0).toUpperCase() + status.slice(1); + const getStatusColor = (status: AROCommandStatus) => { + switch (status) { + case 'pending': return '#ffc107'; + case 'scheduled': return '#007bff'; + case 'taken': return '#28a745'; + case 'cancelled': return '#6c757d'; + case 'failed': return '#dc3545'; + case 'completed': return '#28a745'; + default: return '#000'; + } + }; + return ( + + {statusDisplay} + + ); + }, + }), + ...(aroRequests.some(req => req.created_on) ? [ + columnHelper.accessor("created_on", { + header: "Created On", + cell: (info) => info.getValue() ? new Date(info.getValue()!).toLocaleString() : '', + }) + ] : []), + ], [aroRequests]); + + const table = useReactTable({ + data: aroRequests, + columns, + state: { + sorting, + columnFilters, + }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + }); + return (
+
+ table.getColumn("status")?.setFilterValue(e.target.value)} + style={{ marginRight: "1rem" }} + /> + + Available statuses: pending, scheduled, taken, cancelled, failed, completed + +
+ - - - - - - + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} - {loading - ? ( - - + {loading ? ( + + + + ) : ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} - ) - : ( - aroRequests.map((item) => { - return ( - - ); - }) - )} + )) + )}
#LatitudeLongitudeStatus
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {header.column.getIsSorted() === "asc" ? " ↑" : ""} + {header.column.getIsSorted() === "desc" ? " ↓ " : ""} +
Loading...
Loading...
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
diff --git a/gs/frontend/mcc/src/common/logs.tsx b/gs/frontend/mcc/src/common/logs.tsx index 8df546bf6..62d2bd447 100644 --- a/gs/frontend/mcc/src/common/logs.tsx +++ b/gs/frontend/mcc/src/common/logs.tsx @@ -1,17 +1,44 @@ import Table from "react-bootstrap/Table"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getFilteredRowModel, + createColumnHelper, + flexRender, + type SortingState, + type ColumnFiltersState +} from "@tanstack/react-table"; -function LogItem() { - const [date, setDate] = useState(Date.now()); - const [log, setLog] = useState(""); +type LogData = { + id: number; + date: number; + log: string; +}; + +function Logs() { + const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); const fetchLogData = async () => { - const response = await fetch(`http://localhost:5000/recent-logs/`); - const data = await response.json(); - setDate(data.date); - setLog(data.log); - setLoading(false); + try { + const response = await fetch(`http://localhost:5000/recent-logs/`); + const data = await response.json(); + + if (Array.isArray(data)) { + setLogs(data.map((item, index) => ({ ...item, id: index }))); + } else { + + setLogs([{ id: 0, date: data.date, log: data.log }]); + } + setLoading(false); + } catch (error) { + console.error("Error fetching logs:", error); + setLoading(false); + } }; useEffect(() => { @@ -20,32 +47,85 @@ function LogItem() { return () => clearInterval(interval); }, []); - return ( - - {loading ? Loading... : ( - <> - {date} - {log} - {" "} - - )} - - ); -} + const columnHelper = createColumnHelper(); + + const columns = useMemo(() => [ + columnHelper.accessor("date", { + header: "Time", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("log", { + header: "Log", + cell: (info) => info.getValue(), + }), + ], []); + + const table = useReactTable({ + data: logs, + columns, + state: { + sorting, + columnFilters, + }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + }); -function Logs() { - const count = 5; return (
+
+ table.getColumn("log")?.setFilterValue(e.target.value)} + style={{ marginRight: "1rem" }} + /> +
+ - - - - + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} - {[...Array(count).keys()].map((key) => )} + {loading ? ( + + + + ) : logs.length === 0 ? ( + + + + ) : ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + )) + )}
TimeLog
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {header.column.getIsSorted() === "asc" ? " ↑" : ""} + {header.column.getIsSorted() === "desc" ? " ↓ " : ""} +
Loading...
No logs available
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
diff --git a/gs/frontend/mcc/src/mission-commands/mission-commands.tsx b/gs/frontend/mcc/src/mission-commands/mission-commands.tsx index 813201bde..36c38441b 100644 --- a/gs/frontend/mcc/src/mission-commands/mission-commands.tsx +++ b/gs/frontend/mcc/src/mission-commands/mission-commands.tsx @@ -8,6 +8,7 @@ const MISSON_COMMAND_PREFIX = "MCC_"; function MissionCommands() { const [commands, setCommands] = useState(""); const [commandResponse, setCommandResponse] = useState(""); + const [filterText, setFilterText] = useState(""); const inputRef = useRef(null); useEffect(() => { @@ -73,6 +74,14 @@ function MissionCommands() { ref={inputRef} /> +
+ setFilterText(e.target.value)} + style={{ marginRight: "1rem" }} + /> +
@@ -80,7 +89,16 @@ function MissionCommands() { - {commandResponse && } + + {commandResponse && + + } +
{commandResponse}
+ {filterText ? + commandResponse.toString().includes(filterText) ? + commandResponse : "No matching output" + : commandResponse} +
diff --git a/gs/frontend/mcc/src/shared-types.ts b/gs/frontend/mcc/src/shared-types.ts new file mode 100644 index 000000000..036d4c4a6 --- /dev/null +++ b/gs/frontend/mcc/src/shared-types.ts @@ -0,0 +1,119 @@ +// Shared types based on database schema for ARO and MCC frontends + +// Enums matching database schema +export type AROCommandStatus = + | 'pending' + | 'scheduled' + | 'taken' + | 'cancelled' + | 'failed' + | 'completed'; + +export type CommandStatus = + | 'pending' + | 'scheduled' + | 'ongoing' + | 'cancelled' + | 'failed' + | 'completed'; + +export type SessionStatus = + | 'pending' + | 'scheduled' + | 'ongoing' + | 'completed'; + +export type MainPacketType = 'uplink' | 'downlink'; + +// Database table interfaces +export interface ARORequest { + id: number; + aro_id: number; + latitude: number; // decimal in DB, number in TS + longitude: number; // decimal in DB, number in TS + status: AROCommandStatus; + created_on: string; // ISO datetime string + request_sent_to_obc_on: string | null; + pic_taken_on: string | null; + pic_transmitted_on: string | null; + packet_id: number | null; +} + +export interface Session { + id: number; + start_time: string; // ISO datetime string + end_time: string | null; + status: SessionStatus; +} + +export interface Packet { + id: number; + session_id: number; + raw_data: string; + type: MainPacketType; + subtype: string; // enum - TODO: Define specific values when known + payload_data: string; + created_on: string; + offset: number; +} + +export interface Command { + id: number; + status: CommandStatus; + type: number; // References master.commands.id + params: string; +} + +export interface Telemetry { + id: number; + type: number; // References master.telemetry.id + value: string; +} + +// Master table interfaces +export interface MasterCommand { + id: number; + name: string; + params: string; + format: string; + data_size: number; + total_size: number; +} + +export interface MasterTelemetry { + id: number; + name: string; + format: string; + data_size: number; + total_size: number; +} + +// Extended interfaces with joined data (for frontend display) +export interface TelemetryWithMaster extends Telemetry { + telemetry_name?: string; + telemetry_format?: string; + created_on?: string; +} + +export interface CommandWithMaster extends Command { + command_name?: string; + command_format?: string; + created_on?: string; +} + +// User-related interfaces +export interface UserData { + id: number; + call_sign: string; + first_name: string; + last_name: string | null; + phone_number: string | null; +} + +export interface UserLogin { + id: number; + email: string; + created_on: string; + user_data_id: number; + email_verification_token: string | null; +} diff --git a/gs/frontend/mcc/src/telemetry/telemetry-data.tsx b/gs/frontend/mcc/src/telemetry/telemetry-data.tsx index 39332d59f..2554f4c86 100644 --- a/gs/frontend/mcc/src/telemetry/telemetry-data.tsx +++ b/gs/frontend/mcc/src/telemetry/telemetry-data.tsx @@ -1,18 +1,148 @@ import Table from "react-bootstrap/Table"; +import { useState, useMemo, useEffect } from "react"; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getFilteredRowModel, + createColumnHelper, + flexRender, + type SortingState, + type ColumnFiltersState +} from "@tanstack/react-table"; +import { type TelemetryWithMaster } from "../shared-types.ts"; + function TelemetryData() { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + + useEffect(() => { + const fetchTelemetryData = async () => { + try { + setLoading(true); + const response = await fetch('http://localhost:5000/telemetry'); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const telemetryData = await response.json(); + setData(telemetryData); + } catch (error) { + console.error('Error fetching telemetry data:', error); + + setData([]); + } finally { + setLoading(false); + } + }; + + fetchTelemetryData(); + + + const interval = setInterval(fetchTelemetryData, 30000); // Future code reader: This is optional btw and can be tweaked based on what you want + return () => clearInterval(interval); + }, []); + + const columnHelper = createColumnHelper(); + + const columns = useMemo(() => [ + columnHelper.accessor("id", { + header: "ID", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("telemetry_name", { + header: "Type", + cell: (info) => info.getValue() || `Type ${info.row.original.type}`, + }), + columnHelper.accessor("value", { + header: "Value", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("telemetry_format", { + header: "Format", + cell: (info) => info.getValue() || "Unknown", + }), + columnHelper.accessor("created_on", { + header: "Timestamp", + cell: (info) => info.getValue() ? new Date(info.getValue()!).toLocaleString() : "N/A", + }), + ], []); + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnFilters, + }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + }); + return (
+
+ table.getColumn("telemetry_name")?.setFilterValue(e.target.value)} + style={{ marginRight: "1rem" }} + /> + table.getColumn("value")?.setFilterValue(e.target.value)} + /> +
+ - - - + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} - - - + {loading ? ( + + + + ) : data.length === 0 ? ( + + + + ) : ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + )) + )}
Recent Data
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {header.column.getIsSorted() === "asc" ? " ↑" : ""} + {header.column.getIsSorted() === "desc" ? " ↓ " : ""} +
Placeholder
Loading telemetry data...
No telemetry data available
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
diff --git a/gs/frontend/shared-types.ts b/gs/frontend/shared-types.ts new file mode 100644 index 000000000..036d4c4a6 --- /dev/null +++ b/gs/frontend/shared-types.ts @@ -0,0 +1,119 @@ +// Shared types based on database schema for ARO and MCC frontends + +// Enums matching database schema +export type AROCommandStatus = + | 'pending' + | 'scheduled' + | 'taken' + | 'cancelled' + | 'failed' + | 'completed'; + +export type CommandStatus = + | 'pending' + | 'scheduled' + | 'ongoing' + | 'cancelled' + | 'failed' + | 'completed'; + +export type SessionStatus = + | 'pending' + | 'scheduled' + | 'ongoing' + | 'completed'; + +export type MainPacketType = 'uplink' | 'downlink'; + +// Database table interfaces +export interface ARORequest { + id: number; + aro_id: number; + latitude: number; // decimal in DB, number in TS + longitude: number; // decimal in DB, number in TS + status: AROCommandStatus; + created_on: string; // ISO datetime string + request_sent_to_obc_on: string | null; + pic_taken_on: string | null; + pic_transmitted_on: string | null; + packet_id: number | null; +} + +export interface Session { + id: number; + start_time: string; // ISO datetime string + end_time: string | null; + status: SessionStatus; +} + +export interface Packet { + id: number; + session_id: number; + raw_data: string; + type: MainPacketType; + subtype: string; // enum - TODO: Define specific values when known + payload_data: string; + created_on: string; + offset: number; +} + +export interface Command { + id: number; + status: CommandStatus; + type: number; // References master.commands.id + params: string; +} + +export interface Telemetry { + id: number; + type: number; // References master.telemetry.id + value: string; +} + +// Master table interfaces +export interface MasterCommand { + id: number; + name: string; + params: string; + format: string; + data_size: number; + total_size: number; +} + +export interface MasterTelemetry { + id: number; + name: string; + format: string; + data_size: number; + total_size: number; +} + +// Extended interfaces with joined data (for frontend display) +export interface TelemetryWithMaster extends Telemetry { + telemetry_name?: string; + telemetry_format?: string; + created_on?: string; +} + +export interface CommandWithMaster extends Command { + command_name?: string; + command_format?: string; + created_on?: string; +} + +// User-related interfaces +export interface UserData { + id: number; + call_sign: string; + first_name: string; + last_name: string | null; + phone_number: string | null; +} + +export interface UserLogin { + id: number; + email: string; + created_on: string; + user_data_id: number; + email_verification_token: string | null; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..3972ed8aa --- /dev/null +++ b/package-lock.json @@ -0,0 +1,70 @@ +{ + "name": "OBC-firmware", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@tanstack/react-table": "^8.21.3" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..640360816 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@tanstack/react-table": "^8.21.3" + } +}