Skip to content

Create and use new waterfall data format #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 48 additions & 15 deletions frontend/src/apiClient/visualizationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
CaptureSourceSchema,
CaptureSchema,
} from './captureService';
import { RadioHoundFileSchema } from '../components/waterfall/types';
import {
WaterfallFileSchema,
WaterfallFile,
} from '../components/waterfall/types';
import { FilesWithContent } from '../components/types';
import JSZip from 'jszip';

Expand Down Expand Up @@ -211,20 +214,6 @@ export const useVisualizationFiles = (vizRecord: VisualizationRecordDetail) => {
let parsedContent: unknown = content;
let isValid: boolean | undefined;

// Validate RadioHound files
if (vizRecord.capture_type === 'rh') {
parsedContent = JSON.parse(await content.text());
const validationResult =
RadioHoundFileSchema.safeParse(parsedContent);
isValid = validationResult.success;

if (!isValid) {
console.warn(
`Invalid RadioHound file content for ${file.name}: ${validationResult.error}`,
);
}
}

// Add the file to our files object
files[file.uuid] = {
uuid: file.uuid,
Expand All @@ -248,3 +237,47 @@ export const useVisualizationFiles = (vizRecord: VisualizationRecordDetail) => {

return { files, isLoading, error };
};

/**
* Fetches waterfall data for a visualization from the backend.
* @param id - The ID of the visualization
* @returns An array of WaterfallFile objects containing the waterfall data
* @throws Error if the request fails or the response data is invalid
*/
export const getWaterfallData = async (
id: string,
): Promise<WaterfallFile[]> => {
try {
const response = await apiClient.get(
`/api/visualizations/${id}/get_waterfall_data/`,
);
return zod.array(WaterfallFileSchema).parse(response.data);
} catch (error) {
console.error('Error fetching waterfall data:', error);
throw error;
}
};

export const useWaterfallData = (id: string) => {
const [waterfallData, setWaterfallData] = useState<WaterfallFile[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchWaterfallData = async () => {
setIsLoading(true);
try {
const data = await getWaterfallData(id);
setWaterfallData(data);
} catch (error) {
console.error('Error fetching waterfall data:', error);
setError('Failed to fetch waterfall data');
} finally {
setIsLoading(false);
}
};
fetchWaterfallData();
}, [id]);

return { waterfallData, isLoading, error };
};
37 changes: 20 additions & 17 deletions frontend/src/components/waterfall/ScanDetailsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Table } from 'react-bootstrap';
import _ from 'lodash';

import { formatHertz } from '../../utils/utils';
import { RadioHoundFile } from './types';
import { WaterfallFile } from './types';

