diff --git a/frontend/README.md b/frontend/README.md index f867ea4ac..de434e676 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -31,14 +31,25 @@ The frontend is organized as 6 workspaces out of 9 in the whole monorepo, divide ## State Management -The application uses **Zustand** with a slice-based architecture: +The application uses **Zustand** with a slice-based architecture, organized by feature domain: -- `workspacesSlice` - Manages workspaces, filters, charts, and tabs -- `catalogSlice` - Stores telemetry and command catalogs -- `telemetrySlice` - Real-time telemetry data +### Global Slices + +- `appSlice` - Application mode, settings, and configuration +- `connectionsSlice` - WebSockets connection statuses +- `telemetrySlice` - Real-time telemetry data buffer - `messagesSlice` - System messages and logs -- `appSlice` - Application mode and settings -- `rightSidebarSlice` - UI state for sidebar panels +- `catalogSlice` - Static definitions for telemetry packets and commands + +### Feature Slices + +- **Workspace Feature** (`features/workspace`) + - `workspacesSlice` - Manages workspace layout + - `rightSidebarSlice` - UI state for the collapsible sidebar and its tabs +- **Charts Feature** (`features/charts`) + - `chartsSlice` - Manages chart instances, series configuration, and visualization settings +- **Filtering Feature** (`features/filtering`) + - `filteringSlice` - Manages active filters, search queries, and category selection ### Workspace System @@ -98,7 +109,7 @@ import { Plus, Settings } from "@workspace/ui/icons"; - **CSS Variables** for theming (defined in `globals.css`) - **Tailwind CSS** for utility classes - **Dark mode** support via CSS class toggling -- Multiple color schemes (default, pink, etc.) +- Multiple color schemes (default and pink) ### Adding Icons @@ -133,13 +144,14 @@ frontend/ ├── testing-view/ │ ├── src/ │ │ ├── assets/ # Assets (images, gifs, etc.) -│ │ ├── components/ # UI components +│ │ ├── components/ # Global UI components +│ │ ├── features/ # Components, hooks, types and store slices related to features │ │ ├── layout/ # App layout │ │ ├── pages/ # Route pages -│ │ ├── store/ # Zustand store slices -│ │ ├── hooks/ # Custom hooks +│ │ ├── store/ # Global Zustand store slices +│ │ ├── hooks/ # Global custom hooks │ │ ├── constants/ # Config and constants -│ │ ├── types/ # TypeScript types +│ │ ├── types/ # Global TypeScript types │ │ ├── mocks/ # Mocks │ │ └── lib/ # Utilities │ └── public/ # Static assets diff --git a/frontend/frontend-kit/core/src/logger.ts b/frontend/frontend-kit/core/src/logger.ts index 23f64cdbf..b6ff4f42e 100644 --- a/frontend/frontend-kit/core/src/logger.ts +++ b/frontend/frontend-kit/core/src/logger.ts @@ -1,24 +1,30 @@ -type LoggerModule = "testing-view" | "competition-view" | "core" | "ui"; - -const colors = { - "testing-view": "\x1b[36m", // Cyan - "competition-view": "\x1b[35m", // Magenta - core: "\x1b[33m", // Yellow - ui: "\x1b[32m", // Green - reset: "\x1b[0m", -}; +import { loggerColors } from "./loggerColors"; +import type { LoggerModule } from "./types"; +/** + * Creates a logger for a given module + * @param module - the module to log for (it's just a colored string that will be shown between `[` and `]` before the message itself) + * @returns a logger object with `log`, `warn`, and `error` methods + */ function createLogger(module: LoggerModule) { - const color = colors[module]; + const color = loggerColors[module]; const prefix = `[${module.toUpperCase()}]`; return { - log: console.log.bind(console, `${color}${prefix}${colors.reset}`), - warn: console.warn.bind(console, `${color}${prefix}${colors.reset}`), - error: console.error.bind(console, `${color}${prefix}${colors.reset}`), + // It's important to use `bind` here to correctly display log file path and line number in the console + // Otherwise, console prints will just point to this file + log: console.log.bind(console, `${color}${prefix}${loggerColors.reset}`), + warn: console.warn.bind(console, `${color}${prefix}${loggerColors.reset}`), + error: console.error.bind( + console, + `${color}${prefix}${loggerColors.reset}`, + ), }; } +/** + * Logger object with methods for each module + */ export const logger = { testingView: createLogger("testing-view"), competitionView: createLogger("competition-view"), diff --git a/frontend/frontend-kit/core/src/loggerColors.ts b/frontend/frontend-kit/core/src/loggerColors.ts new file mode 100644 index 000000000..c99f7d68d --- /dev/null +++ b/frontend/frontend-kit/core/src/loggerColors.ts @@ -0,0 +1,7 @@ +export const loggerColors = { + "testing-view": "\x1b[36m", // Cyan + "competition-view": "\x1b[35m", // Magenta + core: "\x1b[33m", // Yellow + ui: "\x1b[32m", // Green + reset: "\x1b[0m", +}; diff --git a/frontend/frontend-kit/core/src/minMaxDownsample.ts b/frontend/frontend-kit/core/src/minMaxDownsample.ts index 9b4cb8685..a0324c22f 100644 --- a/frontend/frontend-kit/core/src/minMaxDownsample.ts +++ b/frontend/frontend-kit/core/src/minMaxDownsample.ts @@ -1,21 +1,74 @@ -export const minMaxDownsample = (buffer: any[]) => { +import { type TelemetryPacket, type VariableValue } from "./types"; + +/** + * Helper to extract a numeric value for comparison. + * Handles both primitive numbers, booleans and { last, average } objects. + */ +const getNumericValue = ( + val: VariableValue | undefined, +): number | undefined => { + if (typeof val === "number") { + return val; + } + + if (typeof val === "boolean") { + return val ? 1 : 0; + } + + if ( + typeof val === "object" && + val !== null && + "last" in val && + "average" in val + ) { + return val.last; + } + + return undefined; +}; + +/** + * Downsamples a buffer of packets using the min-max algorithm.\ + * It considers only numeric variables, booleans and { last, average } object variables. + * + * The idea is to reduce the number of packets in the buffer by keeping only the min and max packets + * to prevent the app from freezing when there are too many packets. (Usually happens on start) + * + * @param buffer - array of packets to downsample, should contain at least 2 elements + * @returns downsampled buffer with only min and max packets from the original buffer (in chronological order) + */ +export const minMaxDownsample = (buffer: TelemetryPacket[]) => { + if (buffer.length < 2) return buffer; + let minIdx = 0; let maxIdx = 0; buffer.forEach((packet, i) => { const measurements = packet.measurementUpdates || {}; + + // At the beginning the initial champion is the first variable in the packet const firstKey = Object.keys(measurements)[0]; - const val = measurements[firstKey as keyof typeof measurements]; + if (!firstKey) return; + + const rawVal = measurements[firstKey]; + const val = getNumericValue(rawVal); + + const minVal = getNumericValue( + buffer[minIdx]?.measurementUpdates[firstKey], + ); + const maxVal = getNumericValue( + buffer[maxIdx]?.measurementUpdates[firstKey], + ); - if (typeof val === "number") { - if (val < (buffer[minIdx].measurementUpdates[firstKey] ?? Infinity)) - minIdx = i; - if (val > (buffer[maxIdx].measurementUpdates[firstKey] ?? -Infinity)) - maxIdx = i; + // Compare local min and max with the global champions + // If one of them is undefined, use Infinity or -Infinity respectively + if (val !== undefined) { + if (val < (minVal ?? Infinity)) minIdx = i; + if (val > (maxVal ?? -Infinity)) maxIdx = i; } }); - // 4. Return them in chronological order to maintain X-axis integrity + // Return them in chronological order to maintain X-axis integrity const result = minIdx < maxIdx ? [buffer[minIdx], buffer[maxIdx]] diff --git a/frontend/frontend-kit/core/src/types.ts b/frontend/frontend-kit/core/src/types.ts index fb7538dde..e0346cc78 100644 --- a/frontend/frontend-kit/core/src/types.ts +++ b/frontend/frontend-kit/core/src/types.ts @@ -1,4 +1,41 @@ +/** + * Options for the `onTopic` method of the `SocketService` class. + */ export interface TopicOptions { + /** The downsampling method to use. */ downsample?: "min-max" | "none"; + + /** The throttle time in milliseconds. */ throttle?: number; } + +/** + * The value of a variable in a telemetry packet. + */ +export type VariableValue = + | { last: number; average: number } + | boolean + | string + | number; + +/** + * The variables of a telemetry packet. + */ +export type Variables = Record; + +/** + * A telemetry packet that arrives in high frequency.\ + * Don't confuse it with the `TelemetryCatalogItem` type. + */ +export interface TelemetryPacket { + count: number; + cycleTime: number; + hexValue: string; + id: number; + measurementUpdates: Variables; +} + +/** + * The modules that can be logged to. Used for the `logger` object. + */ +export type LoggerModule = "testing-view" | "competition-view" | "core" | "ui"; diff --git a/frontend/frontend-kit/core/src/websocket.ts b/frontend/frontend-kit/core/src/websocket.ts index b8bd59dbc..2d4005654 100644 --- a/frontend/frontend-kit/core/src/websocket.ts +++ b/frontend/frontend-kit/core/src/websocket.ts @@ -17,19 +17,39 @@ import { logger } from "./logger"; import { minMaxDownsample } from "./minMaxDownsample"; import { type TopicOptions } from "./types"; +/** + * Service for connecting to the WebSocket server and subscribing to topics + */ class SocketService { + /** + * Singleton instance that holds the WebSocket subject + */ private socketSource$ = new ReplaySubject>(1); + + /** + * Subject that holds the status of the WebSocket connection. + */ public status$ = new BehaviorSubject< "connected" | "disconnected" | "connecting" >("disconnected"); + /** + * Observable that emits the messages from the WebSocket server. + */ public messages$: Observable = this.socketSource$.pipe( switchMap((socket) => socket), shareReplay(1), ); + /** + * Disposable WebSocket connection object. Lives only as long as the connection is open. + */ private ws: WebSocketSubject | null = null; + /** + * Connects to the WebSocket server by creating a new connection object and pushing it to `socketSource$`. + * @param port - the port to connect to. Defaults to 4000. + */ connect(port: number = 4000) { if (this.ws) return; @@ -62,20 +82,34 @@ class SocketService { this.socketSource$.next(this.ws); } + /** + * Cleans up the WebSocket connection by setting the connection object to null and updating the status to "disconnected". + */ private cleanup() { this.ws = null; this.status$.next("disconnected"); } + /** + * Creates an observable that emits the messages from the WebSocket server for a given topic. + * @param topic - the topic to subscribe to. + * @param options - options for the observable. + + * Downsampling and throttling are supported. + * In case of downsampling, throttling option is used as the buffering time and defaults to 100ms. + * @returns an observable that emits the messages from the WebSocket server for a given topic. + */ onTopic(topic: string, options: TopicOptions = {}) { let pipe$ = this.messages$.pipe( filter((msg) => msg.topic === topic), map((msg) => msg.payload), ); + // Apply downsampling if requested if (options.downsample == "min-max") { pipe$ = pipe$.pipe( - bufferTime(options.throttle || 100), + // Apply buffering + bufferTime(options.throttle ?? 100), filter((buffer) => buffer.length > 0), concatMap((buffer) => { if (buffer.length <= 2) return from(buffer); @@ -89,6 +123,7 @@ class SocketService { ); } + // Apply throttling if requested if (options.throttle) { pipe$ = pipe$.pipe(throttleTime(options.throttle, asyncScheduler)); } @@ -96,6 +131,12 @@ class SocketService { return pipe$; } + /** + * Posts a message to the WebSocket server. If the connection is not established, an error is logged and the message is not sent. + * @param topic - the topic to post to. + * @param payload - the payload to post. + * // TODO: reference payloads definition file + */ post(topic: string, payload: any) { if (!this.ws) { logger.core.error("Cannot post: Socket not connected."); diff --git a/frontend/testing-view/src/App.tsx b/frontend/testing-view/src/App.tsx index 2eb92f118..d5db347a5 100644 --- a/frontend/testing-view/src/App.tsx +++ b/frontend/testing-view/src/App.tsx @@ -1,11 +1,11 @@ import { useTopic, useWebSocket } from "@workspace/ui/hooks"; import { Route, Routes } from "react-router"; import { AppModeRouter } from "./components/AppModeRouter"; -import { ModeSwitcher } from "./components/DevTools/ModeSwitcher"; +import { ModeSwitcher } from "./components/devTools/ModeSwitcher"; import { ErrorBoundary } from "./components/ErrorBoundary"; +import { useChartsConfiguration } from "./features/charts/hooks/useChartsConfiguration"; import useAppConfigs from "./hooks/useAppConfigs"; import { useAppMode } from "./hooks/useAppMode"; -import { useChartsConfiguration } from "./hooks/useChartsConfiguration"; import { useErrorHandler } from "./hooks/useErrorHandler"; import { useTransformedBoards } from "./hooks/useTransformedBoards"; import { AppLayout } from "./layout/AppLayout"; diff --git a/frontend/testing-view/src/components/AppModeRouter.tsx b/frontend/testing-view/src/components/AppModeRouter.tsx index b62b2e01a..18148be81 100644 --- a/frontend/testing-view/src/components/AppModeRouter.tsx +++ b/frontend/testing-view/src/components/AppModeRouter.tsx @@ -6,6 +6,12 @@ interface AppModeRouterProps { children: React.ReactNode; } +/** + * This component works as a router + * and renders the appropriate page based on the app mode. + * + * Note: If mode is not loading or error, it renders the children normally + */ export const AppModeRouter = ({ children }: AppModeRouterProps) => { const appMode = useStore((s) => s.appMode); diff --git a/frontend/testing-view/src/components/Error.tsx b/frontend/testing-view/src/components/Error.tsx index ac4706b4d..fbec77e4b 100644 --- a/frontend/testing-view/src/components/Error.tsx +++ b/frontend/testing-view/src/components/Error.tsx @@ -5,10 +5,18 @@ import errorGif from "../assets/error.gif"; import { useStore } from "../store/store"; interface ErrorProps { + /** Optional error to display. Can be null or undefined. In this case component will show default error message */ error?: Error | null; + /** Optional component stack trace to display. Can be null or undefined. In this case component will not show anything */ componentStack?: string | null; } +/** + * Renders error page with the given error and component stack + * + * Displays an error message, optional component stack trace,\ + * and provides actions to reload the application or inspect the stack. + */ export const Error = ({ error: propError, componentStack }: ErrorProps) => { const storeError = useStore((s) => s.error); const error = propError || storeError; diff --git a/frontend/testing-view/src/components/ErrorBoundary.tsx b/frontend/testing-view/src/components/ErrorBoundary.tsx index d53a89989..a69c23611 100644 --- a/frontend/testing-view/src/components/ErrorBoundary.tsx +++ b/frontend/testing-view/src/components/ErrorBoundary.tsx @@ -13,6 +13,12 @@ interface State { componentStack?: string | null; } +/** + * This component works as a wrapper and + * catches and handles any unhandled errors and unhandled promise rejections. + * + * The idea is to prevent the app from crashing when an unhandled error occurs. + */ export class ErrorBoundary extends Component { constructor(props: Props) { super(props); diff --git a/frontend/testing-view/src/components/Footer.tsx b/frontend/testing-view/src/components/Footer.tsx index 24dd95fd1..6846cdb93 100644 --- a/frontend/testing-view/src/components/Footer.tsx +++ b/frontend/testing-view/src/components/Footer.tsx @@ -1,4 +1,7 @@ -const Footer = () => { +/** + * Renders a footer with the app name, current year and the copyright notice + */ +export const Footer = () => { const currentYear = new Date().getFullYear(); const dateRange = currentYear <= 2025 ? `${currentYear}` : `2025-${currentYear}`; diff --git a/frontend/testing-view/src/components/Loading.tsx b/frontend/testing-view/src/components/Loading.tsx index 3ad478f4f..698ebdc71 100644 --- a/frontend/testing-view/src/components/Loading.tsx +++ b/frontend/testing-view/src/components/Loading.tsx @@ -1,5 +1,8 @@ import loadingGif from "../assets/loading-monkey.gif"; +/** + * Renders a loading page with a monkey GIF, text and a glowing loading indicator + */ export const Loading = () => { return (
@@ -32,6 +35,7 @@ export const Loading = () => {

+ {/* Simple glowing loading indicator */}
diff --git a/frontend/testing-view/src/components/DevTools/ModeSwitcher.tsx b/frontend/testing-view/src/components/devTools/ModeSwitcher.tsx similarity index 100% rename from frontend/testing-view/src/components/DevTools/ModeSwitcher.tsx rename to frontend/testing-view/src/components/devTools/ModeSwitcher.tsx diff --git a/frontend/testing-view/src/components/Header/Header.tsx b/frontend/testing-view/src/components/header/Header.tsx similarity index 88% rename from frontend/testing-view/src/components/Header/Header.tsx rename to frontend/testing-view/src/components/header/Header.tsx index 59b2949cf..1f6b977c2 100644 --- a/frontend/testing-view/src/components/Header/Header.tsx +++ b/frontend/testing-view/src/components/header/Header.tsx @@ -1,10 +1,10 @@ import { Separator, SidebarTrigger } from "@workspace/ui"; import { useLocation } from "react-router"; import { PAGES } from "../../constants/pages"; +import { KeyBindingsButton } from "../../features/keyBindings/components/KeyBindingsButton"; +import { LoggerControl } from "../../features/workspace/components/LoggerControl"; +import WorkspaceSwitcher from "../../features/workspace/components/WorkspaceSwitcher"; import { useStore } from "../../store/store"; -import { KeyBindingsButton } from "../Testing/KeyBindings/KeyBindingsButton"; -import { LoggerControl } from "../Testing/LoggerControl"; -import WorkspaceSwitcher from "../Testing/WorkspaceSwitcher"; import { ModeBadge } from "./ModeBadge"; import { ReconnectButton } from "./ReconnectButton"; diff --git a/frontend/testing-view/src/components/Header/ModeBadge.tsx b/frontend/testing-view/src/components/header/ModeBadge.tsx similarity index 100% rename from frontend/testing-view/src/components/Header/ModeBadge.tsx rename to frontend/testing-view/src/components/header/ModeBadge.tsx diff --git a/frontend/testing-view/src/components/Header/ReconnectButton.tsx b/frontend/testing-view/src/components/header/ReconnectButton.tsx similarity index 100% rename from frontend/testing-view/src/components/Header/ReconnectButton.tsx rename to frontend/testing-view/src/components/header/ReconnectButton.tsx diff --git a/frontend/testing-view/src/components/LeftSidebar/AppSidebar.tsx b/frontend/testing-view/src/components/leftSidebar/AppSidebar.tsx similarity index 100% rename from frontend/testing-view/src/components/LeftSidebar/AppSidebar.tsx rename to frontend/testing-view/src/components/leftSidebar/AppSidebar.tsx diff --git a/frontend/testing-view/src/components/LeftSidebar/ColorSchemeToggle.tsx b/frontend/testing-view/src/components/leftSidebar/ColorSchemeToggle.tsx similarity index 100% rename from frontend/testing-view/src/components/LeftSidebar/ColorSchemeToggle.tsx rename to frontend/testing-view/src/components/leftSidebar/ColorSchemeToggle.tsx diff --git a/frontend/testing-view/src/components/LeftSidebar/ConnectionStatusGroup.tsx b/frontend/testing-view/src/components/leftSidebar/ConnectionStatusGroup.tsx similarity index 100% rename from frontend/testing-view/src/components/LeftSidebar/ConnectionStatusGroup.tsx rename to frontend/testing-view/src/components/leftSidebar/ConnectionStatusGroup.tsx diff --git a/frontend/testing-view/src/components/LeftSidebar/Logo.tsx b/frontend/testing-view/src/components/leftSidebar/Logo.tsx similarity index 100% rename from frontend/testing-view/src/components/LeftSidebar/Logo.tsx rename to frontend/testing-view/src/components/leftSidebar/Logo.tsx diff --git a/frontend/testing-view/src/components/LeftSidebar/NavigationGroup.tsx b/frontend/testing-view/src/components/leftSidebar/NavigationGroup.tsx similarity index 100% rename from frontend/testing-view/src/components/LeftSidebar/NavigationGroup.tsx rename to frontend/testing-view/src/components/leftSidebar/NavigationGroup.tsx diff --git a/frontend/testing-view/src/components/LeftSidebar/SettingsItem.tsx b/frontend/testing-view/src/components/leftSidebar/SettingsItem.tsx similarity index 100% rename from frontend/testing-view/src/components/LeftSidebar/SettingsItem.tsx rename to frontend/testing-view/src/components/leftSidebar/SettingsItem.tsx diff --git a/frontend/testing-view/src/components/LeftSidebar/ThemeToggleItem.tsx b/frontend/testing-view/src/components/leftSidebar/ThemeToggleItem.tsx similarity index 100% rename from frontend/testing-view/src/components/LeftSidebar/ThemeToggleItem.tsx rename to frontend/testing-view/src/components/leftSidebar/ThemeToggleItem.tsx diff --git a/frontend/testing-view/src/components/Settings/BooleanField.tsx b/frontend/testing-view/src/components/settings/BooleanField.tsx similarity index 100% rename from frontend/testing-view/src/components/Settings/BooleanField.tsx rename to frontend/testing-view/src/components/settings/BooleanField.tsx diff --git a/frontend/testing-view/src/components/Settings/MultiCheckboxField.tsx b/frontend/testing-view/src/components/settings/MultiCheckboxField.tsx similarity index 100% rename from frontend/testing-view/src/components/Settings/MultiCheckboxField.tsx rename to frontend/testing-view/src/components/settings/MultiCheckboxField.tsx diff --git a/frontend/testing-view/src/components/Settings/PathField.tsx b/frontend/testing-view/src/components/settings/PathField.tsx similarity index 100% rename from frontend/testing-view/src/components/Settings/PathField.tsx rename to frontend/testing-view/src/components/settings/PathField.tsx diff --git a/frontend/testing-view/src/components/Settings/SelectField.tsx b/frontend/testing-view/src/components/settings/SelectField.tsx similarity index 100% rename from frontend/testing-view/src/components/Settings/SelectField.tsx rename to frontend/testing-view/src/components/settings/SelectField.tsx diff --git a/frontend/testing-view/src/components/Settings/SettingsDialog.tsx b/frontend/testing-view/src/components/settings/SettingsDialog.tsx similarity index 100% rename from frontend/testing-view/src/components/Settings/SettingsDialog.tsx rename to frontend/testing-view/src/components/settings/SettingsDialog.tsx diff --git a/frontend/testing-view/src/components/Settings/SettingsForm.tsx b/frontend/testing-view/src/components/settings/SettingsForm.tsx similarity index 100% rename from frontend/testing-view/src/components/Settings/SettingsForm.tsx rename to frontend/testing-view/src/components/settings/SettingsForm.tsx diff --git a/frontend/testing-view/src/components/Settings/TextField.tsx b/frontend/testing-view/src/components/settings/TextField.tsx similarity index 70% rename from frontend/testing-view/src/components/Settings/TextField.tsx rename to frontend/testing-view/src/components/settings/TextField.tsx index ba14d6f5a..71969dfb6 100644 --- a/frontend/testing-view/src/components/Settings/TextField.tsx +++ b/frontend/testing-view/src/components/settings/TextField.tsx @@ -2,6 +2,11 @@ import { Input } from "@workspace/ui/components/shadcn/input"; import { Label } from "@workspace/ui/components/shadcn/label"; import type { FieldProps } from "../../types/common/settings"; +/** + * Text field component for the settings form.\ + * Note: it is also used for numeric values and converted later to the correct type.\ + * Why: because empty numeric input displays 0 and not an empty string. + */ export const TextField = ({ field, value, onChange }: FieldProps) => (
diff --git a/frontend/testing-view/src/components/Testing/Charts/ChartLegend.tsx b/frontend/testing-view/src/features/charts/components/ChartLegend.tsx similarity index 90% rename from frontend/testing-view/src/components/Testing/Charts/ChartLegend.tsx rename to frontend/testing-view/src/features/charts/components/ChartLegend.tsx index d7446e061..576c0d74a 100644 --- a/frontend/testing-view/src/components/Testing/Charts/ChartLegend.tsx +++ b/frontend/testing-view/src/features/charts/components/ChartLegend.tsx @@ -1,11 +1,11 @@ import { X } from "@workspace/ui/icons"; -import { COLORS } from "../../../constants/chartsColors"; -import type { VariableSeries } from "../../../types/workspace/charts"; +import { COLORS } from "../constants/chartsColors"; +import type { WorkspaceChartSeries } from "../types/charts"; import { ChartSettings } from "./ChartSettings"; interface ChartLegendProps { chartId: string; - series: VariableSeries[]; + series: WorkspaceChartSeries[]; disabledIndices: Set; onToggle: (index: number) => void; onRemove: (variable: string, index: number) => void; diff --git a/frontend/testing-view/src/components/Testing/Charts/ChartSettings.tsx b/frontend/testing-view/src/features/charts/components/ChartSettings.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/Charts/ChartSettings.tsx rename to frontend/testing-view/src/features/charts/components/ChartSettings.tsx diff --git a/frontend/testing-view/src/components/Testing/Charts/ChartSurface.tsx b/frontend/testing-view/src/features/charts/components/ChartSurface.tsx similarity index 98% rename from frontend/testing-view/src/components/Testing/Charts/ChartSurface.tsx rename to frontend/testing-view/src/features/charts/components/ChartSurface.tsx index f4a428b83..f251bbd73 100644 --- a/frontend/testing-view/src/components/Testing/Charts/ChartSurface.tsx +++ b/frontend/testing-view/src/features/charts/components/ChartSurface.tsx @@ -4,14 +4,14 @@ import { cn } from "@workspace/ui/lib"; import { memo, useEffect, useRef, useState } from "react"; import uPlot from "uplot"; import { useShallow } from "zustand/shallow"; -import { COLORS } from "../../../constants/chartsColors"; import { useStore } from "../../../store/store"; -import type { VariableSeries } from "../../../types/workspace/charts"; +import { COLORS } from "../constants/chartsColors"; +import type { WorkspaceChartSeries } from "../types/charts"; import { createTooltipPlugin } from "./tooltipPlugin"; interface ChartSurfaceProps { chartId: string; - series: VariableSeries[]; + series: WorkspaceChartSeries[]; disabledIndices: Set; } diff --git a/frontend/testing-view/src/components/Testing/Charts/ChartsGrid.tsx b/frontend/testing-view/src/features/charts/components/ChartsGrid.tsx similarity index 90% rename from frontend/testing-view/src/components/Testing/Charts/ChartsGrid.tsx rename to frontend/testing-view/src/features/charts/components/ChartsGrid.tsx index 0cd35e5d1..17c2b8deb 100644 --- a/frontend/testing-view/src/components/Testing/Charts/ChartsGrid.tsx +++ b/frontend/testing-view/src/features/charts/components/ChartsGrid.tsx @@ -1,6 +1,6 @@ import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable"; import { cn } from "@workspace/ui/lib"; -import type { WorkspaceChartConfig } from "../../../store/slices/workspacesSlice"; +import type { WorkspaceChartConfig } from "../types/charts"; import { SortableChart } from "./SortableChart"; interface ChartsGridProps { diff --git a/frontend/testing-view/src/components/Testing/Charts/SortableChart.tsx b/frontend/testing-view/src/features/charts/components/SortableChart.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/Charts/SortableChart.tsx rename to frontend/testing-view/src/features/charts/components/SortableChart.tsx diff --git a/frontend/testing-view/src/components/Testing/Charts/TelemetryChart.tsx b/frontend/testing-view/src/features/charts/components/TelemetryChart.tsx similarity index 83% rename from frontend/testing-view/src/components/Testing/Charts/TelemetryChart.tsx rename to frontend/testing-view/src/features/charts/components/TelemetryChart.tsx index e66a7e7d6..3e84b721c 100644 --- a/frontend/testing-view/src/components/Testing/Charts/TelemetryChart.tsx +++ b/frontend/testing-view/src/features/charts/components/TelemetryChart.tsx @@ -3,19 +3,30 @@ import { cn } from "@workspace/ui/lib/utils"; import { useState } from "react"; import "uplot/dist/uPlot.min.css"; import { useStore } from "../../../store/store"; -import type { VariableSeries } from "../../../types/workspace/charts"; +import type { WorkspaceChartSeries } from "../types/charts"; import { ChartLegend } from "./ChartLegend"; import { ChartSurface } from "./ChartSurface"; interface TelemetryChartProps { id: string; - series: VariableSeries[]; + series: WorkspaceChartSeries[]; isDragging: boolean; isOver?: boolean; dragAttributes?: any; dragListeners?: any; } +/** + * A draggable, interactive chart container for visualizing telemetry data. + * + * Features: + * - Data Visualization: Renders high-frequency data using `uPlot` via `ChartSurface`. + * - Displays a `ChartLegend` to toggle or remove individual data series. + * - Supports drag-and-drop reordering within the grid. + * + * It manages local state for "disabled series" (toggled off in legend but still in config), + * preventing data loss when users just want to temporarily hide a line. + */ export const TelemetryChart = ({ id, series, diff --git a/frontend/testing-view/src/components/Testing/Charts/tooltipPlugin.ts b/frontend/testing-view/src/features/charts/components/tooltipPlugin.ts similarity index 92% rename from frontend/testing-view/src/components/Testing/Charts/tooltipPlugin.ts rename to frontend/testing-view/src/features/charts/components/tooltipPlugin.ts index 802e42043..2390f70c2 100644 --- a/frontend/testing-view/src/components/Testing/Charts/tooltipPlugin.ts +++ b/frontend/testing-view/src/features/charts/components/tooltipPlugin.ts @@ -1,7 +1,7 @@ -import { COLORS } from "../../../constants/chartsColors"; -import type { VariableSeries } from "../../../types/workspace/charts"; +import { COLORS } from "../constants/chartsColors"; +import type { WorkspaceChartSeries } from "../types/charts"; -export const createTooltipPlugin = (series: VariableSeries[]) => { +export const createTooltipPlugin = (series: WorkspaceChartSeries[]) => { let tooltip: HTMLDivElement, header: HTMLDivElement, rows: HTMLDivElement[], diff --git a/frontend/testing-view/src/constants/chartsColors.ts b/frontend/testing-view/src/features/charts/constants/chartsColors.ts similarity index 100% rename from frontend/testing-view/src/constants/chartsColors.ts rename to frontend/testing-view/src/features/charts/constants/chartsColors.ts diff --git a/frontend/testing-view/src/hooks/useChartsConfiguration.ts b/frontend/testing-view/src/features/charts/hooks/useChartsConfiguration.ts similarity index 76% rename from frontend/testing-view/src/hooks/useChartsConfiguration.ts rename to frontend/testing-view/src/features/charts/hooks/useChartsConfiguration.ts index 1d2ddae67..27c65c5d0 100644 --- a/frontend/testing-view/src/hooks/useChartsConfiguration.ts +++ b/frontend/testing-view/src/features/charts/hooks/useChartsConfiguration.ts @@ -1,6 +1,6 @@ import { useEffect } from "react"; -import { MOCK_CHARTS } from "../mocks/chartsConfigurations"; -import { useStore } from "../store/store"; +import { MOCK_CHARTS } from "../../../mocks/chartsConfigurations"; +import { useStore } from "../../../store/store"; export function useChartsConfiguration() { const appMode = useStore((s) => s.appMode); diff --git a/frontend/testing-view/src/features/charts/store/chartsSlice.ts b/frontend/testing-view/src/features/charts/store/chartsSlice.ts new file mode 100644 index 000000000..aa1314c61 --- /dev/null +++ b/frontend/testing-view/src/features/charts/store/chartsSlice.ts @@ -0,0 +1,146 @@ +import type { StateCreator } from "zustand"; +import { EMPTY_ARRAY } from "../../../constants/emptyArray"; +import type { Store } from "../../../store/store"; +import type { + WorkspaceChartConfig, + WorkspaceChartSeries, +} from "../types/charts"; + +export interface ChartsSlice { + /** Map of WorkspaceID -> List of Charts */ + charts: Record; + + setCharts: (charts: Record) => void; + getActiveWorkspaceCharts: () => WorkspaceChartConfig[]; + + addChart: (workspaceId: string) => string; + removeChart: (workspaceId: string, chartId: string) => void; + reorderCharts: ( + workspaceId: string, + oldIndex: number, + newIndex: number, + ) => void; + + addSeriesToChart: ( + workspaceId: string, + chartId: string, + series: WorkspaceChartSeries, + ) => void; + removeSeriesFromChart: ( + workspaceId: string, + chartId: string, + variable: string, + ) => void; + + /** Sets new buffer size or history limit for a chart. */ + setChartHistoryLimit: ( + workspaceId: string, + chartId: string, + newHistoryLimit: number, + ) => void; +} + +export const createChartsSlice: StateCreator = ( + set, + get, +) => ({ + // Telemetry Charts + charts: { + "workspace-1": [], + "workspace-2": [], + "workspace-3": [], + }, + + setCharts: (charts) => set({ charts }), + + getActiveWorkspaceCharts: () => { + const id = get().getActiveWorkspaceId(); + if (!id) return EMPTY_ARRAY as WorkspaceChartConfig[]; + return get().charts[id] || EMPTY_ARRAY; + }, + + // Future-proofing Actions + addChart: (workspaceId) => { + const newChartId = crypto.randomUUID(); + set((state) => ({ + charts: { + ...state.charts, + [workspaceId]: [ + ...(state.charts[workspaceId] || []), + { id: newChartId, series: [], historyLimit: 200 }, + ], + }, + })); + return newChartId; + }, + + removeChart: (workspaceId, chartId) => + set((state) => ({ + charts: { + ...state.charts, + [workspaceId]: (state.charts[workspaceId] || []).filter( + (c) => c.id !== chartId, + ), + }, + })), + + clearCharts: () => { + const activeWorkspaceId = get().getActiveWorkspaceId(); + if (!activeWorkspaceId) return; + + set((state) => ({ + charts: { + ...state.charts, + [activeWorkspaceId]: [], + }, + })); + }, + + reorderCharts: (workspaceId, oldIndex, newIndex) => { + if (oldIndex < 0 || newIndex < 0) return; + + set((state) => { + const charts = [...(state.charts[workspaceId] || [])]; + const [removed] = charts.splice(oldIndex, 1); + charts.splice(newIndex, 0, removed); + return { + charts: { + ...state.charts, + [workspaceId]: charts, + }, + }; + }); + }, + + addSeriesToChart: (workspaceId, chartId, series) => + set((state) => ({ + charts: { + ...state.charts, + [workspaceId]: (state.charts[workspaceId] || []).map((c) => + c.id === chartId ? { ...c, series: [...c.series, series] } : c, + ), + }, + })), + + removeSeriesFromChart: (workspaceId, chartId, variable) => + set((state) => ({ + charts: { + ...state.charts, + [workspaceId]: (state.charts[workspaceId] || []).map((c) => + c.id === chartId + ? { ...c, series: c.series.filter((s) => s.variable !== variable) } + : c, + ), + }, + })), + + setChartHistoryLimit: (workspaceId, chartId, newHistoryLimit) => + set((state) => ({ + charts: { + ...state.charts, + [workspaceId]: (state.charts[workspaceId] || []).map((c) => + c.id === chartId ? { ...c, historyLimit: newHistoryLimit } : c, + ), + }, + })), +}); diff --git a/frontend/testing-view/src/features/charts/types/charts.ts b/frontend/testing-view/src/features/charts/types/charts.ts new file mode 100644 index 000000000..b1277bda0 --- /dev/null +++ b/frontend/testing-view/src/features/charts/types/charts.ts @@ -0,0 +1,23 @@ +/** + * Workspace chart series. Chosen "lines" to be showed. + */ +export interface WorkspaceChartSeries { + packetId: number; + variable: string; +} + +/** + * Workspace chart configuration. + */ +export interface WorkspaceChartConfig { + id: string; + series: WorkspaceChartSeries[]; + /** Last n points to be kept in memory. The same thing as buffer size of the chart. */ + historyLimit: number; +} + +/** + * Checkbox state type.\ + * Indeterminate state is used when some items under the group are checked, but not all. + */ +export type CheckboxState = boolean | "indeterminate"; diff --git a/frontend/testing-view/src/components/Testing/Filters/FilterCategoryItem.tsx b/frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/Filters/FilterCategoryItem.tsx rename to frontend/testing-view/src/features/filtering/components/FilterCategoryItem.tsx diff --git a/frontend/testing-view/src/components/Testing/Filters/FilterController.tsx b/frontend/testing-view/src/features/filtering/components/FilterController.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/Filters/FilterController.tsx rename to frontend/testing-view/src/features/filtering/components/FilterController.tsx diff --git a/frontend/testing-view/src/components/Testing/Filters/FilterDialog.tsx b/frontend/testing-view/src/features/filtering/components/FilterDialog.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/Filters/FilterDialog.tsx rename to frontend/testing-view/src/features/filtering/components/FilterDialog.tsx diff --git a/frontend/testing-view/src/components/Testing/Filters/FilterItem.tsx b/frontend/testing-view/src/features/filtering/components/FilterItem.tsx similarity index 89% rename from frontend/testing-view/src/components/Testing/Filters/FilterItem.tsx rename to frontend/testing-view/src/features/filtering/components/FilterItem.tsx index 9c949f0a5..c5e4dac6e 100644 --- a/frontend/testing-view/src/components/Testing/Filters/FilterItem.tsx +++ b/frontend/testing-view/src/features/filtering/components/FilterItem.tsx @@ -1,8 +1,8 @@ import { Badge, Checkbox } from "@workspace/ui"; -import type { Item } from "../../../types/common/item"; +import type { CatalogItem } from "../../../types/common/item"; interface FilterItemProps { - item: Item; + item: CatalogItem; isChecked: boolean; onToggle: () => void; } diff --git a/frontend/testing-view/src/features/filtering/store/filteringSlice.ts b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts new file mode 100644 index 000000000..1bcc4cc6d --- /dev/null +++ b/frontend/testing-view/src/features/filtering/store/filteringSlice.ts @@ -0,0 +1,368 @@ +import type { StateCreator } from "zustand"; +import { + createEmptyFilter, + createFullFilter, + generateInitialFilters, + getCatalogKey, +} from "../../../lib/utils"; +import type { Store } from "../../../store/store"; +import type { CatalogItem } from "../../../types/common/item"; +import type { BoardName } from "../../../types/data/board"; +import type { Variable } from "../../../types/data/telemetryCatalogItem"; +import type { VirtualRow } from "../../../types/data/virtualization"; +import type { CheckboxState } from "../../charts/types/charts"; +import type { + SidebarTab, + WorkspaceExpandedItems, +} from "../../workspace/types/sidebar"; +import type { + FilterScope, + TabFilter, + WorkspaceFilters, +} from "../types/filters"; + +export interface FilteringSlice { + /** Sidebar Navigation */ + activeTab: Record; + getActiveTab: () => SidebarTab; + setActiveTab: (tab: SidebarTab) => void; + + filterDialog: { + isOpen: boolean; + scope: FilterScope | null; + }; + openFilterDialog: (scope: FilterScope) => void; + closeFilterDialog: () => void; + + /** Filter State */ + workspaceFilters: Record; + initializeWorkspaceFilters: () => void; + updateFilters: (scope: FilterScope, filters: TabFilter) => void; + getActiveFilters: (scope: FilterScope) => TabFilter | undefined; + + /** Filter Actions */ + selectAllFilters: (scope: FilterScope) => void; + clearFilters: (scope: FilterScope) => void; + toggleCategoryFilter: ( + scope: FilterScope, + category: BoardName, + checked: boolean, + ) => void; + toggleItemFilter: ( + scope: FilterScope, + category: BoardName, + id: number, + ) => void; + + /** Filter Queries */ + getCatalog: (scope: FilterScope) => Record; + getFilteredItems: (scope: FilterScope) => CatalogItem[]; + getFilteredItemsIds: (scope: FilterScope) => number[]; + getFilteredItemsIdsByCategory: ( + scope: FilterScope, + category: BoardName, + ) => number[]; + getFilteredItemsByCategory: ( + scope: FilterScope, + category: BoardName, + ) => CatalogItem[]; + + getFilteredCount: (scope: FilterScope) => number; + getFilteredCountByCategory: ( + scope: FilterScope, + category: BoardName, + ) => number; + getTotalCount: (scope: FilterScope) => number; + getSelectionState: (scope: FilterScope, category: BoardName) => CheckboxState; + + /** Virtualization & Expansion */ + expandedItems: Record; + isItemExpanded: ( + scope: SidebarTab, + type: string, + itemId: number | string, + ) => boolean; + toggleExpandedItem: ( + scope: SidebarTab, + type: string, + itemId: number | string, + ) => void; + getActiveExpanded: (scope: FilterScope) => Set | undefined; + getFlattenedRows: ( + scope: SidebarTab, + categories: readonly BoardName[], + ) => VirtualRow[]; +} + +export const createFilteringSlice: StateCreator< + Store, + [], + [], + FilteringSlice +> = (set, get) => ({ + // Tabs (per workspace) + activeTab: {}, + getActiveTab: () => { + const activeWorkspaceId = get().getActiveWorkspaceId(); + if (!activeWorkspaceId) return "commands"; + return get().activeTab[activeWorkspaceId] || "commands"; + }, + setActiveTab: (tab) => { + const activeWorkspaceId = get().getActiveWorkspaceId(); + if (!activeWorkspaceId) return; + + set((state) => ({ + activeTab: { ...state.activeTab, [activeWorkspaceId]: tab }, + })); + }, + + openFilterDialog: (scope: FilterScope) => + set({ filterDialog: { isOpen: true, scope } }), + closeFilterDialog: () => + set({ filterDialog: { isOpen: false, scope: null } }), + + // Internal helpers + getCatalog: (scope: FilterScope) => { + const catalogKey = getCatalogKey(scope); + if (!catalogKey) return {}; + + return get()[catalogKey]; + }, + + toggleItemFilter: (scope, category, itemId) => { + const workspaceId = get().getActiveWorkspaceId(); + if (!workspaceId) return; + + const currentWorkspaceFilters = get().workspaceFilters[workspaceId] || {}; + const currentTabFilter = + currentWorkspaceFilters[scope] || createEmptyFilter(); + + const currentCategoryIds = currentTabFilter[category] || []; + + const isSelected = currentCategoryIds.includes(itemId); + const newCategoryIds = isSelected + ? currentCategoryIds.filter((id) => id !== itemId) + : [...currentCategoryIds, itemId]; + + get().updateFilters(scope, { + ...currentTabFilter, + [category]: newCategoryIds, + }); + }, + + // Filter actions + selectAllFilters: (scope) => { + const workspaceId = get().getActiveWorkspaceId(); + if (!workspaceId) return; + + const items = get().getCatalog(scope); + + const fullFilter = createFullFilter(items); + get().updateFilters(scope, fullFilter); + }, + clearFilters: (scope) => { + const workspaceId = get().getActiveWorkspaceId(); + if (!workspaceId) return; + const emptyFilter = createEmptyFilter(); + get().updateFilters(scope, emptyFilter); + }, + toggleCategoryFilter: (scope, category, checked) => { + const workspaceId = get().getActiveWorkspaceId(); + if (!workspaceId) return; + + const catalog = get().getCatalog(scope); + + const currentFilters = + get().workspaceFilters[workspaceId]?.[scope] || createEmptyFilter(); + + const newItems = checked + ? catalog?.[category]?.map((item) => item.id) || [] + : []; + + get().updateFilters(scope, { + ...currentFilters, + [category]: newItems, + }); + }, + + workspaceFilters: {}, + initializeWorkspaceFilters: () => { + const commands = get().commandsCatalog; + const telemetry = get().telemetryCatalog; + + const currentFilters = get().workspaceFilters; + + // Only initialize if filters are empty (not persisted) + if (Object.keys(currentFilters).length === 0) { + set({ + workspaceFilters: generateInitialFilters({ + commands: createFullFilter(commands), + telemetry: createFullFilter(telemetry), + logs: createFullFilter(telemetry), + }), + }); + } + }, + updateFilters: (scope, filters) => { + const workspaceId = get().getActiveWorkspaceId(); + if (!workspaceId) return; + + set((state) => ({ + workspaceFilters: { + ...state.workspaceFilters, + [workspaceId]: { + ...(state.workspaceFilters[workspaceId] || {}), + [scope]: filters, + }, + }, + })); + }, + + getFilteredItemsByCategory: (scope, category) => { + const selected = get().getFilteredItemsIdsByCategory(scope, category); + const catalog = get().getCatalog(scope); + const items = catalog?.[category] || []; + return items.filter((i) => selected.includes(i.id)); + }, + + // Helper getters + getActiveFilters: (scope) => { + const id = get().getActiveWorkspaceId(); + return id ? get().workspaceFilters[id]?.[scope] : undefined; + }, + + // Expanded items + expandedItems: {}, + isItemExpanded: (scope, type, itemId) => { + const activeWorkspaceId = get().getActiveWorkspaceId(); + if (!activeWorkspaceId) return false; + + const expandedItems = get().expandedItems[activeWorkspaceId]?.[scope]; + if (!expandedItems) return false; + + return expandedItems.has(`${type}:${itemId}`); + }, + toggleExpandedItem: (scope, type, itemId) => { + const activeWorkspaceId = get().getActiveWorkspaceId(); + if (!activeWorkspaceId) return; + + set((state) => { + const expandedItems = + state.expandedItems[activeWorkspaceId]?.[scope] || new Set(); + const newExpandedItems = new Set(expandedItems); + + if (newExpandedItems.has(`${type}:${itemId}`)) { + newExpandedItems.delete(`${type}:${itemId}`); + } else { + newExpandedItems.add(`${type}:${itemId}`); + } + + return { + expandedItems: { + ...state.expandedItems, + [activeWorkspaceId]: { + ...state.expandedItems[activeWorkspaceId], + [scope]: newExpandedItems, + }, + }, + }; + }); + }, + getFlattenedRows: (scope, categories) => { + const rows: VirtualRow[] = []; + + categories.forEach((category) => { + const items = get().getFilteredItemsByCategory(scope, category); + if (items.length === 0) return; + + // Add the Header (Board) + rows.push({ + type: "board", + id: category, + label: category, + count: items.length, + }); + + // If the board is expanded, add its packets + if (get().isItemExpanded(scope, "board", category)) { + items.forEach((item) => { + rows.push({ + type: "packet", + id: item.id, + data: item, + }); + + // If the packet is expanded, add its variables/measurements + if (get().isItemExpanded(scope, "packet", item.id)) { + if ("measurements" in item) { + const variables = item.measurements as Variable[]; + variables.forEach((m) => { + rows.push({ + type: "variable", + id: `${item.id}-${m.id}`, + data: m, + packetId: item.id, + }); + }); + } + } + }); + } + }); + + return rows; + }, + // Filter dialog + filterDialog: { + isOpen: false, + scope: null, + }, + + getActiveExpanded: (scope) => { + const id = get().getActiveWorkspaceId(); + return id ? get().expandedItems[id]?.[scope] : undefined; + }, + + // Getters for filtered items + getFilteredItemsIds: (scope) => { + const filters = get().getActiveFilters(scope); + return filters ? Object.values(filters).flat() : []; + }, + + getFilteredItemsIdsByCategory: (scope, category) => { + return get().getActiveFilters(scope)?.[category] || []; + }, + getFilteredItems: (scope) => { + const filters = get().getActiveFilters(scope); + if (!filters) return []; + + const catalog = get().getCatalog(scope); + if (!catalog) return []; + + return Object.entries(catalog).flatMap(([cat, items]) => { + const selected = filters[cat as BoardName] || []; + return items.filter((i) => selected.includes(i.id)); + }); + }, + + // Stats getters + getFilteredCount: (scope) => get().getFilteredItemsIds(scope).length, + + getFilteredCountByCategory: (scope, category) => + get().getFilteredItemsIdsByCategory(scope, category).length, + + getTotalCount: (scope) => { + const catalog = get().getCatalog(scope); + return Object.values(catalog).reduce((acc, items) => acc + items.length, 0); + }, + + getSelectionState: (scope, category) => { + const selectedCount = get().getFilteredCountByCategory(scope, category); + const catalog = get().getCatalog(scope); + const totalItems = catalog?.[category]?.length || 0; + + if (totalItems === 0 || selectedCount === 0) return false; + if (selectedCount === totalItems) return true; + return "indeterminate"; + }, +}); diff --git a/frontend/testing-view/src/features/filtering/types/filters.ts b/frontend/testing-view/src/features/filtering/types/filters.ts new file mode 100644 index 000000000..3817a0ad7 --- /dev/null +++ b/frontend/testing-view/src/features/filtering/types/filters.ts @@ -0,0 +1,18 @@ +import type { BoardName } from "../../../types/data/board"; +import type { SidebarTab } from "../../workspace/types/sidebar"; + +/** + * Filter scope identifiers. + * **Be careful** don't confuse this one with sidebar tabs. + */ +export type FilterScope = SidebarTab | "logs"; + +/** + * Record of filtered items ids per board. + */ +export type TabFilter = Record; + +/** + * Record of filtered items ids per filter scope. + */ +export type WorkspaceFilters = Record; diff --git a/frontend/testing-view/src/components/Testing/KeyBindings/AddKeyBindingDialog.tsx b/frontend/testing-view/src/features/keyBindings/components/AddKeyBindingDialog.tsx similarity index 97% rename from frontend/testing-view/src/components/Testing/KeyBindings/AddKeyBindingDialog.tsx rename to frontend/testing-view/src/features/keyBindings/components/AddKeyBindingDialog.tsx index efe35962b..cd4d633ed 100644 --- a/frontend/testing-view/src/components/Testing/KeyBindings/AddKeyBindingDialog.tsx +++ b/frontend/testing-view/src/features/keyBindings/components/AddKeyBindingDialog.tsx @@ -17,11 +17,11 @@ import { } from "@workspace/ui"; import { X } from "@workspace/ui/icons"; import { useEffect, useRef, useState } from "react"; -import { SPECIAL_KEY_BINDINGS } from "../../../constants/specialKeyBindings"; import { getDefaultParameterValues } from "../../../lib/commandUtils"; import { useStore } from "../../../store/store"; import type { CommandCatalogItem } from "../../../types/data/commandCatalogItem"; -import { CommandParameters } from "../RightSidebar/Tabs/Commands/CommandParameters"; +import { CommandParameters } from "../../workspace/components/rightSidebar/tabs/commands/CommandParameters"; +import { SPECIAL_KEY_BINDINGS } from "../constants/specialKeyBindings"; interface AddKeyBindingDialogProps { open: boolean; diff --git a/frontend/testing-view/src/components/Testing/KeyBindings/KeyBindingCard.tsx b/frontend/testing-view/src/features/keyBindings/components/KeyBindingCard.tsx similarity index 97% rename from frontend/testing-view/src/components/Testing/KeyBindings/KeyBindingCard.tsx rename to frontend/testing-view/src/features/keyBindings/components/KeyBindingCard.tsx index 23c1f1070..68d1e5596 100644 --- a/frontend/testing-view/src/components/Testing/KeyBindings/KeyBindingCard.tsx +++ b/frontend/testing-view/src/features/keyBindings/components/KeyBindingCard.tsx @@ -3,7 +3,7 @@ import { ChevronDown, X } from "@workspace/ui/icons"; import { cn } from "@workspace/ui/lib"; import { useState } from "react"; import { useStore } from "../../../store/store"; -import type { KeyBinding } from "../../../types/workspace/keyBinding"; +import type { KeyBinding } from "../types/keyBinding"; interface KeyBindingCardProps { binding: KeyBinding; diff --git a/frontend/testing-view/src/components/Testing/KeyBindings/KeyBindingsButton.tsx b/frontend/testing-view/src/features/keyBindings/components/KeyBindingsButton.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/KeyBindings/KeyBindingsButton.tsx rename to frontend/testing-view/src/features/keyBindings/components/KeyBindingsButton.tsx diff --git a/frontend/testing-view/src/components/Testing/KeyBindings/KeyBindingsDialog.tsx b/frontend/testing-view/src/features/keyBindings/components/KeyBindingsDialog.tsx similarity index 95% rename from frontend/testing-view/src/components/Testing/KeyBindings/KeyBindingsDialog.tsx rename to frontend/testing-view/src/features/keyBindings/components/KeyBindingsDialog.tsx index 1565988ea..66f5a5e56 100644 --- a/frontend/testing-view/src/components/Testing/KeyBindings/KeyBindingsDialog.tsx +++ b/frontend/testing-view/src/features/keyBindings/components/KeyBindingsDialog.tsx @@ -10,11 +10,10 @@ import { Plus } from "@workspace/ui/icons"; import { useState } from "react"; import { useShallow } from "zustand/shallow"; import { useStore } from "../../../store/store"; -import type { KeyBinding } from "../../../types/workspace/keyBinding"; +import type { KeyBinding } from "../types/keyBinding"; import { AddKeyBindingDialog } from "./AddKeyBindingDialog"; import { KeyBindingCard } from "./KeyBindingCard"; import { OrphanedKeyBindingCard } from "./OrphanedKeyBindingCard"; -// import { AddKeyBindingDialog } from "./AddKeyBindingDialog"; interface KeyBindingsDialogProps { open: boolean; diff --git a/frontend/testing-view/src/components/Testing/KeyBindings/OrphanedKeyBindingCard.tsx b/frontend/testing-view/src/features/keyBindings/components/OrphanedKeyBindingCard.tsx similarity index 95% rename from frontend/testing-view/src/components/Testing/KeyBindings/OrphanedKeyBindingCard.tsx rename to frontend/testing-view/src/features/keyBindings/components/OrphanedKeyBindingCard.tsx index ceb8a4040..7afcbff84 100644 --- a/frontend/testing-view/src/components/Testing/KeyBindings/OrphanedKeyBindingCard.tsx +++ b/frontend/testing-view/src/features/keyBindings/components/OrphanedKeyBindingCard.tsx @@ -1,7 +1,7 @@ import { Badge, Button } from "@workspace/ui"; import { AlertTriangle, X } from "@workspace/ui/icons"; import { useStore } from "../../../store/store"; -import type { KeyBinding } from "../../../types/workspace/keyBinding"; +import type { KeyBinding } from "../types/keyBinding"; interface OrphanedKeyBindingCardProps { binding: KeyBinding; diff --git a/frontend/testing-view/src/constants/specialKeyBindings.ts b/frontend/testing-view/src/features/keyBindings/constants/specialKeyBindings.ts similarity index 100% rename from frontend/testing-view/src/constants/specialKeyBindings.ts rename to frontend/testing-view/src/features/keyBindings/constants/specialKeyBindings.ts diff --git a/frontend/testing-view/src/hooks/useGlobalKeyBindings.ts b/frontend/testing-view/src/features/keyBindings/hooks/useGlobalKeyBindings.ts similarity index 94% rename from frontend/testing-view/src/hooks/useGlobalKeyBindings.ts rename to frontend/testing-view/src/features/keyBindings/hooks/useGlobalKeyBindings.ts index dc534d8bd..e942868a6 100644 --- a/frontend/testing-view/src/hooks/useGlobalKeyBindings.ts +++ b/frontend/testing-view/src/features/keyBindings/hooks/useGlobalKeyBindings.ts @@ -1,9 +1,9 @@ import { logger, socketService } from "@workspace/core"; import { useEffect } from "react"; +import { getDefaultParameterValues } from "../../../lib/commandUtils"; +import { useStore } from "../../../store/store"; +import type { CommandCatalogItem } from "../../../types/data/commandCatalogItem"; import { SPECIAL_KEY_BINDINGS } from "../constants/specialKeyBindings"; -import { getDefaultParameterValues } from "../lib/commandUtils"; -import { useStore } from "../store/store"; -import type { CommandCatalogItem } from "../types/data/commandCatalogItem"; export const useGlobalKeyBindings = () => { const getKeyBindings = useStore((s) => s.getKeyBindings); diff --git a/frontend/testing-view/src/features/keyBindings/types/keyBinding.ts b/frontend/testing-view/src/features/keyBindings/types/keyBinding.ts new file mode 100644 index 000000000..8458855ef --- /dev/null +++ b/frontend/testing-view/src/features/keyBindings/types/keyBinding.ts @@ -0,0 +1,13 @@ +/** + * Key binding definition. + */ +export interface KeyBinding { + /** UUID for this binding */ + id: string; + /** The command's ID */ + commandId: number; + /** The key (e.g., "1", "a") */ + key: string; + /** The parameters for this binding executed with the command */ + parameters: Record; +} diff --git a/frontend/testing-view/src/components/Testing/DndOverlay.tsx b/frontend/testing-view/src/features/workspace/components/DndOverlay.tsx similarity index 85% rename from frontend/testing-view/src/components/Testing/DndOverlay.tsx rename to frontend/testing-view/src/features/workspace/components/DndOverlay.tsx index 345efdc29..16ebaab93 100644 --- a/frontend/testing-view/src/components/Testing/DndOverlay.tsx +++ b/frontend/testing-view/src/features/workspace/components/DndOverlay.tsx @@ -1,8 +1,8 @@ import { DragOverlay } from "@dnd-kit/core"; import { Badge } from "@workspace/ui"; -import type { WorkspaceChartConfig } from "../../store/slices/workspacesSlice"; -import type { DndActiveData } from "../../types/app/dndData"; -import { TelemetryChart } from "./Charts/TelemetryChart"; +import { TelemetryChart } from "../../charts/components/TelemetryChart"; +import type { WorkspaceChartConfig } from "../../charts/types/charts"; +import type { DndActiveData } from "../types/dndData"; interface DndOverlayProps { activeDragData: DndActiveData | null; diff --git a/frontend/testing-view/src/components/Testing/EmptyWorkspace.tsx b/frontend/testing-view/src/features/workspace/components/EmptyWorkspace.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/EmptyWorkspace.tsx rename to frontend/testing-view/src/features/workspace/components/EmptyWorkspace.tsx diff --git a/frontend/testing-view/src/components/Testing/LoggerControl.tsx b/frontend/testing-view/src/features/workspace/components/LoggerControl.tsx similarity index 88% rename from frontend/testing-view/src/components/Testing/LoggerControl.tsx rename to frontend/testing-view/src/features/workspace/components/LoggerControl.tsx index 89862ed34..c4ac6d891 100644 --- a/frontend/testing-view/src/components/Testing/LoggerControl.tsx +++ b/frontend/testing-view/src/features/workspace/components/LoggerControl.tsx @@ -1,12 +1,12 @@ import { Button, Separator } from "@workspace/ui"; import { Settings2 } from "@workspace/ui/icons"; import { cn } from "@workspace/ui/lib"; -import { LOGGER_CONTROL_CONFIG } from "../../constants/loggerControlConfig"; -import useConfirmClose from "../../hooks/useConfirmClose"; -import { useLogger } from "../../hooks/useLogger"; -import { useStore } from "../../store/store"; -import type { BoardName } from "../../types/data/board"; -import type { TelemetryCatalogItem } from "../../types/data/telemetryCatalogItem"; +import { LOGGER_CONTROL_CONFIG } from "../../../constants/loggerControlConfig"; +import useConfirmClose from "../../../hooks/useConfirmClose"; +import { useLogger } from "../../../hooks/useLogger"; +import { useStore } from "../../../store/store"; +import type { BoardName } from "../../../types/data/board"; +import type { TelemetryCatalogItem } from "../../../types/data/telemetryCatalogItem"; interface LoggerControlProps { disabled: boolean; diff --git a/frontend/testing-view/src/components/Testing/MainPanel.tsx b/frontend/testing-view/src/features/workspace/components/MainPanel.tsx similarity index 90% rename from frontend/testing-view/src/components/Testing/MainPanel.tsx rename to frontend/testing-view/src/features/workspace/components/MainPanel.tsx index f0e0239ca..9c4f2caa5 100644 --- a/frontend/testing-view/src/components/Testing/MainPanel.tsx +++ b/frontend/testing-view/src/features/workspace/components/MainPanel.tsx @@ -1,8 +1,8 @@ import { useDroppable } from "@dnd-kit/core"; import { cn } from "@workspace/ui/lib"; -import { useStore } from "../../store/store"; -import type { DndActiveData } from "../../types/app/dndData"; -import { ChartsGrid } from "./Charts/ChartsGrid"; +import { useStore } from "../../../store/store"; +import { ChartsGrid } from "../../charts/components/ChartsGrid"; +import type { DndActiveData } from "../types/dndData"; import { EmptyWorkspace } from "./EmptyWorkspace"; import { TestingToolbar } from "./Toolbar"; diff --git a/frontend/testing-view/src/components/Testing/Toolbar.tsx b/frontend/testing-view/src/features/workspace/components/Toolbar.tsx similarity index 97% rename from frontend/testing-view/src/components/Testing/Toolbar.tsx rename to frontend/testing-view/src/features/workspace/components/Toolbar.tsx index 4aaaa6fc2..872c4f692 100644 --- a/frontend/testing-view/src/components/Testing/Toolbar.tsx +++ b/frontend/testing-view/src/features/workspace/components/Toolbar.tsx @@ -1,6 +1,6 @@ import { Button } from "@workspace/ui"; import { ChevronLeft, Plus } from "@workspace/ui/icons"; -import { useStore } from "../../store/store"; +import { useStore } from "../../../store/store"; interface TestingToolbarProps { columns: number; diff --git a/frontend/testing-view/src/components/Testing/WorkspaceDialog.tsx b/frontend/testing-view/src/features/workspace/components/WorkspaceDialog.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/WorkspaceDialog.tsx rename to frontend/testing-view/src/features/workspace/components/WorkspaceDialog.tsx diff --git a/frontend/testing-view/src/components/Testing/WorkspaceSwitcher.tsx b/frontend/testing-view/src/features/workspace/components/WorkspaceSwitcher.tsx similarity index 98% rename from frontend/testing-view/src/components/Testing/WorkspaceSwitcher.tsx rename to frontend/testing-view/src/features/workspace/components/WorkspaceSwitcher.tsx index 1f097aad1..6a1a90607 100644 --- a/frontend/testing-view/src/components/Testing/WorkspaceSwitcher.tsx +++ b/frontend/testing-view/src/features/workspace/components/WorkspaceSwitcher.tsx @@ -18,8 +18,8 @@ import { } from "@workspace/ui/icons"; import { cn } from "@workspace/ui/lib"; import { useState } from "react"; -import { useStore } from "../../store/store"; -import type { Workspace } from "../../types/workspace/workspace"; +import { useStore } from "../../../store/store"; +import type { Workspace } from "../types/workspace"; import { WorkspaceDialog } from "./WorkspaceDialog"; interface WorkspaceSwitcherProps { diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/RightSidebar.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/RightSidebar.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/RightSidebar/RightSidebar.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/RightSidebar.tsx diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/RightSidebarContent.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/RightSidebarContent.tsx similarity index 88% rename from frontend/testing-view/src/components/Testing/RightSidebar/RightSidebarContent.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/RightSidebarContent.tsx index e3d474f23..647240281 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/RightSidebarContent.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/RightSidebarContent.tsx @@ -4,10 +4,10 @@ import { ResizablePanelGroup, } from "@workspace/ui"; import { Activity } from "react"; -import { useStore } from "../../../store/store"; -import MessagesSection from "./Sections/MessagesSection"; -import { NoneSelectedSection } from "./Sections/NoneSelectedSection"; -import TabsSection from "./Sections/TabsSection"; +import { useStore } from "../../../../store/store"; +import MessagesSection from "./sections/MessagesSection"; +import { NoneSelectedSection } from "./sections/NoneSelectedSection"; +import TabsSection from "./sections/TabsSection"; interface RightSidebarContentProps {} diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/RightSidebarHeader.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/RightSidebarHeader.tsx similarity index 98% rename from frontend/testing-view/src/components/Testing/RightSidebar/RightSidebarHeader.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/RightSidebarHeader.tsx index 786b51198..a4bf54257 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/RightSidebarHeader.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/RightSidebarHeader.tsx @@ -13,7 +13,7 @@ import { PanelRight, X, } from "@workspace/ui/icons"; -import { useStore } from "../../../store/store"; +import { useStore } from "../../../../store/store"; interface RightSidebarHeaderProps { onClose: () => void; diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Sections/MessageItem.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/MessageItem.tsx similarity index 81% rename from frontend/testing-view/src/components/Testing/RightSidebar/Sections/MessageItem.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/sections/MessageItem.tsx index 8a7ffa8a1..4b5ccbcb9 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Sections/MessageItem.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/MessageItem.tsx @@ -7,9 +7,9 @@ import { import { ChevronDown } from "@workspace/ui/icons"; import { cn } from "@workspace/ui/lib"; import { useState } from "react"; -import { MESSAGE_KIND_COLORS } from "../../../../constants/messageKindColors"; -import { formatTimestamp } from "../../../../lib/utils"; -import type { Message } from "../../../../types/data/message"; +import { MESSAGE_KIND_COLORS } from "../../../../../constants/messageKindColors"; +import { formatTimestamp } from "../../../../../lib/utils"; +import type { Message } from "../../../../../types/data/message"; interface MessageItemProps { message: Message; @@ -82,12 +82,14 @@ export const MessageItem = ({ message }: MessageItemProps) => {
e.stopPropagation()}>
- {/* If it's a protection message, showing the sub-kind can be helpful */} - {message.payload?.kind && ( - - {message.payload.kind.replace("_", " ")} - - )} + {/* If it's a detailed message, showing the sub-kind can be helpful */} + {message.payload && + typeof message.payload === "object" && + message.payload.kind && ( + + {message.payload.kind.replace("_", " ")} + + )}

{renderMessageContent(message.payload)}

diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Sections/MessagesList.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/MessagesList.tsx similarity index 91% rename from frontend/testing-view/src/components/Testing/RightSidebar/Sections/MessagesList.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/sections/MessagesList.tsx index 8b3790075..cbc53dce3 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Sections/MessagesList.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/MessagesList.tsx @@ -1,4 +1,4 @@ -import { useStore } from "../../../../store/store"; +import { useStore } from "../../../../../store/store"; import { MessageItem } from "./MessageItem"; export const MessagesList = () => { diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Sections/MessagesSection.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/MessagesSection.tsx similarity index 95% rename from frontend/testing-view/src/components/Testing/RightSidebar/Sections/MessagesSection.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/sections/MessagesSection.tsx index 8eae514a2..5343bb876 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Sections/MessagesSection.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/MessagesSection.tsx @@ -1,6 +1,6 @@ import { Button } from "@workspace/ui"; import { Trash2 } from "@workspace/ui/icons"; -import { useStore } from "../../../../store/store"; +import { useStore } from "../../../../../store/store"; import { MessagesList } from "./MessagesList"; const MessagesSection = () => { diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Sections/NoneSelectedSection.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/NoneSelectedSection.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/RightSidebar/Sections/NoneSelectedSection.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/sections/NoneSelectedSection.tsx diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Sections/TabsSection.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TabsSection.tsx similarity index 85% rename from frontend/testing-view/src/components/Testing/RightSidebar/Sections/TabsSection.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TabsSection.tsx index 016784fbb..63e86e18e 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Sections/TabsSection.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/sections/TabsSection.tsx @@ -7,14 +7,14 @@ import { TabsList, TabsTrigger, } from "@workspace/ui"; -import { BOARD_NAMES } from "../../../../constants/boards"; -import { useStore } from "../../../../store/store"; -import type { CommandCatalogItem } from "../../../../types/data/commandCatalogItem"; -import type { TelemetryCatalogItem } from "../../../../types/data/telemetryCatalogItem"; -import type { SidebarTab } from "../../../../types/workspace/sidebar"; -import { CommandItem } from "../Tabs/Commands/CommandItem"; -import { Tab } from "../Tabs/Tab"; -import { TelemetryItem } from "../Tabs/Telemetry/TelemetryItem"; +import { BOARD_NAMES } from "../../../../../constants/boards"; +import { useStore } from "../../../../../store/store"; +import type { CommandCatalogItem } from "../../../../../types/data/commandCatalogItem"; +import type { TelemetryCatalogItem } from "../../../../../types/data/telemetryCatalogItem"; +import type { SidebarTab } from "../../../types/sidebar"; +import { CommandItem } from "../tabs/commands/CommandItem"; +import { Tab } from "../tabs/Tab"; +import { TelemetryItem } from "../tabs/telemetry/TelemetryItem"; interface TabsSectionProps { isSplit: boolean; diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/CategoryHeader.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/CategoryHeader.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/CategoryHeader.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/CategoryHeader.tsx diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/CategoryItem.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/CategoryItem.tsx similarity index 80% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/CategoryItem.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/CategoryItem.tsx index fb7560dc8..3622797d8 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/CategoryItem.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/CategoryItem.tsx @@ -1,16 +1,16 @@ import { Collapsible, CollapsibleContent } from "@workspace/ui"; import { type ComponentType } from "react"; import { useShallow } from "zustand/shallow"; -import { useStore } from "../../../../store/store"; -import type { Item } from "../../../../types/common/item"; -import type { BoardName } from "../../../../types/data/board"; -import type { SidebarTab } from "../../../../types/workspace/sidebar"; +import { useStore } from "../../../../../store/store"; +import type { CatalogItem } from "../../../../../types/common/item"; +import type { BoardName } from "../../../../../types/data/board"; +import type { SidebarTab } from "../../../types/sidebar"; import { CategoryHeader } from "./CategoryHeader"; interface CategoryItemProps { category: BoardName; scope: SidebarTab; - ItemComponent: ComponentType<{ item: Item }>; + ItemComponent: ComponentType<{ item: CatalogItem }>; } export const CategoryItem = ({ diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/EmptyTab.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/EmptyTab.tsx similarity index 100% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/EmptyTab.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/EmptyTab.tsx diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/StandardList.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/StandardList.tsx similarity index 51% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/StandardList.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/StandardList.tsx index cbedc7977..92bf8839e 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/StandardList.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/StandardList.tsx @@ -1,15 +1,21 @@ import { type ComponentType } from "react"; -import type { Item } from "../../../../types/common/item"; -import type { BoardName } from "../../../../types/data/board"; -import type { SidebarTab } from "../../../../types/workspace/sidebar"; +import type { CatalogItem } from "../../../../../types/common/item"; +import type { BoardName } from "../../../../../types/data/board"; import { CategoryItem } from "./CategoryItem"; +import type { SidebarTab } from "../../../types/sidebar"; interface StandardListProps { scope: SidebarTab; categories: readonly BoardName[]; - ItemComponent: ComponentType<{ item: Item }>; + ItemComponent: ComponentType<{ item: CatalogItem }>; } +/** + * Standard tree renderer for smaller catalogs. Right now used for commands tab.\ + * Based on categories (board names) and item component recieved as prop. + * + * It's counterpart is VirtualizedList, which is used for Telemetry data and implements virtualization. + */ export const StandardList = ({ scope, categories, diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Tab.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/Tab.tsx similarity index 79% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Tab.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/Tab.tsx index 4178663b6..7e5dfa44e 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Tab.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/Tab.tsx @@ -1,18 +1,18 @@ import { type ComponentType } from "react"; -import { useStore } from "../../../../store/store"; -import type { Item } from "../../../../types/common/item"; -import type { BoardName } from "../../../../types/data/board"; -import type { SidebarTab } from "../../../../types/workspace/sidebar"; +import { useStore } from "../../../../../store/store"; +import type { CatalogItem } from "../../../../../types/common/item"; +import type { BoardName } from "../../../../../types/data/board"; import { EmptyTab } from "./EmptyTab"; import { StandardList } from "./StandardList"; import { TabHeader } from "./TabHeader"; import { VirtualizedList } from "./VirtualizedList"; +import type { SidebarTab } from "../../../types/sidebar"; interface TabProps { title: string; scope: SidebarTab; categories: readonly BoardName[]; - ItemComponent: ComponentType<{ item: Item }>; + ItemComponent: ComponentType<{ item: CatalogItem }>; virtualized?: boolean; } diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/TabHeader.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx similarity index 89% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/TabHeader.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx index aa92aefee..13ff18450 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/TabHeader.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/TabHeader.tsx @@ -1,7 +1,7 @@ import { Button } from "@workspace/ui"; import { ListFilterPlus } from "@workspace/ui/icons"; -import { useStore } from "../../../../store/store"; -import type { SidebarTab } from "../../../../types/workspace/sidebar"; +import { useStore } from "../../../../../store/store"; +import type { SidebarTab } from "../../../types/sidebar"; interface TabHeaderProps { title: string; diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/VirtualizedList.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/VirtualizedList.tsx similarity index 69% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/VirtualizedList.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/VirtualizedList.tsx index 04f770c58..c646ffeb3 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/VirtualizedList.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/VirtualizedList.tsx @@ -2,11 +2,11 @@ import { useVirtualizer } from "@tanstack/react-virtual"; import { cn } from "@workspace/ui/lib"; import { useCallback, useRef } from "react"; -import { usePacketRows } from "../../../../hooks/usePacketRows"; -import type { BoardName } from "../../../../types/data/board"; -import type { VirtualRow } from "../../../../types/data/virtualization"; -import type { SidebarTab } from "../../../../types/workspace/sidebar"; -import { TelemetryRow } from "./Telemetry/TelemetryRow"; +import type { BoardName } from "../../../../../types/data/board"; +import type { VirtualRow } from "../../../../../types/data/virtualization"; +import { usePacketRows } from "../../../hooks/usePacketRows"; +import type { SidebarTab } from "../../../types/sidebar"; +import { TelemetryRow } from "./telemetry/TelemetryRow"; interface VirtualizedListProps { className?: string; @@ -14,6 +14,15 @@ interface VirtualizedListProps { categories: readonly BoardName[]; } +/** + * A highly optimized list renderer for data. Only used for Telemetry data. + * + * Unlike StandardList, this component flattens the Board -> Packet -> Variable tree + * into a single virtualized array to handle thousands of items without lag by rerendering only the visible items. + * + * It relies on `usePacketRows` (which calls `getFlattenedRows` from the store) + * to maintain the visual illusion of a tree structure while actually rendering a flat list. + */ export const VirtualizedList = ({ scope, categories, diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Commands/CommandItem.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/commands/CommandItem.tsx similarity index 96% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Commands/CommandItem.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/commands/CommandItem.tsx index be415a8f7..6de562468 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Commands/CommandItem.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/commands/CommandItem.tsx @@ -8,9 +8,9 @@ import { import { ChevronDown, Play, Send } from "@workspace/ui/icons"; import { cn } from "@workspace/ui/lib"; import { useState } from "react"; -import { getDefaultParameterValues } from "../../../../../lib/commandUtils"; -import { useStore } from "../../../../../store/store"; -import type { CommandCatalogItem } from "../../../../../types/data/commandCatalogItem"; +import { getDefaultParameterValues } from "../../../../../../lib/commandUtils"; +import { useStore } from "../../../../../../store/store"; +import type { CommandCatalogItem } from "../../../../../../types/data/commandCatalogItem"; import { CommandParameters } from "./CommandParameters"; interface CommandItemProps { diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Commands/CommandParameters.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/commands/CommandParameters.tsx similarity index 98% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Commands/CommandParameters.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/commands/CommandParameters.tsx index ac92ce798..52dd8454b 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Commands/CommandParameters.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/commands/CommandParameters.tsx @@ -12,7 +12,7 @@ import type { CommandParameter, EnumCommandParameter, NumericCommandParameter, -} from "../../../../../types/data/commandCatalogItem"; +} from "../../../../../../types/data/commandCatalogItem"; interface CommandParametersProps { fields: Record; diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/ChartPicker.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/ChartPicker.tsx similarity index 94% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/ChartPicker.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/ChartPicker.tsx index ebfc0e697..8a06598d0 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/ChartPicker.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/ChartPicker.tsx @@ -8,7 +8,7 @@ import { DropdownMenuTrigger, } from "@workspace/ui"; import { Plus } from "@workspace/ui/icons"; -import type { WorkspaceChartConfig } from "../../../../../store/slices/workspacesSlice"; +import type { WorkspaceChartConfig } from "../../../../../charts/types/charts"; interface ChartPickerProps { charts: WorkspaceChartConfig[]; diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryHeader.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryHeader.tsx similarity index 95% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryHeader.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryHeader.tsx index 9e80cc936..c9011defb 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryHeader.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryHeader.tsx @@ -2,8 +2,8 @@ import { Badge, Separator } from "@workspace/ui"; import { Activity, ChevronDown } from "@workspace/ui/icons"; import { cn } from "@workspace/ui/lib"; import { memo, useEffect, useRef, useState } from "react"; -import { useStore } from "../../../../../store/store"; -import type { TelemetryCatalogItem } from "../../../../../types/data/telemetryCatalogItem"; +import { useStore } from "../../../../../../store/store"; +import type { TelemetryCatalogItem } from "../../../../../../types/data/telemetryCatalogItem"; interface TelemetryHeaderProps { telemetryCatalogItem: TelemetryCatalogItem; diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryItem.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryItem.tsx similarity index 91% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryItem.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryItem.tsx index e88853223..d263e7cd6 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryItem.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryItem.tsx @@ -4,8 +4,8 @@ import { CollapsibleTrigger, } from "@workspace/ui"; import { memo } from "react"; -import { useStore } from "../../../../../store/store"; -import type { TelemetryCatalogItem } from "../../../../../types/data/telemetryCatalogItem"; +import { useStore } from "../../../../../../store/store"; +import type { TelemetryCatalogItem } from "../../../../../../types/data/telemetryCatalogItem"; import { TelemetryHeader } from "./TelemetryHeader"; import { VariableItem } from "./VariableItem"; diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryRow.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryRow.tsx similarity index 90% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryRow.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryRow.tsx index 9648479cc..14f911ba2 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryRow.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryRow.tsx @@ -1,6 +1,6 @@ import { memo, useCallback } from "react"; -import { useStore } from "../../../../../store/store"; -import type { VirtualRow } from "../../../../../types/data/virtualization"; +import { useStore } from "../../../../../../store/store"; +import type { VirtualRow } from "../../../../../../types/data/virtualization"; import { CategoryHeader } from "../CategoryHeader"; import { TelemetryHeader } from "./TelemetryHeader"; import { VariableItem } from "./VariableItem"; diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryValue.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryValue.tsx similarity index 98% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryValue.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryValue.tsx index f5604e372..ab9934224 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/TelemetryValue.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/TelemetryValue.tsx @@ -2,7 +2,7 @@ import { Badge } from "@workspace/ui"; import { Check, X } from "@workspace/ui/icons"; import { cn } from "@workspace/ui/lib"; import { useCallback } from "react"; -import { useStore } from "../../../../../store/store"; +import { useStore } from "../../../../../../store/store"; interface TelemetryValueProps { units?: string; diff --git a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/VariableItem.tsx b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/VariableItem.tsx similarity index 93% rename from frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/VariableItem.tsx rename to frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/VariableItem.tsx index 064d8675a..8c0351b29 100644 --- a/frontend/testing-view/src/components/Testing/RightSidebar/Tabs/Telemetry/VariableItem.tsx +++ b/frontend/testing-view/src/features/workspace/components/rightSidebar/tabs/telemetry/VariableItem.tsx @@ -2,9 +2,9 @@ import { useDraggable } from "@dnd-kit/core"; import { Badge } from "@workspace/ui"; import { cn } from "@workspace/ui/lib"; import { useShallow } from "zustand/shallow"; -import { getTypeBadgeClass } from "../../../../../lib/utils"; -import { useStore } from "../../../../../store/store"; -import type { Variable } from "../../../../../types/data/telemetryCatalogItem"; +import { getTypeBadgeClass } from "../../../../../../lib/utils"; +import { useStore } from "../../../../../../store/store"; +import type { Variable } from "../../../../../../types/data/telemetryCatalogItem"; import ChartPicker from "./ChartPicker"; import { TelemetryValue } from "./TelemetryValue"; @@ -125,7 +125,7 @@ export const VariableItem = ({ packetId, variable }: VariableItemProps) => { {/* Live Value */} diff --git a/frontend/testing-view/src/constants/defaultWorkspaces.ts b/frontend/testing-view/src/features/workspace/constants/defaultWorkspaces.ts similarity index 86% rename from frontend/testing-view/src/constants/defaultWorkspaces.ts rename to frontend/testing-view/src/features/workspace/constants/defaultWorkspaces.ts index acf4541ff..b2349283b 100644 --- a/frontend/testing-view/src/constants/defaultWorkspaces.ts +++ b/frontend/testing-view/src/features/workspace/constants/defaultWorkspaces.ts @@ -1,4 +1,4 @@ -import type { Workspace } from "../types/workspace/workspace"; +import type { Workspace } from "../types/workspace"; export const DEFAULT_WORKSPACES: Workspace[] = [ { diff --git a/frontend/testing-view/src/hooks/useDnd.ts b/frontend/testing-view/src/features/workspace/hooks/useDnd.ts similarity index 95% rename from frontend/testing-view/src/hooks/useDnd.ts rename to frontend/testing-view/src/features/workspace/hooks/useDnd.ts index da9d5d7e4..821d99e59 100644 --- a/frontend/testing-view/src/hooks/useDnd.ts +++ b/frontend/testing-view/src/features/workspace/hooks/useDnd.ts @@ -6,8 +6,8 @@ import { useSensors, } from "@dnd-kit/core"; import { useState } from "react"; -import { useStore } from "../store/store"; -import type { DndActiveData } from "../types/app/dndData"; +import { useStore } from "../../../store/store"; +import type { DndActiveData } from "../types/dndData"; export function useDnd() { const [activeData, setActiveData] = useState(null); diff --git a/frontend/testing-view/src/hooks/usePacketRows.ts b/frontend/testing-view/src/features/workspace/hooks/usePacketRows.ts similarity index 78% rename from frontend/testing-view/src/hooks/usePacketRows.ts rename to frontend/testing-view/src/features/workspace/hooks/usePacketRows.ts index d05293d76..bcc6ac39f 100644 --- a/frontend/testing-view/src/hooks/usePacketRows.ts +++ b/frontend/testing-view/src/features/workspace/hooks/usePacketRows.ts @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { useShallow } from "zustand/shallow"; -import { useStore } from "../store/store"; -import type { BoardName } from "../types/data/board"; -import type { SidebarTab } from "../types/workspace/sidebar"; +import { useStore } from "../../../store/store"; +import type { BoardName } from "../../../types/data/board"; +import type { SidebarTab } from "../types/sidebar"; export const usePacketRows = ( scope: SidebarTab, diff --git a/frontend/testing-view/src/store/slices/rightSidebarSlice.ts b/frontend/testing-view/src/features/workspace/store/rightSidebarSlice.ts similarity index 95% rename from frontend/testing-view/src/store/slices/rightSidebarSlice.ts rename to frontend/testing-view/src/features/workspace/store/rightSidebarSlice.ts index 6a7575f6f..29be2806e 100644 --- a/frontend/testing-view/src/store/slices/rightSidebarSlice.ts +++ b/frontend/testing-view/src/features/workspace/store/rightSidebarSlice.ts @@ -1,5 +1,5 @@ import type { StateCreator } from "zustand"; -import type { Store } from "../store"; +import type { Store } from "../../../store/store"; export interface RightSidebarSlice { // Section visibility diff --git a/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts new file mode 100644 index 000000000..ea09a6844 --- /dev/null +++ b/frontend/testing-view/src/features/workspace/store/workspacesSlice.ts @@ -0,0 +1,249 @@ +import type { StateCreator } from "zustand"; +import { createFullFilter } from "../../../lib/utils"; +import type { Store } from "../../../store/store"; +import type { KeyBinding } from "../../keyBindings/types/keyBinding"; +import { DEFAULT_WORKSPACES } from "../constants/defaultWorkspaces"; +import type { SidebarTab } from "../types/sidebar"; +import type { Workspace } from "../types/workspace"; + +export interface WorkspacesSlice { + /** Workspaces */ + activeWorkspace: Workspace | null; + workspaces: Workspace[]; + setActiveWorkspace: (workspace: Workspace) => void; + updateWorkspace: (id: string, name: string, description: string) => void; + removeWorkspace: (id: string) => void; + addWorkspace: (name: string, description: string) => void; + getActiveWorkspaceId: () => string | null; + + /** Key Bindings */ + addKeyBinding: ( + commandId: number, + key: string, + parameters: Record, + ) => void; + removeKeyBinding: (bindingId: string) => void; + getKeyBindings: () => KeyBinding[]; + getCommandIdsByKey: (key: string) => number[]; + getKeyBindingForCommand: (commandId: number) => string | undefined; + getKeyBindingParameters: ( + commandId: number, + key: string, + ) => Record | undefined; +} + +export const createWorkspacesSlice: StateCreator< + Store, + [], + [], + WorkspacesSlice +> = (set, get) => ({ + /** Workspaces */ + activeWorkspace: DEFAULT_WORKSPACES[0], + workspaces: DEFAULT_WORKSPACES, + setActiveWorkspace: (workspace) => set({ activeWorkspace: workspace }), + addWorkspace: (name, description) => { + const newWorkspaceId = crypto.randomUUID(); + + const newWorkspace: Workspace = { + id: newWorkspaceId, + name, + description, + keyBindings: [], + }; + + set((state) => { + // Add the new workspace + const newWorkspaces = [...state.workspaces, newWorkspace]; + + // Initialize filters for the new workspace + const commands = state.commandsCatalog; + const telemetry = state.telemetryCatalog; + + const newWorkspaceFilters = { + ...state.workspaceFilters, + [newWorkspaceId]: { + commands: createFullFilter(commands), + telemetry: createFullFilter(telemetry), + logs: createFullFilter(telemetry), + }, + }; + + // Initialize expanded items for the new workspace + const newExpandedItems = { + ...state.expandedItems, + [newWorkspaceId]: { + commands: new Set(), + telemetry: new Set(), + logs: new Set(), + }, + }; + + // Initialize active tab for the new workspace + const newActiveTabs = { + ...state.activeTab, + [newWorkspaceId]: "commands" as SidebarTab, + }; + + // Initialize charts for the new workspace + const newCharts = { + ...state.charts, + [newWorkspaceId]: [], + }; + + return { + workspaces: newWorkspaces, + activeWorkspace: newWorkspace, // Auto-switch to the new workspace + workspaceFilters: newWorkspaceFilters, + expandedItems: newExpandedItems, + activeTab: newActiveTabs, + charts: newCharts, + }; + }); + }, + + updateWorkspace: (id, name, description) => { + set((state) => { + const newWorkspaces = state.workspaces.map((workspace) => + workspace.id === id ? { ...workspace, name, description } : workspace, + ); + + // Update activeWorkspace if it's the one being edited + const newActiveWorkspace = + state.activeWorkspace?.id === id + ? { ...state.activeWorkspace, name, description } + : state.activeWorkspace; + + return { + workspaces: newWorkspaces, + activeWorkspace: newActiveWorkspace, + }; + }); + }, + + removeWorkspace: (id) => { + set((state) => { + const newWorkspaces = state.workspaces.filter( + (workspace) => workspace.id !== id, + ); + + // Determine new active workspace + let newActiveWorkspace = state.activeWorkspace; + if (state.activeWorkspace?.id === id) { + // If we're deleting the active workspace, switch to another one + newActiveWorkspace = newWorkspaces[0] || null; + } + + // Clean up workspace-specific data + const newWorkspaceFilters = { ...state.workspaceFilters }; + const newExpandedItems = { ...state.expandedItems }; + const newActiveTabs = { ...state.activeTab }; + const newCharts = { ...state.charts }; + + delete newWorkspaceFilters[id]; + delete newExpandedItems[id]; + delete newActiveTabs[id]; + delete newCharts[id]; + + return { + workspaces: newWorkspaces, + activeWorkspace: newActiveWorkspace, + workspaceFilters: newWorkspaceFilters, + expandedItems: newExpandedItems, + activeTab: newActiveTabs, + charts: newCharts, + }; + }); + }, + + getActiveWorkspaceId: () => { + const activeWorkspace = get().activeWorkspace; + return activeWorkspace?.id ?? null; + }, + + // Key Bindings + addKeyBinding: (commandId, key, parameters) => { + const activeWorkspaceId = get().getActiveWorkspaceId(); + if (!activeWorkspaceId) return; + + const bindingId = crypto.randomUUID(); + + set((state) => { + const updatedWorkspaces = state.workspaces.map((workspace) => { + if (workspace.id === activeWorkspaceId) { + return { + ...workspace, + keyBindings: [ + ...(workspace.keyBindings || []), + { id: bindingId, commandId, key, parameters }, + ], + }; + } + return workspace; + }); + + const updatedActiveWorkspace = updatedWorkspaces.find( + (w) => w.id === activeWorkspaceId, + ); + + return { + workspaces: updatedWorkspaces, + activeWorkspace: updatedActiveWorkspace || state.activeWorkspace, + }; + }); + }, + + removeKeyBinding: (bindingId) => { + const activeWorkspaceId = get().getActiveWorkspaceId(); + if (!activeWorkspaceId) return; + + set((state) => { + const updatedWorkspaces = state.workspaces.map((workspace) => { + if (workspace.id === activeWorkspaceId) { + return { + ...workspace, + keyBindings: (workspace.keyBindings || []).filter( + (binding) => binding.id !== bindingId, + ), + }; + } + return workspace; + }); + + const updatedActiveWorkspace = updatedWorkspaces.find( + (w) => w.id === activeWorkspaceId, + ); + + return { + workspaces: updatedWorkspaces, + activeWorkspace: updatedActiveWorkspace || state.activeWorkspace, + }; + }); + }, + + getKeyBindings: () => { + const activeWorkspace = get().activeWorkspace; + return activeWorkspace?.keyBindings || []; + }, + + getCommandIdsByKey: (key) => { + const bindings = get().getKeyBindings(); + return bindings + .filter((binding) => binding.key === key) + .map((binding) => binding.commandId); + }, + + getKeyBindingForCommand: (commandId) => { + const bindings = get().getKeyBindings(); + const binding = bindings.find((b) => b.commandId === commandId); + return binding?.key; + }, + + getKeyBindingParameters: (commandId, key) => { + const bindings = get().getKeyBindings(); + const binding = bindings.find( + (b) => b.commandId === commandId && b.key === key, + ); + return binding?.parameters; + }, +}); diff --git a/frontend/testing-view/src/types/app/dndData.ts b/frontend/testing-view/src/features/workspace/types/dndData.ts similarity index 100% rename from frontend/testing-view/src/types/app/dndData.ts rename to frontend/testing-view/src/features/workspace/types/dndData.ts diff --git a/frontend/testing-view/src/features/workspace/types/sidebar.ts b/frontend/testing-view/src/features/workspace/types/sidebar.ts new file mode 100644 index 000000000..701e6971f --- /dev/null +++ b/frontend/testing-view/src/features/workspace/types/sidebar.ts @@ -0,0 +1,11 @@ +import type { FilterScope } from "../../filtering/types/filters"; + +/** + * Sidebar tab identifiers. + */ +export type SidebarTab = "commands" | "telemetry"; + +/** + * Record of expanded items per filter scope. + */ +export type WorkspaceExpandedItems = Record>; diff --git a/frontend/testing-view/src/features/workspace/types/workspace.ts b/frontend/testing-view/src/features/workspace/types/workspace.ts new file mode 100644 index 000000000..493e4376a --- /dev/null +++ b/frontend/testing-view/src/features/workspace/types/workspace.ts @@ -0,0 +1,15 @@ +import type { KeyBinding } from "../../keyBindings/types/keyBinding"; + +/** + * Workspace definition. + */ +export interface Workspace { + /** The name of the workspace */ + name: string; + /** The id of the workspace */ + id: string; + /** The description of the workspace */ + description: string; + /** The key bindings for the workspace */ + keyBindings: KeyBinding[]; +} diff --git a/frontend/testing-view/src/hooks/useAppConfigs.ts b/frontend/testing-view/src/hooks/useAppConfigs.ts index 04853dfef..24ceb704b 100644 --- a/frontend/testing-view/src/hooks/useAppConfigs.ts +++ b/frontend/testing-view/src/hooks/useAppConfigs.ts @@ -1,6 +1,6 @@ import { useFetchConfig } from "@workspace/ui/hooks"; import { useEffect } from "react"; -import type { OrdersData, PacketsData } from "../types/data/transformedBoards"; +import type { OrdersData, PacketsData } from "../types/data/board"; const useAppConfigs = (isConnected: boolean) => { const backendUrl = import.meta.env.VITE_BACKEND_URL; diff --git a/frontend/testing-view/src/hooks/useAppMode.ts b/frontend/testing-view/src/hooks/useAppMode.ts index fa1feb36c..13eb13307 100644 --- a/frontend/testing-view/src/hooks/useAppMode.ts +++ b/frontend/testing-view/src/hooks/useAppMode.ts @@ -1,7 +1,7 @@ import { logger } from "@workspace/core"; import { useCallback, useEffect } from "react"; import { useStore } from "../store/store"; -import type { OrdersData, PacketsData } from "../types/data/transformedBoards"; +import type { OrdersData, PacketsData } from "../types/data/board"; export function useAppMode( packets: PacketsData | null, diff --git a/frontend/testing-view/src/hooks/useBoardData.ts b/frontend/testing-view/src/hooks/useBoardData.ts index 724c0314b..22ff3d8f9 100644 --- a/frontend/testing-view/src/hooks/useBoardData.ts +++ b/frontend/testing-view/src/hooks/useBoardData.ts @@ -4,18 +4,14 @@ import { formatName } from "../lib/utils"; import { MOCK_COMMANDS_CATALOG } from "../mocks/commands"; import { MOCK_TELEMETRY_CATALOG } from "../mocks/telemetry"; import type { AppMode } from "../types/app/mode"; -import type { BoardName } from "../types/data/board"; +import type { BoardName, OrdersData, PacketsData } from "../types/data/board"; import type { CommandCatalogItem } from "../types/data/commandCatalogItem"; import type { TelemetryCatalogItem } from "../types/data/telemetryCatalogItem"; -import type { - OrdersData, - PacketsData, - TransformedBoards, -} from "../types/data/transformedBoards"; +import type { TransformedBoards } from "../types/data/transformedBoards"; export function useBoardData( - packets: PacketsData | null, - commands: OrdersData | null, + packets: PacketsData | null | undefined, + commands: OrdersData | null | undefined, appMode: AppMode, ) { const transformedBoards = useMemo(() => { diff --git a/frontend/testing-view/src/hooks/useErrorHandler.ts b/frontend/testing-view/src/hooks/useErrorHandler.ts index 7f7f3c475..60f14b26c 100644 --- a/frontend/testing-view/src/hooks/useErrorHandler.ts +++ b/frontend/testing-view/src/hooks/useErrorHandler.ts @@ -2,6 +2,12 @@ import { logger } from "@workspace/core"; import { useEffect } from "react"; import { useStore } from "../store/store"; +/** + * This hook listens for global errors and unhandled promises rejections + * and sets the app mode to "error" in global store's app slice. + * @returns a function to manually report errors + * * Note: returned function is supposed to be used in the ErrorBoundary component + */ export function useErrorHandler() { const setError = useStore((s) => s.setError); const setAppMode = useStore((s) => s.setAppMode); @@ -38,7 +44,11 @@ export function useErrorHandler() { }; }, [setError, setAppMode]); - // Return a function to manually report errors + /** + * This functions prints the error and the error info in console using logger + * and sets the error in global store's app slice. + * It is designed to be used in the ErrorBoundary component. + */ const reportError = (error: Error, ErrorInfo?: React.ErrorInfo) => { logger.testingView.error( `Error${ErrorInfo ? ` in ${ErrorInfo}` : ""}:`, diff --git a/frontend/testing-view/src/hooks/useTransformedBoards.ts b/frontend/testing-view/src/hooks/useTransformedBoards.ts index 96c590837..c8fc04c40 100644 --- a/frontend/testing-view/src/hooks/useTransformedBoards.ts +++ b/frontend/testing-view/src/hooks/useTransformedBoards.ts @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { useStore } from "../store/store"; -import type { OrdersData, PacketsData } from "../types/data/transformedBoards"; +import type { OrdersData, PacketsData } from "../types/data/board"; import { useBoardData } from "./useBoardData"; export function useTransformedBoards( @@ -12,7 +12,9 @@ export function useTransformedBoards( const setTelemetryCatalog = useStore((s) => s.setTelemetryCatalog); const setCommandsCatalog = useStore((s) => s.setCommandsCatalog); - const initializeTabFilters = useStore((s) => s.initializeTabFilters); + const initializeWorkspaceFilters = useStore( + (s) => s.initializeWorkspaceFilters, + ); useEffect(() => { if ( @@ -23,12 +25,12 @@ export function useTransformedBoards( setTelemetryCatalog(transformedBoards.telemetryCatalog); setCommandsCatalog(transformedBoards.commandsCatalog); - initializeTabFilters(); + initializeWorkspaceFilters(); }, [ transformedBoards, setTelemetryCatalog, setCommandsCatalog, - initializeTabFilters, + initializeWorkspaceFilters, ]); // Debug logs diff --git a/frontend/testing-view/src/layout/AppLayout.tsx b/frontend/testing-view/src/layout/AppLayout.tsx index 4a701ed9f..faf746950 100644 --- a/frontend/testing-view/src/layout/AppLayout.tsx +++ b/frontend/testing-view/src/layout/AppLayout.tsx @@ -1,9 +1,9 @@ import { SidebarInset, SidebarProvider } from "@workspace/ui/components"; import React, { useEffect } from "react"; import Footer from "../components/Footer"; -import Header from "../components/Header/Header"; -import AppSidebar from "../components/LeftSidebar/AppSidebar"; -import { SettingsDialog } from "../components/Settings/SettingsDialog"; +import Header from "../components/header/Header"; +import AppSidebar from "../components/leftSidebar/AppSidebar"; +import { SettingsDialog } from "../components/settings/SettingsDialog"; import { useStore } from "../store/store"; interface AppLayoutProps { diff --git a/frontend/testing-view/src/lib/utils.test.ts b/frontend/testing-view/src/lib/utils.test.ts index b01eba7ad..947b6c5c7 100644 --- a/frontend/testing-view/src/lib/utils.test.ts +++ b/frontend/testing-view/src/lib/utils.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { variablesBadgeClasses } from "../constants/variablesBadgeClasses"; +import type { FilterScope } from "../features/filtering/types/filters"; import type { MessageTimestamp } from "../types/data/message"; -import type { FilterScope } from "../types/workspace/filters"; import { createEmptyFilter, createFullFilter, diff --git a/frontend/testing-view/src/lib/utils.ts b/frontend/testing-view/src/lib/utils.ts index c657e7fb2..629263f43 100644 --- a/frontend/testing-view/src/lib/utils.ts +++ b/frontend/testing-view/src/lib/utils.ts @@ -1,15 +1,15 @@ import { acronyms } from "../constants/acronyms"; import { BOARD_NAMES } from "../constants/boards"; -import { DEFAULT_WORKSPACES } from "../constants/defaultWorkspaces"; import { variablesBadgeClasses } from "../constants/variablesBadgeClasses"; -import type { Item } from "../types/common/item"; -import type { BoardName } from "../types/data/board"; -import type { MessageTimestamp } from "../types/data/message"; import type { FilterScope, TabFilter, WorkspaceFilters, -} from "../types/workspace/filters"; +} from "../features/filtering/types/filters"; +import { DEFAULT_WORKSPACES } from "../features/workspace/constants/defaultWorkspaces"; +import type { CatalogItem } from "../types/common/item"; +import type { BoardName } from "../types/data/board"; +import type { MessageTimestamp } from "../types/data/message"; type InitialFilters = Record; @@ -37,7 +37,7 @@ export const createEmptyFilter = (): TabFilter => { }; export const createFullFilter = ( - dataSource: Record, + dataSource: Record, ): TabFilter => { return BOARD_NAMES.reduce((acc, category) => { acc[category] = dataSource[category]?.map((item) => item.id) || []; diff --git a/frontend/testing-view/src/mocks/chartsConfigurations.ts b/frontend/testing-view/src/mocks/chartsConfigurations.ts index 173f88306..cb1a5eacc 100644 --- a/frontend/testing-view/src/mocks/chartsConfigurations.ts +++ b/frontend/testing-view/src/mocks/chartsConfigurations.ts @@ -1,4 +1,4 @@ -import type { WorkspaceChartConfig } from "../store/slices/workspacesSlice"; +import type { WorkspaceChartConfig } from "../features/charts/types/charts"; export const MOCK_CHARTS: Record = { "workspace-1": [ diff --git a/frontend/testing-view/src/mocks/messages.ts b/frontend/testing-view/src/mocks/messages.ts deleted file mode 100644 index 979051b05..000000000 --- a/frontend/testing-view/src/mocks/messages.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Message } from "../types/data/message"; - -// Mock Messages -export const MOCK_MESSAGES: Message[] = [ - { - id: "1", - timestamp: { - counter: 1, - second: 56, - minute: 34, - hour: 12, - day: 1, - month: 1, - year: 2026, - }, - kind: "info", - payload: { - connection: "established", - device: "0x01", - }, - board: "board1", - name: "connection", - }, - { - id: "2", - timestamp: { - counter: 2, - second: 12, - minute: 35, - hour: 12, - day: 1, - month: 1, - year: 2026, - }, - kind: "warning", - payload: { - latency: 250, - threshold: 200, - }, - board: "board1", - name: "latency", - }, - { - id: "3", - timestamp: { - counter: 4, - second: 1, - minute: 36, - hour: 12, - day: 1, - month: 1, - year: 2026, - }, - kind: "fault", - payload: { - error: "Failed to send packet", - details: "Timeout after 3 retries", - }, - board: "board1", - name: "packet", - }, - { - id: "4", - timestamp: { - counter: 5, - second: 15, - minute: 36, - hour: 12, - day: 1, - month: 1, - year: 2026, - }, - kind: "ok", - payload: { - message: "Calibration complete", - details: "Accuracy: 99.8%", - }, - board: "board1", - name: "calibration", - }, -]; diff --git a/frontend/testing-view/src/pages/Testing.tsx b/frontend/testing-view/src/pages/Testing.tsx index a5ac7388f..b02ccbb9b 100644 --- a/frontend/testing-view/src/pages/Testing.tsx +++ b/frontend/testing-view/src/pages/Testing.tsx @@ -5,12 +5,12 @@ import { ResizablePanelGroup, } from "@workspace/ui"; import { SquareLibrary } from "@workspace/ui/icons"; -import { DndOverlay } from "../components/Testing/DndOverlay"; -import { FilterController } from "../components/Testing/Filters/FilterController"; -import { MainPanel } from "../components/Testing/MainPanel"; -import { RightSidebar } from "../components/Testing/RightSidebar/RightSidebar"; -import { useDnd } from "../hooks/useDnd"; -import { useGlobalKeyBindings } from "../hooks/useGlobalKeyBindings"; +import { FilterController } from "../features/filtering/components/FilterController"; +import { useGlobalKeyBindings } from "../features/keyBindings/hooks/useGlobalKeyBindings"; +import { DndOverlay } from "../features/workspace/components/DndOverlay"; +import { MainPanel } from "../features/workspace/components/MainPanel"; +import { RightSidebar } from "../features/workspace/components/rightSidebar/RightSidebar"; +import { useDnd } from "../features/workspace/hooks/useDnd"; import { useStore } from "../store/store"; export const Testing = () => { diff --git a/frontend/testing-view/src/store/slices/catalogSlice.ts b/frontend/testing-view/src/store/slices/catalogSlice.ts index 0439d04b0..e2e60017f 100644 --- a/frontend/testing-view/src/store/slices/catalogSlice.ts +++ b/frontend/testing-view/src/store/slices/catalogSlice.ts @@ -1,23 +1,27 @@ import type { StateCreator } from "zustand"; -import type { Item } from "../../types/common/item"; +import type { CatalogItem } from "../../types/common/item"; import type { BoardName } from "../../types/data/board"; import type { Store } from "../store"; export interface CatalogSlice { // Commands catalog - commandsCatalog: Record; - setCommandsCatalog: (commandsCatalog: Record) => void; + commandsCatalog: Record; + setCommandsCatalog: ( + commandsCatalog: Record, + ) => void; // Telemetry catalog - telemetryCatalog: Record; - setTelemetryCatalog: (telemetryCatalog: Record) => void; + telemetryCatalog: Record; + setTelemetryCatalog: ( + telemetryCatalog: Record, + ) => void; } export const createCatalogSlice: StateCreator = ( set, ) => ({ - commandsCatalog: {} as Record, - telemetryCatalog: {} as Record, + commandsCatalog: {} as Record, + telemetryCatalog: {} as Record, setCommandsCatalog: (commandsCatalog) => set({ commandsCatalog }), setTelemetryCatalog: (telemetryCatalog) => set({ telemetryCatalog }), }); diff --git a/frontend/testing-view/src/store/slices/workspacesSlice.ts b/frontend/testing-view/src/store/slices/workspacesSlice.ts deleted file mode 100644 index 6705043c0..000000000 --- a/frontend/testing-view/src/store/slices/workspacesSlice.ts +++ /dev/null @@ -1,762 +0,0 @@ -import type { StateCreator } from "zustand"; -import { DEFAULT_WORKSPACES } from "../../constants/defaultWorkspaces"; -import { EMPTY_ARRAY } from "../../constants/emptyArray"; -import { - createEmptyFilter, - createFullFilter, - generateInitialFilters, - getCatalogKey, -} from "../../lib/utils"; -import type { Item } from "../../types/common/item"; -import type { BoardName } from "../../types/data/board"; -import type { Measurement } from "../../types/data/telemetryCatalogItem"; -import type { VirtualRow } from "../../types/data/virtualization"; -import type { - FilterScope, - TabFilter, - WorkspaceFilters, -} from "../../types/workspace/filters"; -import type { KeyBinding } from "../../types/workspace/keyBinding"; -import type { - SidebarTab, - WorkspaceExpandedItems, -} from "../../types/workspace/sidebar"; -import type { Workspace } from "../../types/workspace/workspace"; -import type { Store } from "../store"; - -export interface WorkspaceChartSeries { - packetId: number; - variable: string; -} - -export interface WorkspaceChartConfig { - id: string; - series: WorkspaceChartSeries[]; - historyLimit: number; -} - -export type CheckboxState = boolean | "indeterminate"; - -export interface WorkspacesSlice { - // Workspaces - activeWorkspace: Workspace | null; - workspaces: Workspace[]; - setActiveWorkspace: (workspace: Workspace) => void; - updateWorkspace: (id: string, name: string, description: string) => void; - removeWorkspace: (id: string) => void; - addWorkspace: (name: string, description: string) => void; - getActiveWorkspaceId: () => string | null; - - // Internal helpers - getCatalog: (scope: FilterScope) => Record; - - // Tabs (per workspace) - activeTab: Record; - getActiveTab: () => SidebarTab; - setActiveTab: (tab: SidebarTab) => void; - - // Filters (per workspace) - tabFilters: Record; - initializeTabFilters: () => void; - updateFilters: (scope: FilterScope, filters: TabFilter) => void; - - // Helper getters - getActiveFilters: (scope: FilterScope) => TabFilter | undefined; - getActiveExpanded: (scope: FilterScope) => Set | undefined; - - // Getters for filtered items - getFilteredItems: (scope: FilterScope) => Item[]; - getFilteredItemsIds: (scope: FilterScope) => number[]; - getFilteredItemsIdsByCategory: ( - scope: FilterScope, - category: BoardName, - ) => number[]; - getFilteredItemsByCategory: ( - scope: FilterScope, - category: BoardName, - ) => Item[]; - - // Stats getters - getFilteredCount: (scope: FilterScope) => number; - getFilteredCountByCategory: ( - scope: FilterScope, - category: BoardName, - ) => number; - getTotalCount: (scope: FilterScope) => number; - - // Selection state getters - getSelectionState: (scope: FilterScope, category: BoardName) => CheckboxState; - - // Filter actions - selectAllFilters: (scope: FilterScope) => void; - clearFilters: (scope: FilterScope) => void; - toggleCategoryFilter: ( - scope: FilterScope, - category: BoardName, - checked: boolean, - ) => void; - toggleItemFilter: ( - scope: FilterScope, - category: BoardName, - id: number, - ) => void; - - // Expanded items (per workspace) - expandedItems: Record; - isItemExpanded: ( - scope: SidebarTab, - type: string, - itemId: number | string, - ) => boolean; - toggleExpandedItem: ( - scope: SidebarTab, - type: string, - itemId: number | string, - ) => void; - getFlattenedRows: ( - scope: SidebarTab, - categories: readonly BoardName[], - ) => VirtualRow[]; - - // Filter dialog - filterDialog: { - isOpen: boolean; - scope: FilterScope | null; - }; - openFilterDialog: (scope: FilterScope) => void; - closeFilterDialog: () => void; - - // Telemetry Charts - charts: Record; - setCharts: (charts: Record) => void; - getActiveWorkspaceCharts: () => WorkspaceChartConfig[]; - addChart: (workspaceId: string) => string; - removeChart: (workspaceId: string, chartId: string) => void; - reorderCharts: ( - workspaceId: string, - oldIndex: number, - newIndex: number, - ) => void; - addSeriesToChart: ( - workspaceId: string, - chartId: string, - series: WorkspaceChartSeries, - ) => void; - removeSeriesFromChart: ( - workspaceId: string, - chartId: string, - variable: string, - ) => void; - setChartHistoryLimit: ( - workspaceId: string, - chartId: string, - newHistoryLimit: number, - ) => void; - - // Key Bindings - addKeyBinding: ( - commandId: number, - key: string, - parameters: Record, - ) => void; - removeKeyBinding: (bindingId: string) => void; - getKeyBindings: () => KeyBinding[]; - getCommandIdsByKey: (key: string) => number[]; - getKeyBindingForCommand: (commandId: number) => string | undefined; - getKeyBindingParameters: ( - commandId: number, - key: string, - ) => Record | undefined; -} - -export const createWorkspacesSlice: StateCreator< - Store, - [], - [], - WorkspacesSlice -> = (set, get) => ({ - // Workspaces - activeWorkspace: DEFAULT_WORKSPACES[0], - workspaces: DEFAULT_WORKSPACES, - setActiveWorkspace: (workspace) => set({ activeWorkspace: workspace }), - addWorkspace: (name, description) => { - const newWorkspaceId = crypto.randomUUID(); - - const newWorkspace: Workspace = { - id: newWorkspaceId, - name, - description, - keyBindings: [], - }; - - set((state) => { - // Add the new workspace - const newWorkspaces = [...state.workspaces, newWorkspace]; - - // Initialize filters for the new workspace - const commands = state.commandsCatalog; - const telemetry = state.telemetryCatalog; - - const newTabFilters = { - ...state.tabFilters, - [newWorkspaceId]: { - commands: createFullFilter(commands), - telemetry: createFullFilter(telemetry), - logs: createFullFilter(telemetry), - }, - }; - - // Initialize expanded items for the new workspace - const newExpandedItems = { - ...state.expandedItems, - [newWorkspaceId]: { - commands: new Set(), - telemetry: new Set(), - logs: new Set(), - }, - }; - - // Initialize active tab for the new workspace - const newActiveTabs = { - ...state.activeTab, - [newWorkspaceId]: "commands" as SidebarTab, - }; - - // Initialize charts for the new workspace - const newCharts = { - ...state.charts, - [newWorkspaceId]: [], - }; - - return { - workspaces: newWorkspaces, - activeWorkspace: newWorkspace, // Auto-switch to the new workspace - tabFilters: newTabFilters, - expandedItems: newExpandedItems, - activeTab: newActiveTabs, - charts: newCharts, - }; - }); - }, - - updateWorkspace: (id, name, description) => { - set((state) => { - const newWorkspaces = state.workspaces.map((workspace) => - workspace.id === id ? { ...workspace, name, description } : workspace, - ); - - // Update activeWorkspace if it's the one being edited - const newActiveWorkspace = - state.activeWorkspace?.id === id - ? { ...state.activeWorkspace, name, description } - : state.activeWorkspace; - - return { - workspaces: newWorkspaces, - activeWorkspace: newActiveWorkspace, - }; - }); - }, - - removeWorkspace: (id) => { - set((state) => { - const newWorkspaces = state.workspaces.filter( - (workspace) => workspace.id !== id, - ); - - // Determine new active workspace - let newActiveWorkspace = state.activeWorkspace; - if (state.activeWorkspace?.id === id) { - // If we're deleting the active workspace, switch to another one - newActiveWorkspace = newWorkspaces[0] || null; - } - - // Clean up workspace-specific data - const newTabFilters = { ...state.tabFilters }; - const newExpandedItems = { ...state.expandedItems }; - const newActiveTabs = { ...state.activeTab }; - const newCharts = { ...state.charts }; - - delete newTabFilters[id]; - delete newExpandedItems[id]; - delete newActiveTabs[id]; - delete newCharts[id]; - - return { - workspaces: newWorkspaces, - activeWorkspace: newActiveWorkspace, - tabFilters: newTabFilters, - expandedItems: newExpandedItems, - activeTab: newActiveTabs, - charts: newCharts, - }; - }); - }, - - getActiveWorkspaceId: () => { - const activeWorkspace = get().activeWorkspace; - return activeWorkspace?.id ?? null; - }, - - // Internal helpers - getCatalog: (scope: FilterScope) => { - const catalogKey = getCatalogKey(scope); - if (!catalogKey) return {}; - - return get()[catalogKey]; - }, - - // Tabs (per workspace) - activeTab: {}, - getActiveTab: () => { - const activeWorkspaceId = get().getActiveWorkspaceId(); - if (!activeWorkspaceId) return "commands"; - return get().activeTab[activeWorkspaceId] || "commands"; - }, - setActiveTab: (tab) => { - const activeWorkspaceId = get().getActiveWorkspaceId(); - if (!activeWorkspaceId) return; - - set((state) => ({ - activeTab: { ...state.activeTab, [activeWorkspaceId]: tab }, - })); - }, - - // Filters (per workspace) - tabFilters: {}, - initializeTabFilters: () => { - const commands = get().commandsCatalog; - const telemetry = get().telemetryCatalog; - - const currentFilters = get().tabFilters; - - // Only initialize if filters are empty (not persisted) - if (Object.keys(currentFilters).length === 0) { - set({ - tabFilters: generateInitialFilters({ - commands: createFullFilter(commands), - telemetry: createFullFilter(telemetry), - logs: createFullFilter(telemetry), - }), - }); - } - }, - updateFilters: (scope, filters) => { - const workspaceId = get().getActiveWorkspaceId(); - if (!workspaceId) return; - - set((state) => ({ - tabFilters: { - ...state.tabFilters, - [workspaceId]: { - ...(state.tabFilters[workspaceId] || {}), - [scope]: filters, - }, - }, - })); - }, - - // Helper getters - getActiveFilters: (scope) => { - const id = get().getActiveWorkspaceId(); - return id ? get().tabFilters[id]?.[scope] : undefined; - }, - - getActiveExpanded: (scope) => { - const id = get().getActiveWorkspaceId(); - return id ? get().expandedItems[id]?.[scope] : undefined; - }, - - // Getters for filtered items - getFilteredItemsIds: (scope) => { - const filters = get().getActiveFilters(scope); - return filters ? Object.values(filters).flat() : []; - }, - - getFilteredItemsIdsByCategory: (scope, category) => { - return get().getActiveFilters(scope)?.[category] || []; - }, - getFilteredItems: (scope) => { - const filters = get().getActiveFilters(scope); - if (!filters) return []; - - const catalog = get().getCatalog(scope); - if (!catalog) return []; - - return Object.entries(catalog).flatMap(([cat, items]) => { - const selected = filters[cat as BoardName] || []; - return items.filter((i) => selected.includes(i.id)); - }); - }, - getFilteredItemsByCategory: (scope, category) => { - const selected = get().getFilteredItemsIdsByCategory(scope, category); - const catalog = get().getCatalog(scope); - const items = catalog?.[category] || []; - return items.filter((i) => selected.includes(i.id)); - }, - - // Stats getters - getFilteredCount: (scope) => get().getFilteredItemsIds(scope).length, - - getFilteredCountByCategory: (scope, category) => - get().getFilteredItemsIdsByCategory(scope, category).length, - - getTotalCount: (scope) => { - const catalog = get().getCatalog(scope); - return Object.values(catalog).reduce((acc, items) => acc + items.length, 0); - }, - - getSelectionState: (scope, category) => { - const selectedCount = get().getFilteredCountByCategory(scope, category); - const catalog = get().getCatalog(scope); - const totalItems = catalog?.[category]?.length || 0; - - if (totalItems === 0 || selectedCount === 0) return false; - if (selectedCount === totalItems) return true; - return "indeterminate"; - }, - - // getCategoryCheckedCount: (scope, category) => { - // const activeWorkspaceId = get().getActiveWorkspaceId(); - // if (!activeWorkspaceId) return 0; - - // const filter = get().tabFilters[activeWorkspaceId][scope]; - // if (!filter) return 0; - - // return filter[category].length; - // }, - - // Filter actions - selectAllFilters: (scope) => { - const workspaceId = get().getActiveWorkspaceId(); - if (!workspaceId) return; - - const items = get().getCatalog(scope); - - const fullFilter = createFullFilter(items); - get().updateFilters(scope, fullFilter); - }, - clearFilters: (scope) => { - const workspaceId = get().getActiveWorkspaceId(); - if (!workspaceId) return; - const emptyFilter = createEmptyFilter(); - get().updateFilters(scope, emptyFilter); - }, - toggleCategoryFilter: (scope, category, checked) => { - const workspaceId = get().getActiveWorkspaceId(); - if (!workspaceId) return; - - const catalog = get().getCatalog(scope); - - const currentFilters = - get().tabFilters[workspaceId]?.[scope] || createEmptyFilter(); - - const newItems = checked - ? catalog?.[category]?.map((item) => item.id) || [] - : []; - - get().updateFilters(scope, { - ...currentFilters, - [category]: newItems, - }); - }, - - toggleItemFilter: (scope, category, itemId) => { - const workspaceId = get().getActiveWorkspaceId(); - if (!workspaceId) return; - - const currentWorkspaceFilters = get().tabFilters[workspaceId] || {}; - const currentTabFilter = - currentWorkspaceFilters[scope] || createEmptyFilter(); - - const currentCategoryIds = currentTabFilter[category] || []; - - const isSelected = currentCategoryIds.includes(itemId); - const newCategoryIds = isSelected - ? currentCategoryIds.filter((id) => id !== itemId) - : [...currentCategoryIds, itemId]; - - get().updateFilters(scope, { - ...currentTabFilter, - [category]: newCategoryIds, - }); - }, - - // Expanded items - expandedItems: {}, - isItemExpanded: (scope, type, itemId) => { - const activeWorkspaceId = get().getActiveWorkspaceId(); - if (!activeWorkspaceId) return false; - - const expandedItems = get().expandedItems[activeWorkspaceId]?.[scope]; - if (!expandedItems) return false; - - return expandedItems.has(`${type}:${itemId}`); - }, - toggleExpandedItem: (scope, type, itemId) => { - const activeWorkspaceId = get().getActiveWorkspaceId(); - if (!activeWorkspaceId) return; - - set((state) => { - const expandedItems = - state.expandedItems[activeWorkspaceId]?.[scope] || new Set(); - const newExpandedItems = new Set(expandedItems); - - if (newExpandedItems.has(`${type}:${itemId}`)) { - newExpandedItems.delete(`${type}:${itemId}`); - } else { - newExpandedItems.add(`${type}:${itemId}`); - } - - return { - expandedItems: { - ...state.expandedItems, - [activeWorkspaceId]: { - ...state.expandedItems[activeWorkspaceId], - [scope]: newExpandedItems, - }, - }, - }; - }); - }, - getFlattenedRows: (scope, categories) => { - const rows: VirtualRow[] = []; - - categories.forEach((category) => { - const items = get().getFilteredItemsByCategory(scope, category); - if (items.length === 0) return; - - // Add the Header (Board) - rows.push({ - type: "board", - id: category, - label: category, - count: items.length, - }); - - // If the board is expanded, add its packets - if (get().isItemExpanded(scope, "board", category)) { - items.forEach((item) => { - rows.push({ - type: "packet", - id: item.id, - data: item, - }); - - // If the packet is expanded, add its variables/measurements - if (get().isItemExpanded(scope, "packet", item.id)) { - if ("measurements" in item) { - const variables = item.measurements as Measurement[]; - variables.forEach((m) => { - rows.push({ - type: "variable", - id: `${item.id}-${m.id}`, - data: m, - packetId: item.id, - }); - }); - } - } - }); - } - }); - - return rows; - }, - // Filter dialog - filterDialog: { - isOpen: false, - scope: null, - }, - - openFilterDialog: (scope: FilterScope) => - set({ filterDialog: { isOpen: true, scope } }), - closeFilterDialog: () => - set({ filterDialog: { isOpen: false, scope: null } }), - - // Telemetry Charts - charts: { - "workspace-1": [], - "workspace-2": [], - "workspace-3": [], - }, - - setCharts: (charts) => set({ charts }), - - getActiveWorkspaceCharts: () => { - const id = get().getActiveWorkspaceId(); - if (!id) return EMPTY_ARRAY as WorkspaceChartConfig[]; - return get().charts[id] || EMPTY_ARRAY; - }, - - // Future-proofing Actions - addChart: (workspaceId) => { - const newChartId = crypto.randomUUID(); - set((state) => ({ - charts: { - ...state.charts, - [workspaceId]: [ - ...(state.charts[workspaceId] || []), - { id: newChartId, series: [], historyLimit: 200 }, - ], - }, - })); - return newChartId; - }, - - removeChart: (workspaceId, chartId) => - set((state) => ({ - charts: { - ...state.charts, - [workspaceId]: (state.charts[workspaceId] || []).filter( - (c) => c.id !== chartId, - ), - }, - })), - - clearCharts: () => { - const activeWorkspaceId = get().getActiveWorkspaceId(); - if (!activeWorkspaceId) return; - - set((state) => ({ - charts: { - ...state.charts, - [activeWorkspaceId]: [], - }, - })); - }, - - reorderCharts: (workspaceId, oldIndex, newIndex) => { - if (oldIndex < 0 || newIndex < 0) return; - - set((state) => { - const charts = [...(state.charts[workspaceId] || [])]; - const [removed] = charts.splice(oldIndex, 1); - charts.splice(newIndex, 0, removed); - return { - charts: { - ...state.charts, - [workspaceId]: charts, - }, - }; - }); - }, - - addSeriesToChart: (workspaceId, chartId, series) => - set((state) => ({ - charts: { - ...state.charts, - [workspaceId]: (state.charts[workspaceId] || []).map((c) => - c.id === chartId ? { ...c, series: [...c.series, series] } : c, - ), - }, - })), - - removeSeriesFromChart: (workspaceId, chartId, variable) => - set((state) => ({ - charts: { - ...state.charts, - [workspaceId]: (state.charts[workspaceId] || []).map((c) => - c.id === chartId - ? { ...c, series: c.series.filter((s) => s.variable !== variable) } - : c, - ), - }, - })), - - setChartHistoryLimit: (workspaceId, chartId, newHistoryLimit) => - set((state) => ({ - charts: { - ...state.charts, - [workspaceId]: (state.charts[workspaceId] || []).map((c) => - c.id === chartId ? { ...c, historyLimit: newHistoryLimit } : c, - ), - }, - })), - - // Key Bindings - addKeyBinding: (commandId, key, parameters) => { - const activeWorkspaceId = get().getActiveWorkspaceId(); - if (!activeWorkspaceId) return; - - const bindingId = crypto.randomUUID(); - - set((state) => { - const updatedWorkspaces = state.workspaces.map((workspace) => { - if (workspace.id === activeWorkspaceId) { - return { - ...workspace, - keyBindings: [ - ...(workspace.keyBindings || []), - { id: bindingId, commandId, key, parameters }, - ], - }; - } - return workspace; - }); - - const updatedActiveWorkspace = updatedWorkspaces.find( - (w) => w.id === activeWorkspaceId, - ); - - return { - workspaces: updatedWorkspaces, - activeWorkspace: updatedActiveWorkspace || state.activeWorkspace, - }; - }); - }, - - removeKeyBinding: (bindingId) => { - const activeWorkspaceId = get().getActiveWorkspaceId(); - if (!activeWorkspaceId) return; - - set((state) => { - const updatedWorkspaces = state.workspaces.map((workspace) => { - if (workspace.id === activeWorkspaceId) { - return { - ...workspace, - keyBindings: (workspace.keyBindings || []).filter( - (binding) => binding.id !== bindingId, - ), - }; - } - return workspace; - }); - - const updatedActiveWorkspace = updatedWorkspaces.find( - (w) => w.id === activeWorkspaceId, - ); - - return { - workspaces: updatedWorkspaces, - activeWorkspace: updatedActiveWorkspace || state.activeWorkspace, - }; - }); - }, - - getKeyBindings: () => { - const activeWorkspace = get().activeWorkspace; - return activeWorkspace?.keyBindings || []; - }, - - getCommandIdsByKey: (key) => { - const bindings = get().getKeyBindings(); - return bindings - .filter((binding) => binding.key === key) - .map((binding) => binding.commandId); - }, - - getKeyBindingForCommand: (commandId) => { - const bindings = get().getKeyBindings(); - const binding = bindings.find((b) => b.commandId === commandId); - return binding?.key; - }, - - getKeyBindingParameters: (commandId, key) => { - const bindings = get().getKeyBindings(); - const binding = bindings.find( - (b) => b.commandId === commandId && b.key === key, - ); - return binding?.parameters; - }, -}); diff --git a/frontend/testing-view/src/store/store.ts b/frontend/testing-view/src/store/store.ts index 4c17cf660..9ed8695ee 100644 --- a/frontend/testing-view/src/store/store.ts +++ b/frontend/testing-view/src/store/store.ts @@ -1,5 +1,21 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { + createChartsSlice, + type ChartsSlice, +} from "../features/charts/store/chartsSlice"; +import { + createFilteringSlice, + type FilteringSlice, +} from "../features/filtering/store/filteringSlice"; +import { + createRightSidebarSlice, + type RightSidebarSlice, +} from "../features/workspace/store/rightSidebarSlice"; +import { + createWorkspacesSlice, + type WorkspacesSlice, +} from "../features/workspace/store/workspacesSlice"; import { createAppSlice, type AppSlice } from "./slices/appSlice"; import { createCatalogSlice, type CatalogSlice } from "./slices/catalogSlice"; import { @@ -10,18 +26,10 @@ import { createMessagesSlice, type MessagesSlice, } from "./slices/messagesSlice"; -import { - createRightSidebarSlice, - type RightSidebarSlice, -} from "./slices/rightSidebarSlice"; import { createTelemetrySlice, type TelemetrySlice, } from "./slices/telemetrySlice"; -import { - createWorkspacesSlice, - type WorkspacesSlice, -} from "./slices/workspacesSlice"; export type Store = AppSlice & CatalogSlice & @@ -29,7 +37,9 @@ export type Store = AppSlice & TelemetrySlice & RightSidebarSlice & ConnectionsSlice & - MessagesSlice; + MessagesSlice & + ChartsSlice & + FilteringSlice; export const useStore = create()( // devtools( @@ -42,6 +52,8 @@ export const useStore = create()( ...createRightSidebarSlice(...a), ...createConnectionsSlice(...a), ...createMessagesSlice(...a), + ...createChartsSlice(...a), + ...createFilteringSlice(...a), }), { // Partial persist @@ -61,7 +73,7 @@ export const useStore = create()( // Workspace UI state activeTab: state.activeTab, - tabFilters: state.tabFilters, + tabFilters: state.workspaceFilters, }), }, ), diff --git a/frontend/testing-view/src/types/common/config.ts b/frontend/testing-view/src/types/common/config.ts index a3e28858a..0f2a862fa 100644 --- a/frontend/testing-view/src/types/common/config.ts +++ b/frontend/testing-view/src/types/common/config.ts @@ -1,5 +1,11 @@ +/** + * Type of the field value in the config data. + */ export type ConfigField = string | string[] | number | boolean; +/** + * Configuration type used in app slice to store values from the form. + */ export interface ConfigData { [section: string]: { [field: string]: ConfigField; diff --git a/frontend/testing-view/src/types/common/connection.ts b/frontend/testing-view/src/types/common/connection.ts index 0259f91dc..56a4c4445 100644 --- a/frontend/testing-view/src/types/common/connection.ts +++ b/frontend/testing-view/src/types/common/connection.ts @@ -1,4 +1,10 @@ +/** + * One connection to the vehicle. + */ export interface Connection { + /** Name of the connection */ name: string; + + /** Whether the connection is established */ isConnected: boolean; } diff --git a/frontend/testing-view/src/types/common/item.ts b/frontend/testing-view/src/types/common/item.ts index decb1d8e4..fd320b5ef 100644 --- a/frontend/testing-view/src/types/common/item.ts +++ b/frontend/testing-view/src/types/common/item.ts @@ -1,5 +1,18 @@ +/** + * Raw item from the catalog (commands or telemetry) as it arrives from the backend. + */ export interface Item { + /** Unique (If firmware fixes it) ID of the item from ADJ */ id: number; + + /** Name of the item from ADJ */ name: string; +} + +/** + * Item from the catalog (commands or telemetry) with my label. + */ +export interface CatalogItem extends Item { + /** Label of the item generated from the name on frontend startup */ label: string; } diff --git a/frontend/testing-view/src/types/common/logger.ts b/frontend/testing-view/src/types/common/logger.ts index d3f3c0d9a..0c364c272 100644 --- a/frontend/testing-view/src/types/common/logger.ts +++ b/frontend/testing-view/src/types/common/logger.ts @@ -1 +1,4 @@ +/** + * Status of the logger. + */ export type LoggerStatus = "standby" | "recording" | "loading" | "error"; diff --git a/frontend/testing-view/src/types/common/settings.ts b/frontend/testing-view/src/types/common/settings.ts index bca604420..bbe3e20df 100644 --- a/frontend/testing-view/src/types/common/settings.ts +++ b/frontend/testing-view/src/types/common/settings.ts @@ -1,3 +1,6 @@ +/** + * Type of the field component showed in the settings form and defined in the settings schema (configuration). + */ export type FieldType = | "text" | "number" @@ -6,21 +9,50 @@ export type FieldType = | "multi-checkbox" | "path"; +/** + * Generic props for a field component showed in the settings form.\ + * @template T - The type of the field value. + * **!IMPORTANT:** it is a type used for input component and possibly doesn't match the actual type of the field value.\ + * **E.g.** number field value T should be a string, but the actual type is number. + */ export interface FieldProps { + /** Field configuration */ field: SettingField; + /** Current value of the field */ value: T; + /** Function to handle the change of the field value */ onChange: (value: T) => void; } +/** + * One item of the settings schema from config.toml. + */ export interface SettingField { + /** Label of the field */ label: string; + + /** Type of the field */ type: FieldType; + + /** Options of the field */ options?: string[]; + + /** Placeholder of the field */ placeholder?: string; + + /** Path to the field in the config.toml configuration.\ + * **Note:** it should match config.toml section and variable name.\ + * **E.g.** `vehicle.boards` or `adj.branch`. */ path: string; } +/** + * One section of the settings from config.toml. + */ export interface SettingsSection { + /** Title of the section */ title: string; + + /** Fields of the section */ fields: SettingField[]; } diff --git a/frontend/testing-view/src/types/data/board.ts b/frontend/testing-view/src/types/data/board.ts index f1bfea21f..4c92463ad 100644 --- a/frontend/testing-view/src/types/data/board.ts +++ b/frontend/testing-view/src/types/data/board.ts @@ -1,15 +1,45 @@ import type { CommandCatalogItem } from "./commandCatalogItem"; import type { TelemetryCatalogItem } from "./telemetryCatalogItem"; +/** + * Name of a board, like LCU or HVBMS.\ + * I decided not to use array of predefined strings in case a new board is added in the future. + */ export type BoardName = string; + +/** + * Common properties of boards. + */ export interface BoardData { name: BoardName; } +/** + * Basically ADJ telemetry packets definition from the backend. + */ +export interface BoardPacketsData extends BoardData { + /** List of telemetry packets (also referred as packets in some places) */ + packets: TelemetryCatalogItem[]; +} + +/** + * Basically ADJ commands definition from the backend. + */ export interface BoardOrdersData extends BoardData { + /** List of commands (also referred as orders in some places) */ orders: CommandCatalogItem[]; } -export interface BoardPacketsData extends BoardData { - packets: TelemetryCatalogItem[]; +/** + * Final type of the result of the packets fetching. + */ +export interface PacketsData { + boards: BoardPacketsData[]; +} + +/** + * Final type of the result of the commands fetching. + */ +export interface OrdersData { + boards: BoardOrdersData[]; } diff --git a/frontend/testing-view/src/types/data/commandCatalogItem.ts b/frontend/testing-view/src/types/data/commandCatalogItem.ts index 0e0d2efad..923b2fae4 100644 --- a/frontend/testing-view/src/types/data/commandCatalogItem.ts +++ b/frontend/testing-view/src/types/data/commandCatalogItem.ts @@ -1,5 +1,8 @@ -import type { Item } from "../common/item"; +import type { CatalogItem, Item } from "../common/item"; +/** + * Params of a command. + */ export interface CommandParameter { kind: string; id: string; @@ -7,21 +10,37 @@ export interface CommandParameter { type: string; } +/** + * Numeric parameter has safe and warning ranges. + */ export interface NumericCommandParameter extends CommandParameter { safeRange: (number | null)[]; warningRange: (number | null)[]; } +/** + * Enum parameter has options. + */ export interface EnumCommandParameter extends CommandParameter { options: string[]; } +/** + * Map of parameter id to parameter type of a command.\ + * Each parameter can be either numeric or enum. + */ export interface CommandParameters { [key: string]: NumericCommandParameter | EnumCommandParameter; } +/** + * Definition of a command packet as it arrives from the backend. + */ export interface RawOrder extends Item { fields: CommandParameters; } -export type CommandCatalogItem = RawOrder; +/** + * Definition of a command catalog item as it arrives from the backend and my label. + */ +export type CommandCatalogItem = CatalogItem & RawOrder; diff --git a/frontend/testing-view/src/types/data/message.ts b/frontend/testing-view/src/types/data/message.ts index 25f7101fe..7d8c2998a 100644 --- a/frontend/testing-view/src/types/data/message.ts +++ b/frontend/testing-view/src/types/data/message.ts @@ -1,4 +1,15 @@ +import type { BoardName } from "./board"; + +/** + * Defines current moment in time.\ + * **Be careful** as there is no way to check if a timestamp is unique.\ + * Because seconds basically don't tell us anything in the context of high-frequency systems + * and counter only means uniqueness considering the same packet type + */ export interface MessageTimestamp { + /** Counter which is incremented by 1 every time packet of one type is generated,\ + * but it can be the same between different packet types + */ counter: number; second: number; minute: number; @@ -8,14 +19,42 @@ export interface MessageTimestamp { year: number; } +/** + * Possible payloads for a `ok`, `warning` and `fault` messages. + */ +export type DetailedPayload = + | { + kind: "LOWER_BOUND" | "UPPER_BOUND"; + data: { bound: number; value: number }; + } + | { kind: "OUT_OF_BOUNDS"; data: { bounds: [number, number]; value: number } } + | { kind: "EQUALS"; data: { value: number } } + | { kind: "NOT_EQUALS"; data: { want: number; value: number } } + | { + kind: "TIME_ACCUMULATION"; + data: { value: number; bound: number; timelimit: number }; + } + | { kind: "ERROR_HANDLER" | "WARNING"; data: string }; + +/** + * Definition of a MessagePacket as it arrives from the backend. + */ export interface MessagePacket { + /** Message type */ kind: "info" | "warning" | "fault" | "ok"; - payload: any; - board: string; + /** For `info` messages, the payload is a string. + * For `warning`, `fault` and `ok` messages, the payload is a DetailedPayload. + */ + payload: string | DetailedPayload; + board: BoardName; name: string; timestamp: MessageTimestamp; } +/** + * Message definition on frontend. + */ export interface Message extends MessagePacket { - id: string; // Generated on frontend for React keys + /** Unique message id generated on frontend for React keys */ + id: string; } diff --git a/frontend/testing-view/src/types/data/telemetryCatalogItem.ts b/frontend/testing-view/src/types/data/telemetryCatalogItem.ts index b847e9f94..a51593d90 100644 --- a/frontend/testing-view/src/types/data/telemetryCatalogItem.ts +++ b/frontend/testing-view/src/types/data/telemetryCatalogItem.ts @@ -1,20 +1,44 @@ -import type { Item } from "../common/item"; +import type { CatalogItem, Item } from "../common/item"; +/** + * Variable definition. Sometimes also called Measurement. This is the same thing. + */ export interface Variable { id: string; name: string; type: string; - units: string; + units?: string; } -export type Measurement = any; +export interface NumericVariable extends Variable { + safeRange: (number | null)[]; + warningRange: (number | null)[]; +} + +export interface EnumVariable extends Variable { + options: string[]; +} + +type BooleanVariable = Variable; +export type TelemetryVariable = + | NumericVariable + | EnumVariable + | BooleanVariable; + +/** + * Definition of a telemetry packet as it arrives from the backend. + */ export interface RawPacket extends Item { - hexValue: string; count: number; cycleTime: number; type: string; - measurements: Measurement[]; + measurements: TelemetryVariable[]; + /** Currently unused (always equals to "000000" placeholder on the backend) */ + hexValue: string; } -export type TelemetryCatalogItem = RawPacket; +/** + * Definition of a telemetry catalog item as it arrives from the backend and my label. + */ +export type TelemetryCatalogItem = CatalogItem & RawPacket; diff --git a/frontend/testing-view/src/types/data/transformedBoards.ts b/frontend/testing-view/src/types/data/transformedBoards.ts index 6b9582734..38e9658ea 100644 --- a/frontend/testing-view/src/types/data/transformedBoards.ts +++ b/frontend/testing-view/src/types/data/transformedBoards.ts @@ -1,19 +1,18 @@ -import type { BoardName, BoardOrdersData, BoardPacketsData } from "./board"; +import type { BoardName } from "./board"; import type { CommandCatalogItem } from "./commandCatalogItem"; import type { TelemetryCatalogItem } from "./telemetryCatalogItem"; -// Packets fetching return data type -export interface PacketsData { - boards: BoardPacketsData[]; -} +/** + * Final result of the useBoardData hook and boards transformation.\ + * This is the format I actually use + */ +export interface TransformedBoards { + /** Map of board name to list of telemetry catalog items */ + telemetryCatalog: Record; -// Commands fetching return data type -export interface OrdersData { - boards: BoardOrdersData[]; -} + /** Map of board name to list of command catalog items */ + commandsCatalog: Record; -export interface TransformedBoards { - telemetryCatalog: Record; - commandsCatalog: Record; + /** Set of all available boards (not used for now) */ boards: Set; } diff --git a/frontend/testing-view/src/types/data/virtualization.ts b/frontend/testing-view/src/types/data/virtualization.ts index 4b57ae220..d6989ef33 100644 --- a/frontend/testing-view/src/types/data/virtualization.ts +++ b/frontend/testing-view/src/types/data/virtualization.ts @@ -1,3 +1,6 @@ +/** + * A row of data in a virtualized list. + */ export type VirtualRow = | { type: "board"; id: string; label: string; count: number } | { type: "packet"; id: number; data: any } diff --git a/frontend/testing-view/src/types/telemetry/telemetry.ts b/frontend/testing-view/src/types/telemetry/telemetry.ts index bb879ba3d..330c63722 100644 --- a/frontend/testing-view/src/types/telemetry/telemetry.ts +++ b/frontend/testing-view/src/types/telemetry/telemetry.ts @@ -1,16 +1,12 @@ -export type Variables = Record< - string, - { last: number; average: number } | boolean | string | number ->; - -export interface TelemetryPacket { - count: number; - cycleTime: number; - hexValue: string; - id: number; - measurementUpdates: Variables; -} +import type { TelemetryPacket } from "@workspace/core"; +/** + * Map of telemetry packets per telemetry packetid. + */ export type TelemetryData = Record; +/** + * Full map of telemetry packets per telemetry packetid.\ + * This is not the same type as TelemetryData conceptually, but they match. + */ export type TelemetryState = Record; diff --git a/frontend/testing-view/src/types/workspace/charts.ts b/frontend/testing-view/src/types/workspace/charts.ts deleted file mode 100644 index 3a888b7f9..000000000 --- a/frontend/testing-view/src/types/workspace/charts.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type VariableSeries = { - packetId: number; - variable: string; -}; diff --git a/frontend/testing-view/src/types/workspace/filters.ts b/frontend/testing-view/src/types/workspace/filters.ts deleted file mode 100644 index 93cb06b86..000000000 --- a/frontend/testing-view/src/types/workspace/filters.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { BoardName } from "../data/board"; -import type { SidebarTab } from "./sidebar"; - -export type FilterScope = SidebarTab | "logs"; - -export type TabFilter = Record; - -export type WorkspaceFilters = Record; diff --git a/frontend/testing-view/src/types/workspace/keyBinding.ts b/frontend/testing-view/src/types/workspace/keyBinding.ts deleted file mode 100644 index 39c7423d4..000000000 --- a/frontend/testing-view/src/types/workspace/keyBinding.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface KeyBinding { - id: string; // UUID for this binding - commandId: number; // The command's ID - key: string; // The key (e.g., "1", "a") - parameters: Record; -} diff --git a/frontend/testing-view/src/types/workspace/sidebar.ts b/frontend/testing-view/src/types/workspace/sidebar.ts deleted file mode 100644 index 040cc6509..000000000 --- a/frontend/testing-view/src/types/workspace/sidebar.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { FilterScope } from "./filters"; - -export type SidebarTab = "commands" | "telemetry"; - -export type WorkspaceExpandedItems = Record>; diff --git a/frontend/testing-view/src/types/workspace/workspace.ts b/frontend/testing-view/src/types/workspace/workspace.ts deleted file mode 100644 index ea42e3d0b..000000000 --- a/frontend/testing-view/src/types/workspace/workspace.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { KeyBinding } from "./keyBinding"; - -export interface Workspace { - name: string; - id: string; - description: string; - keyBindings: KeyBinding[]; -}