diff --git a/src/App.tsx b/src/App.tsx index c271bfc..c8e35bf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { SearchResults } from './components/SearchResults'; import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorBoundaryWrapper } from './components/ErrorBoundaryWrapper'; import { Footer } from './Footer'; +import { IS_CROSSMATCH_ENABLED } from './configs/constants'; function App() { const location = useLocation(); @@ -35,8 +36,14 @@ function App() { } /> } /> - } /> - } /> + {/* Gated like the Cross Matcher nav button, so a disabled deployment can't reach these + pages by URL either; they fall through to PageNotFound instead */} + {IS_CROSSMATCH_ENABLED && ( + <> + } /> + } /> + + )} } /> diff --git a/src/components/NearbySourcesSection.tsx b/src/components/NearbySourcesSection.tsx index c5317c2..e55cb88 100644 --- a/src/components/NearbySourcesSection.tsx +++ b/src/components/NearbySourcesSection.tsx @@ -52,11 +52,8 @@ export function NearbySourcesSection({ {s.name}

- {angularSeparationDeg( - sourceRa, - sourceDec, - s.ra, - s.dec + {( + angularSeparationDeg(sourceRa, sourceDec, s.ra, s.dec) * 60 ).toFixed(2)}{' '} arcmin

diff --git a/src/components/RangeInput.tsx b/src/components/RangeInput.tsx index e8387d4..23ea2cb 100644 --- a/src/components/RangeInput.tsx +++ b/src/components/RangeInput.tsx @@ -51,6 +51,7 @@ export function RangeInput({ onChange={handleChange} onMouseUp={handleCommit} onTouchEnd={handleCommit} + onKeyUp={handleCommit} /> ); diff --git a/src/components/SearchResults.tsx b/src/components/SearchResults.tsx index 6aff23b..92f2b89 100644 --- a/src/components/SearchResults.tsx +++ b/src/components/SearchResults.tsx @@ -84,6 +84,7 @@ export function SearchResults() { )} ( - + {row.original.source_id} ), @@ -99,8 +103,8 @@ export function SearchResults() { accessorFn: (row) => row.source_id, }, { - header: 'ra', - // accessorFn: (row) => row.ra.toFixed(1), + header: 'RA (deg)', + accessorFn: (row) => row.ra, cell: ({ row }) => ( {row.original.ra.toFixed(3)} @@ -109,8 +113,8 @@ export function SearchResults() { size: 125, }, { - header: 'dec', - // accessorFn: (row) => row.dec.toFixed(1), + header: 'Dec (deg)', + accessorFn: (row) => row.dec, cell: ({ row }) => ( {row.original.dec.toFixed(3)} diff --git a/src/components/Sources.tsx b/src/components/Sources.tsx deleted file mode 100644 index 751e3e8..0000000 --- a/src/components/Sources.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useMemo, useState } from 'react'; -import { SourcesFeedItem, SourcesFeedResponse } from '../types'; -import { Table } from './Table'; -import { Link } from 'react-router'; -import { ColumnDef, InitialTableState } from '@tanstack/react-table'; -import { useQuery } from '../hooks/useQuery'; -import { getNanoPlotSVG } from '../utils/nanoPlotHelpers'; -import { DEFAULT_SOURCES_PER_PAGE } from '../configs/constants'; -import { lightcurveApi } from '../api/client'; - -/** - * Renders a Table of sources returned by the /sources endpoint - */ -export function Sources() { - const [currentPageNumber, setCurrentPageNumber] = useState(1); - - const { data, isLoading } = useQuery< - | { sources: SourcesFeedResponse['items']; totalNumberOfSources: number } - | undefined - >({ - initialData: undefined, - queryKey: [currentPageNumber], - queryFn: async () => { - const start = - currentPageNumber === 1 - ? 0 - : (currentPageNumber - 1) * DEFAULT_SOURCES_PER_PAGE; - - const sourcesFeedResponse = await lightcurveApi.getSourcesFeed(start); - - const sourcesWithNanoplot = (await Promise.all( - sourcesFeedResponse.items.map(async (r) => { - const plotData = { - time: r.time, - flux: r.flux, - }; - const plot = await getNanoPlotSVG(plotData); - return { - ...r, - nanoplot: plot ?? '', - }; - }) - )) as SourcesFeedResponse['items']; - - return { - sources: sourcesWithNanoplot, - totalNumberOfSources: sourcesFeedResponse.total_number_of_sources, - }; - }, - }); - - const initialState: InitialTableState = useMemo(() => { - return { - pagination: { - pageSize: DEFAULT_SOURCES_PER_PAGE, - }, - }; - }, []); - - const customPaginationState = useMemo(() => { - return { - totalItems: data?.totalNumberOfSources ?? 0, - itemsPerPage: DEFAULT_SOURCES_PER_PAGE, - currentPageNumber, - setCurrentPageNumber, - }; - }, [data?.totalNumberOfSources, currentPageNumber]); - - return ( -
( - - {row.original.source_id} - - ), - size: 50, - accessorFn: (row) => row.source_id, - }, - { - header: 'ra', - // accessorFn: (row) => row.ra.toFixed(3), - cell: ({ row }) => ( - - {row.original.ra.toFixed(3)} - - ), - size: 50, - }, - { - header: 'dec', - // accessorFn: (row) => row.dec.toFixed(3), - cell: ({ row }) => ( - - {row.original.dec.toFixed(3)} - - ), - size: 50, - }, - { - header: 'recent data', - size: 200, - cell: ({ row }) => ( -
- ), - }, - ] as ColumnDef[] - } - /> - ); -} diff --git a/src/components/UnassignedSource.tsx b/src/components/UnassignedSource.tsx index ce93795..9f6e8f5 100644 --- a/src/components/UnassignedSource.tsx +++ b/src/components/UnassignedSource.tsx @@ -111,26 +111,30 @@ export function UnassignedSource() { STATUS_TO_EXCLUDE_SIMBAD_SEARCH.includes(data.source.status) ) return { data: [], queryTimestamp: undefined }; - let matches: SimbadMatch[] = []; + // Any failure here (network error, non-2xx status, or an unexpected response shape) is + // surfaced in the SIMBAD card below rather than shown as an empty match list, which would + // read as "nothing nearby" and could lead a reviewer to wrongly rule out an external match. const getSimbadHits = await fetch( SIMBAD_BASE_URL + '/cone' + `?ra=${toSimbadRa(data.source.ra)}&dec=${data.source.dec}&sr=${UNASSIGNED_BEAM_RADIUS_ARCMIN / 60}&verb=2&maxrec=10&responseformat=json&order_by=nb_ref&order_dir=desc` ); - try { - const hitsJson = (await getSimbadHits.json()) as SimbadConeResponse; - matches = hitsJson.data.map((row) => ({ - identifier: row[1] ?? 'SIMBAD object', - objectType: row[4] ?? null, - ra: row[2] ?? null, - dec: row[3] ?? null, - separationArcmin: row[0] != null ? row[0] * 60 : null, - })); - } catch { - console.error( - 'An error occurred when querying for nearby SIMBAD sources.' + if (!getSimbadHits.ok) { + throw new Error( + `SIMBAD returned ${getSimbadHits.status} ${getSimbadHits.statusText}` ); } + const hitsJson = (await getSimbadHits.json()) as SimbadConeResponse; + if (!Array.isArray(hitsJson?.data)) { + throw new Error('SIMBAD returned an unexpected response.'); + } + const matches: SimbadMatch[] = hitsJson.data.map((row) => ({ + identifier: row[1] ?? 'SIMBAD object', + objectType: row[4] ?? null, + ra: row[2] ?? null, + dec: row[3] ?? null, + separationArcmin: row[0] != null ? row[0] * 60 : null, + })); return { // ISO 8601, not a locale-formatted string: this doubles as the `queried_at` sent to the // server's external_evidence.queried_at (a datetime field) if the reviewer accepts one @@ -244,10 +248,6 @@ export function UnassignedSource() { throw new Error(error.message); } - if (simbadError) { - throw new Error(simbadError.message); - } - return (
{isSimbadQueryActive ? (

Loading...

+ ) : simbadError ? ( +

+ The SIMBAD search failed, so potential matches couldn’t be + checked. Reload the page to try again. +

) : ( <>

diff --git a/src/components/UnassignedSourcesTable.tsx b/src/components/UnassignedSourcesTable.tsx index 51fc94f..c751f62 100644 --- a/src/components/UnassignedSourcesTable.tsx +++ b/src/components/UnassignedSourcesTable.tsx @@ -132,12 +132,13 @@ export function UnassignedSourcesTable() {

{tableData.length}{' '} {statusFilter === INCLUDE_ALL_STATUSES - ? 'detected' - : formatSourceStatus(statusFilter).toLowerCase()}{' '} - sources awaiting review + ? 'sources' + : statusFilter === 'unmatched' + ? 'sources awaiting review' + : `${formatSourceStatus(statusFilter).toLowerCase()} sources`}

diff --git a/tests/App.test.tsx b/tests/App.test.tsx index b4c2221..cbdc8b7 100644 --- a/tests/App.test.tsx +++ b/tests/App.test.tsx @@ -31,4 +31,17 @@ describe('App', () => { screen.getByRole('heading', { name: /page not found/i }) ).toBeInTheDocument(); }); + + it('renders the not-found page for cross-match routes when cross-matching is disabled', () => { + // VITE_CROSSMATCH_ENABLED isn't set in the test environment + render( + + + + ); + + expect( + screen.getByRole('heading', { name: /page not found/i }) + ).toBeInTheDocument(); + }); }); diff --git a/tests/components/RangeInput.test.tsx b/tests/components/RangeInput.test.tsx index baba74a..76972a9 100644 --- a/tests/components/RangeInput.test.tsx +++ b/tests/components/RangeInput.test.tsx @@ -100,4 +100,27 @@ describe('RangeInput', () => { expect(onFinalChange).toHaveBeenCalledTimes(1); expect(onFinalChange).toHaveBeenCalledWith(9); }); + + it('calls onFinalChange after a keyboard change', () => { + const onFinalChange = vi.fn(); + + render( + + ); + + const slider = screen.getByRole('slider'); + // The browser fires change on keydown, then keyup once the key is released + fireEvent.change(slider, { target: { value: '6' } }); + fireEvent.keyUp(slider, { key: 'ArrowRight' }); + vi.advanceTimersByTime(100); + + expect(onFinalChange).toHaveBeenCalledTimes(1); + expect(onFinalChange).toHaveBeenCalledWith(6); + }); });