interface DetailRowProps {
label: string;
Expand All @@ -19,10 +19,10 @@ function DetailRow({ label, value }: DetailRowProps): JSX.Element {
}

interface ScanDetailsProps {
rhFile: RadioHoundFile;
waterfallFile: WaterfallFile;
}

export function ScanDetails({ rhFile }: ScanDetailsProps): JSX.Element {
export function ScanDetails({ waterfallFile }: ScanDetailsProps): JSX.Element {
// const downloadUrl = useMemo(() => {
// const blob = new Blob([JSON.stringify(capture, null, 4)], {
// type: 'application/json',
Expand All @@ -47,7 +47,7 @@ export function ScanDetails({ rhFile }: ScanDetailsProps): JSX.Element {

// Helper function to safely get nested values
const getScanValue = <T,>(path: string, defaultValue?: T): T | undefined => {
return _.get(rhFile, path, defaultValue) as T;
return _.get(waterfallFile, path, defaultValue) as T;
};

return (
Expand All @@ -57,7 +57,7 @@ export function ScanDetails({ rhFile }: ScanDetailsProps): JSX.Element {
<tbody>
<DetailRow
label="Node"
value={`${getScanValue('short_name')}${
value={`${getScanValue('device_name')}${
getScanValue('mac_address')
? ` (${getScanValue('mac_address')})`
: ''
Expand All @@ -66,10 +66,10 @@ export function ScanDetails({ rhFile }: ScanDetailsProps): JSX.Element {
<DetailRow
label="Scan Time"
value={
getScanValue('metadata.scan_time')
getScanValue('custom_fields.scan_time')
? `${
Math.round(
Number(getScanValue('metadata.scan_time')) * 1000,
Number(getScanValue('custom_fields.scan_time')) * 1000,
) / 1000
}s`
: undefined
Expand All @@ -80,24 +80,24 @@ export function ScanDetails({ rhFile }: ScanDetailsProps): JSX.Element {
<DetailRow
label="Frequency Minimum"
value={
getScanValue('metadata.fmin')
? formatHertz(getScanValue('metadata.fmin') as number)
getScanValue('min_frequency')
? formatHertz(getScanValue('min_frequency') as number)
: undefined
}
/>
<DetailRow
label="Frequency Maximum"
value={
getScanValue('metadata.fmax')
? formatHertz(getScanValue('metadata.fmax') as number)
getScanValue('max_frequency')
? formatHertz(getScanValue('max_frequency') as number)
: undefined
}
/>
<DetailRow
label="Number of Samples"
value={
typeof getScanValue('metadata.xcount') === 'number'
? (getScanValue('metadata.xcount') as number).toLocaleString()
typeof getScanValue('num_samples') === 'number'
? (getScanValue('num_samples') as number).toLocaleString()
: undefined
}
/>
Expand All @@ -112,17 +112,20 @@ export function ScanDetails({ rhFile }: ScanDetailsProps): JSX.Element {
<DetailRow
label="GPS Lock"
value={
getScanValue('metadata.gps_lock') !== undefined
? getScanValue('metadata.gps_lock')
getScanValue('custom_fields.gps_lock') !== undefined
? getScanValue('custom_fields.gps_lock')
? 'True'
: 'False'
: undefined
}
/>
<DetailRow label="Job" value={getScanValue('metadata.name')} />
<DetailRow
label="Job"
value={getScanValue('custom_fields.job_name')}
/>
<DetailRow
label="Comments"
value={getScanValue('metadata.comments')}
value={getScanValue('custom_fields.comments')}
/>
</tbody>
</Table>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/waterfall/WaterfallPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useRef, useEffect, useState } from 'react';
import _ from 'lodash';
import { scaleLinear, interpolateHslLong, rgb } from 'd3';

import { ScanState, WaterfallType, Display } from './types';
import { ScanState, ScanWaterfallType, Display } from './types';
import { WATERFALL_MAX_ROWS } from './index';

const SCROLL_INDICATOR_SIZE = 15;
Expand Down Expand Up @@ -31,7 +31,7 @@ const downIndicatorStyle: React.CSSProperties = {
interface WaterfallPlotProps {
scan: ScanState;
display: Display;
setWaterfall: (waterfall: WaterfallType) => void;
setWaterfall: (waterfall: ScanWaterfallType) => void;
setScaleChanged: (scaleChanged: boolean) => void;
setResetScale: (resetScale: boolean) => void;
currentFileIndex: number;
Expand Down
27 changes: 13 additions & 14 deletions frontend/src/components/waterfall/WaterfallVizContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { useState } from 'react';
import { Alert, Row, Col } from 'react-bootstrap';

import { WaterfallVisualization } from '.';
import { RadioHoundFile } from './types';
import WaterfallControls from './WaterfallControls';
import ScanDetailsTable from './ScanDetailsTable';
import { VizContainerProps } from '../types';
import { useVisualizationFiles } from '../../apiClient/visualizationService';
import { useWaterfallData } from '../../apiClient/visualizationService';
import LoadingBlock from '../LoadingBlock';

export interface WaterfallSettings {
Expand All @@ -18,8 +17,9 @@ export interface WaterfallSettings {
export const WaterfallVizContainer = ({
visualizationRecord,
}: VizContainerProps) => {
const { files, isLoading, error } =
useVisualizationFiles(visualizationRecord);
const { waterfallData, isLoading, error } = useWaterfallData(
visualizationRecord.uuid,
);
const [settings, setSettings] = useState<WaterfallSettings>({
fileIndex: 0,
isPlaying: false,
Expand All @@ -36,7 +36,7 @@ export const WaterfallVizContainer = ({
);
}

if (Object.keys(files).length === 0) {
if (waterfallData.length === 0) {
return (
<Alert variant="warning">
<Alert.Heading>No Data Found</Alert.Heading>
Expand All @@ -47,12 +47,9 @@ export const WaterfallVizContainer = ({

// We currently only support one capture per visualization, so grab the first
// capture and use its files
const rhFiles = visualizationRecord.captures[0].files
.map((file) => files[file.uuid].fileContent as RadioHoundFile)
.sort(
(a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
);
const waterfallFiles = waterfallData.sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
);

return (
<div>
Expand All @@ -62,20 +59,22 @@ export const WaterfallVizContainer = ({
<WaterfallControls
settings={settings}
setSettings={setSettings}
numFiles={rhFiles.length}
numFiles={waterfallFiles.length}
/>
</div>
</Col>
<Col>
<Row>
<WaterfallVisualization
rhFiles={rhFiles}
waterfallFiles={waterfallFiles}
settings={settings}
setSettings={setSettings}
/>
</Row>
<Row>
<ScanDetailsTable rhFile={rhFiles[settings.fileIndex]} />
<ScanDetailsTable
waterfallFile={waterfallFiles[settings.fileIndex]}
/>
</Row>
</Col>
</Row>
Expand Down
Loading