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);
+ });
});