From be48cd057f55ece5a4c550a013854dca457e9557 Mon Sep 17 00:00:00 2001 From: kraysent Date: Mon, 3 Aug 2026 14:30:19 +0100 Subject: [PATCH 1/4] Add admin task for merging PGCs --- src/App.tsx | 4 + src/components/ui/Navbar.tsx | 21 ++ src/pages/Admin.tsx | 37 +++ src/pages/AdminMergePgc.tsx | 563 +++++++++++++++++++++++++++++++++++ 4 files changed, 625 insertions(+) create mode 100644 src/pages/Admin.tsx create mode 100644 src/pages/AdminMergePgc.tsx diff --git a/src/App.tsx b/src/App.tsx index 81bc457..99a06a2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,8 @@ import { Layout } from "./components/ui/Layout"; import { SearchBar } from "./components/ui/Searchbar"; import { LoginPage } from "./pages/Login"; import { TableDetailsPage } from "./pages/TableDetails"; +import { AdminPage } from "./pages/Admin"; +import { AdminMergePgcPage } from "./pages/AdminMergePgc"; function App() { return ( @@ -45,6 +47,8 @@ function App() { element={} /> } /> + } /> + } /> } /> , + label: "Admin", + end: false, +}; + const configuredProductionWeb = "https://leda.sao.ru"; function openCurrentPathOnOrigin(productionWebInput: string): void { @@ -106,6 +114,19 @@ export function Navbar() { ))} + {isLoggedIn() ? ( + + + sidebarRailControlClassName(isActive) + } + > + {adminNavItem.icon} + + + ) : null}
{showOpenProductionButton ? ( diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx new file mode 100644 index 0000000..7516b73 --- /dev/null +++ b/src/pages/Admin.tsx @@ -0,0 +1,37 @@ +import { ReactElement, useEffect } from "react"; +import { Navigate } from "react-router-dom"; +import { isLoggedIn } from "../auth/token"; +import { Link } from "../components/core/Link"; + +const tasks = [ + { + to: "/admin/merge-pgc", + title: "Merge PGC objects", + description: + "Reassign all records from a source PGC onto a surviving target PGC.", + }, +]; + +export function AdminPage(): ReactElement { + useEffect(() => { + document.title = "Admin | HyperLEDA"; + }, []); + + if (!isLoggedIn()) { + return ; + } + + return ( +
+

Admin

+
    + {tasks.map((task) => ( +
  • + {task.title} +

    {task.description}

    +
  • + ))} +
+
+ ); +} diff --git a/src/pages/AdminMergePgc.tsx b/src/pages/AdminMergePgc.tsx new file mode 100644 index 0000000..00b5aac --- /dev/null +++ b/src/pages/AdminMergePgc.tsx @@ -0,0 +1,563 @@ +import { ReactElement, ReactNode, useEffect, useRef, useState } from "react"; +import { Navigate } from "react-router-dom"; +import { isLoggedIn } from "../auth/token"; +import { mergePgcs } from "../clients/admin/sdk.gen"; +import { querySimple } from "../clients/backend/sdk.gen"; +import { Catalogs, PgcObject, Schema } from "../clients/backend/types.gen"; +import { adminClient, backendClient } from "../clients/config"; +import { AladinViewer } from "../components/core/Aladin"; +import { + Declination, + QuantityWithError, + RightAscension, +} from "../components/core/Astronomy"; +import { Button } from "../components/core/Button"; +import { Link } from "../components/core/Link"; + +const MIN_ALADIN_FOV_DEG = 0.05; +const ALADIN_FOV_PADDING = 1.4; +const SEARCH_DEBOUNCE_MS = 300; +const NAME_SUGGESTION_LIMIT = 5; + +type SkySource = { + ra: number; + dec: number; + label: string; + id: number; +}; + +function skyViewForSources(sources: SkySource[]): { + ra: number; + dec: number; + fov: number; +} | null { + if (sources.length === 0) { + return null; + } + + const ra = + sources.reduce((sum, source) => sum + source.ra, 0) / sources.length; + const dec = + sources.reduce((sum, source) => sum + source.dec, 0) / sources.length; + + if (sources.length === 1) { + return { ra, dec, fov: MIN_ALADIN_FOV_DEG }; + } + + const raSpan = + Math.max(...sources.map((source) => source.ra)) - + Math.min(...sources.map((source) => source.ra)); + const decSpan = + Math.max(...sources.map((source) => source.dec)) - + Math.min(...sources.map((source) => source.dec)); + const fov = + Math.max(raSpan, decSpan, MIN_ALADIN_FOV_DEG) * ALADIN_FOV_PADDING; + + return { ra, dec, fov }; +} + +function objectToSkySource(object: PgcObject, role: string): SkySource | null { + const equatorial = object.catalogs.coordinates?.equatorial; + if (equatorial?.ra === undefined || equatorial?.dec === undefined) { + return null; + } + + const name = object.catalogs.designation?.name || `PGC ${object.pgc}`; + return { + ra: equatorial.ra, + dec: equatorial.dec, + label: `${name} (${role})`, + id: object.pgc, + }; +} + +function isPgcNumberInput(value: string): boolean { + return /^\d+$/.test(value.trim()); +} + +function objectLabel(object: PgcObject): string { + return object.catalogs.designation?.name || `PGC ${object.pgc}`; +} + +async function fetchPgc( + pgc: number, +): Promise<{ object: PgcObject; schema: Schema }> { + const response = await querySimple({ + client: backendClient, + query: { + pgcs: [pgc], + }, + }); + + if (response.error || !response.data) { + const err = response.error; + throw new Error( + `Error during query: ${typeof err === "object" ? JSON.stringify(err) : err}`, + ); + } + + const objects = response.data.data.objects; + const schema = response.data.data.schema; + const object = objects?.[0]; + + if (!object || Object.keys(object.catalogs).length === 0) { + throw new Error(`Object PGC ${pgc} not found`); + } + + return { object, schema }; +} + +async function fetchByName( + name: string, + pageSize: number, +): Promise<{ objects: PgcObject[]; schema: Schema }> { + const response = await querySimple({ + client: backendClient, + query: { + name, + page: 0, + page_size: pageSize, + }, + }); + + if (response.error || !response.data) { + const err = response.error; + throw new Error( + `Error during query: ${typeof err === "object" ? JSON.stringify(err) : err}`, + ); + } + + return { + objects: response.data.data.objects, + schema: response.data.data.schema, + }; +} + +function ObjectSummary({ + catalogs, + schema, + name, +}: { + catalogs: Catalogs; + schema: Schema; + name: ReactNode; +}): ReactElement { + const equatorial = catalogs.coordinates?.equatorial; + const redshift = catalogs.redshift; + + return ( +
+
Name
+
{name}
+ {equatorial ? ( + <> +
RA
+
+ + + +
+
Dec
+
+ + + +
+ + ) : null} + {redshift ? ( + <> +
Redshift
+
+ + {redshift.z.toFixed(5)} + +
+ + ) : null} +
+ ); +} + +interface PgcSelection { + object: PgcObject; + schema: Schema; +} + +interface PgcPickerProps { + label: string; + selection: PgcSelection | null; + onSelect: (selection: PgcSelection | null) => void; + disabled?: boolean; +} + +function PgcPicker({ + label, + selection, + onSelect, + disabled, +}: PgcPickerProps): ReactElement { + const [query, setQuery] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [suggestions, setSuggestions] = useState([]); + const debounceRef = useRef | null>(null); + const requestIdRef = useRef(0); + + useEffect( + () => () => { + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + requestIdRef.current += 1; + }, + [], + ); + + function selectResult(result: PgcSelection): void { + onSelect(result); + setQuery(""); + setSuggestions([]); + setError(null); + } + + async function runSearch(raw: string): Promise { + const trimmed = raw.trim(); + if (!trimmed) { + setSuggestions([]); + setError(null); + setLoading(false); + return; + } + + const requestId = ++requestIdRef.current; + setLoading(true); + setError(null); + + try { + if (isPgcNumberInput(trimmed)) { + setSuggestions([]); + const pgc = Number.parseInt(trimmed, 10); + if (pgc <= 0) { + throw new Error("Enter a valid PGC number"); + } + const result = await fetchPgc(pgc); + if (requestId !== requestIdRef.current) { + return; + } + onSelect(result); + setError(null); + } else { + const { objects, schema } = await fetchByName( + trimmed, + NAME_SUGGESTION_LIMIT, + ); + if (requestId !== requestIdRef.current) { + return; + } + const next = objects + .filter((object) => Object.keys(object.catalogs).length > 0) + .slice(0, NAME_SUGGESTION_LIMIT) + .map((object) => ({ object, schema })); + setSuggestions(next); + if (next.length === 0) { + setError(`No objects found for "${trimmed}"`); + } + } + } catch (err) { + if (requestId !== requestIdRef.current) { + return; + } + setSuggestions([]); + setError(err instanceof Error ? err.message : String(err)); + } finally { + if (requestId === requestIdRef.current) { + setLoading(false); + } + } + } + + function scheduleSearch(value: string): void { + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + debounceRef.current = setTimeout(() => { + debounceRef.current = null; + void runSearch(value); + }, SEARCH_DEBOUNCE_MS); + } + + function handleQueryChange(value: string): void { + setQuery(value); + setError(null); + setSuggestions([]); + scheduleSearch(value); + } + + function clearSelection(): void { + requestIdRef.current += 1; + if (debounceRef.current) { + clearTimeout(debounceRef.current); + debounceRef.current = null; + } + onSelect(null); + setError(null); + setQuery(""); + setSuggestions([]); + setLoading(false); + } + + return ( +
+

{label}

+
+ handleQueryChange(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + if (debounceRef.current) { + clearTimeout(debounceRef.current); + debounceRef.current = null; + } + if (suggestions.length > 0) { + selectResult(suggestions[0]); + return; + } + void runSearch(query); + } + if (event.key === "Escape") { + setSuggestions([]); + } + }} + className="bg-surface-2 border border-border rounded px-3 py-2 text-sm text-primary placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent w-full" + /> + {loading || suggestions.length > 0 ? ( +
    + {loading ? ( +
  • +
    +
  • + ) : ( + suggestions.map((suggestion) => ( +
  • + +
  • + )) + )} +
+ ) : null} +
+ {error ? ( +

+ {error} +

+ ) : null} + {selection ? ( +
+
+ + PGC {selection.object.pgc} + + +
+ + {objectLabel(selection.object)} + + } + /> +
+ ) : null} +
+ ); +} + +export function AdminMergePgcPage(): ReactElement { + const [target, setTarget] = useState(null); + const [source, setSource] = useState(null); + const [merging, setMerging] = useState(false); + const [mergeError, setMergeError] = useState(null); + const [mergeSuccess, setMergeSuccess] = useState(null); + + useEffect(() => { + document.title = "Merge PGC objects | HyperLEDA"; + }, []); + + if (!isLoggedIn()) { + return ; + } + + const samePgc = + target !== null && + source !== null && + target.object.pgc === source.object.pgc; + const canMerge = target !== null && source !== null && !samePgc && !merging; + + const skySources: SkySource[] = []; + if (target) { + const sky = objectToSkySource(target.object, "target"); + if (sky) { + skySources.push(sky); + } + } + if (source) { + const sky = objectToSkySource(source.object, "source"); + if (sky) { + skySources.push(sky); + } + } + + const skyView = + target && source && skySources.length > 0 + ? skyViewForSources(skySources) + : null; + + async function handleMerge(): Promise { + if (!target || !source || target.object.pgc === source.object.pgc) { + return; + } + + setMerging(true); + setMergeError(null); + setMergeSuccess(null); + + try { + const response = await mergePgcs({ + client: adminClient, + body: { + target_pgc: target.object.pgc, + source_pgcs: [source.object.pgc], + }, + }); + + if (response.error || !response.data?.data) { + throw new Error( + typeof response.error === "object" + ? JSON.stringify(response.error) + : String(response.error || "Unknown error"), + ); + } + + const result = response.data.data; + setMergeSuccess( + `Merged PGC ${result.merged_pgcs.join(", ")} into PGC ${result.target_pgc}. Reassigned ${result.reassigned_records} record(s).`, + ); + setSource(null); + } catch (err) { + setMergeError(err instanceof Error ? err.message : String(err)); + } finally { + setMerging(false); + } + } + + return ( +
+
+

Merge PGC objects

+

+ Select a target PGC (survives) and a source PGC (records are + reassigned, then the source disappears). +

+
+ +
+ { + setTarget(selection); + setMergeError(null); + setMergeSuccess(null); + }} + disabled={merging} + /> + { + setSource(selection); + setMergeError(null); + setMergeSuccess(null); + }} + disabled={merging} + /> +
+ + {samePgc ? ( +

+ Target and source must be different PGC numbers. +

+ ) : null} + + {skyView ? ( + + ) : null} + +
+ + {mergeError ? ( +

+ {mergeError} +

+ ) : null} + {mergeSuccess ? ( +

+ {mergeSuccess} +

+ ) : null} +
+
+ ); +} From ed673c4d2a3f52422d1bfc328d49a47e081fc8d4 Mon Sep 17 00:00:00 2001 From: kraysent Date: Mon, 3 Aug 2026 14:43:32 +0100 Subject: [PATCH 2/4] add common suggestable input component --- src/components/core/SuggestibleInput.tsx | 64 +++++++++++ src/components/ui/Searchbar.tsx | 53 ++++++--- src/pages/AdminMergePgc.tsx | 133 +++++++++++------------ 3 files changed, 167 insertions(+), 83 deletions(-) create mode 100644 src/components/core/SuggestibleInput.tsx diff --git a/src/components/core/SuggestibleInput.tsx b/src/components/core/SuggestibleInput.tsx new file mode 100644 index 0000000..eb63ad3 --- /dev/null +++ b/src/components/core/SuggestibleInput.tsx @@ -0,0 +1,64 @@ +import { + KeyboardEventHandler, + FocusEventHandler, + ReactElement, + ReactNode, +} from "react"; +import classNames from "classnames"; + +interface SuggestibleInputProps { + value: string; + onChange: (value: string) => void; + getSuggestions: (value: string) => ReactNode[]; + placeholder?: string; + disabled?: boolean; + className?: string; + onKeyDown?: KeyboardEventHandler; + onFocus?: FocusEventHandler; + onBlur?: FocusEventHandler; +} + +export function SuggestibleInput({ + value, + onChange, + getSuggestions, + placeholder, + disabled, + className, + onKeyDown, + onFocus, + onBlur, +}: SuggestibleInputProps): ReactElement { + const suggestions = getSuggestions(value); + + return ( +
+ onChange(event.target.value)} + onKeyDown={onKeyDown} + onFocus={onFocus} + onBlur={onBlur} + className={classNames( + "bg-surface-2 border border-border rounded px-3 py-2 w-full text-sm text-primary placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent", + className, + )} + /> + {suggestions.length > 0 ? ( +
    + {suggestions.map((node, index) => ( +
  • + {node} +
  • + ))} +
+ ) : null} +
+ ); +} diff --git a/src/components/ui/Searchbar.tsx b/src/components/ui/Searchbar.tsx index 4653b88..59f4f9f 100644 --- a/src/components/ui/Searchbar.tsx +++ b/src/components/ui/Searchbar.tsx @@ -1,7 +1,8 @@ -import { ReactElement, useState } from "react"; +import { ReactElement, ReactNode, useState } from "react"; import { Link, NavigateFunction, useNavigate } from "react-router-dom"; import classNames from "classnames"; import { Button } from "../core/Button"; +import { SuggestibleInput } from "../core/SuggestibleInput"; import { formatCoordinateInspectHint, inspectCoordinateQuery, @@ -60,7 +61,6 @@ export function SearchBar({ const [focused, setFocused] = useState(false); const navigate = useNavigate(); const onSearchHandler = onSearch ?? searchHandler(navigate); - const suggestion = focused ? searchSuggestion(searchQuery) : null; function handleSubmit() { if (searchQuery.trim()) { @@ -68,6 +68,35 @@ export function SearchBar({ } } + function getSuggestions(value: string): ReactNode[] { + if (!focused) { + return []; + } + const suggestion = searchSuggestion(value); + if (!suggestion) { + return []; + } + const nodes: ReactNode[] = [ +
+ {suggestion.primary} +
, + ]; + if (suggestion.secondary) { + nodes.push( +
+ {suggestion.secondary} +
, + ); + } + return nodes; + } + return (
-
- + setSearchQuery(e.target.value)} + onChange={setSearchQuery} + getSuggestions={getSuggestions} + placeholder="Search for an object..." + className="h-10 px-2 py-1" onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} onKeyDown={(e) => { @@ -108,14 +137,6 @@ export function SearchBar({ } }} /> - {suggestion ? ( -
-
{suggestion.primary}
- {suggestion.secondary ? ( -
{suggestion.secondary}
- ) : null} -
- ) : null}
+ )); + } + return (

{label}

-
- handleQueryChange(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.preventDefault(); - if (debounceRef.current) { - clearTimeout(debounceRef.current); - debounceRef.current = null; - } - if (suggestions.length > 0) { - selectResult(suggestions[0]); - return; - } - void runSearch(query); + { + if (event.key === "Enter") { + event.preventDefault(); + if (debounceRef.current) { + clearTimeout(debounceRef.current); + debounceRef.current = null; } - if (event.key === "Escape") { - setSuggestions([]); + if (nameResults.length > 0) { + selectResult(nameResults[0]); + return; } - }} - className="bg-surface-2 border border-border rounded px-3 py-2 text-sm text-primary placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent w-full" - /> - {loading || suggestions.length > 0 ? ( -
    - {loading ? ( -
  • -
    -
  • - ) : ( - suggestions.map((suggestion) => ( -
  • - -
  • - )) - )} -
- ) : null} -
+ void runSearch(query); + } + if (event.key === "Escape") { + setNameResults([]); + } + }} + /> {error ? (

{error} From d2782caf8fc33fbb5504f244bc12d469c589ee51 Mon Sep 17 00:00:00 2001 From: kraysent Date: Mon, 3 Aug 2026 14:44:56 +0100 Subject: [PATCH 3/4] better text --- src/pages/AdminMergePgc.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/AdminMergePgc.tsx b/src/pages/AdminMergePgc.tsx index 5ddac75..a5efc99 100644 --- a/src/pages/AdminMergePgc.tsx +++ b/src/pages/AdminMergePgc.tsx @@ -495,7 +495,7 @@ export function AdminMergePgcPage(): ReactElement {

Merge PGC objects

Select a target PGC (survives) and a source PGC (records are - reassigned, then the source disappears). + reassigned, then the source disappears). You probably will want to rerun import to layer 2 after this operation to update references.

From 8457abc9e2e8c2df91fc2204066d4ce2387fb6d3 Mon Sep 17 00:00:00 2001 From: kraysent Date: Mon, 3 Aug 2026 14:52:18 +0100 Subject: [PATCH 4/4] code review --- configs/config.js | 4 +- src/components/catalogs/ObjectSummary.tsx | 100 ++++++++++++++++++++++ src/pages/AdminMergePgc.tsx | 74 ++-------------- src/pages/RecordCrossmatchDetails.tsx | 90 +------------------ vite.config.ts | 4 +- 5 files changed, 115 insertions(+), 157 deletions(-) create mode 100644 src/components/catalogs/ObjectSummary.tsx diff --git a/configs/config.js b/configs/config.js index 074add3..c0ebecf 100644 --- a/configs/config.js +++ b/configs/config.js @@ -1,4 +1,4 @@ window.__APP_CONFIG__ = { - backendBaseUrl: "https://leda.sao.ru", - adminBaseUrl: "https://leda.sao.ru" + backendBaseUrl: "https://leda.kraysent.dev", + adminBaseUrl: "https://leda.kraysent.dev" }; diff --git a/src/components/catalogs/ObjectSummary.tsx b/src/components/catalogs/ObjectSummary.tsx new file mode 100644 index 0000000..a5206ee --- /dev/null +++ b/src/components/catalogs/ObjectSummary.tsx @@ -0,0 +1,100 @@ +import { ReactElement, ReactNode } from "react"; +import { Schema } from "../../clients/backend/types.gen"; +import { + Declination, + QuantityWithError, + RightAscension, +} from "../core/Astronomy"; + +type ObjectSummaryCatalogs = { + coordinates?: { + equatorial: { + ra: number; + dec: number; + e_ra: number; + e_dec: number; + }; + } | null; + redshift?: { + z: number; + e_z: number; + } | null; +}; + +export function ObjectSummary({ + catalogs, + schema, + name, + layout = "rows", +}: { + catalogs: ObjectSummaryCatalogs; + schema: Schema; + name: ReactNode; + layout?: "rows" | "columnar"; +}): ReactElement { + const equatorial = catalogs?.coordinates?.equatorial; + const redshift = catalogs?.redshift; + const raUnit = schema.units.coordinates?.equatorial?.ra || "deg"; + const eRaUnit = schema.units.coordinates?.equatorial?.e_ra || raUnit; + const eDecUnit = schema.units.coordinates?.equatorial?.e_dec || raUnit; + + const nameField = ( + <> +
Name
+
{name}
+ + ); + + const raField = equatorial ? ( + <> +
RA
+
+ + + +
+ + ) : null; + + const decField = equatorial ? ( + <> +
Dec
+
+ + + +
+ + ) : null; + + const redshiftField = redshift ? ( + <> +
Redshift
+
+ + {redshift.z.toFixed(5)} + +
+ + ) : null; + + if (layout === "columnar") { + return ( +
+
{nameField}
+ {raField &&
{raField}
} + {decField &&
{decField}
} + {redshiftField &&
{redshiftField}
} +
+ ); + } + + return ( +
+ {nameField} + {raField} + {decField} + {redshiftField} +
+ ); +} diff --git a/src/pages/AdminMergePgc.tsx b/src/pages/AdminMergePgc.tsx index a5efc99..61d3e47 100644 --- a/src/pages/AdminMergePgc.tsx +++ b/src/pages/AdminMergePgc.tsx @@ -3,14 +3,10 @@ import { Navigate } from "react-router-dom"; import { isLoggedIn } from "../auth/token"; import { mergePgcs } from "../clients/admin/sdk.gen"; import { querySimple } from "../clients/backend/sdk.gen"; -import { Catalogs, PgcObject, Schema } from "../clients/backend/types.gen"; +import { PgcObject, Schema } from "../clients/backend/types.gen"; import { adminClient, backendClient } from "../clients/config"; +import { ObjectSummary } from "../components/catalogs/ObjectSummary"; import { AladinViewer } from "../components/core/Aladin"; -import { - Declination, - QuantityWithError, - RightAscension, -} from "../components/core/Astronomy"; import { Button } from "../components/core/Button"; import { Link } from "../components/core/Link"; import { SuggestibleInput } from "../components/core/SuggestibleInput"; @@ -57,17 +53,20 @@ function skyViewForSources(sources: SkySource[]): { return { ra, dec, fov }; } +function objectLabel(object: PgcObject): string { + return object.catalogs.designation?.name || `PGC ${object.pgc}`; +} + function objectToSkySource(object: PgcObject, role: string): SkySource | null { const equatorial = object.catalogs.coordinates?.equatorial; if (equatorial?.ra === undefined || equatorial?.dec === undefined) { return null; } - const name = object.catalogs.designation?.name || `PGC ${object.pgc}`; return { ra: equatorial.ra, dec: equatorial.dec, - label: `${name} (${role})`, + label: `${objectLabel(object)} (${role})`, id: object.pgc, }; } @@ -76,10 +75,6 @@ function isPgcNumberInput(value: string): boolean { return /^\d+$/.test(value.trim()); } -function objectLabel(object: PgcObject): string { - return object.catalogs.designation?.name || `PGC ${object.pgc}`; -} - async function fetchPgc( pgc: number, ): Promise<{ object: PgcObject; schema: Schema }> { @@ -134,58 +129,6 @@ async function fetchByName( }; } -function ObjectSummary({ - catalogs, - schema, - name, -}: { - catalogs: Catalogs; - schema: Schema; - name: ReactNode; -}): ReactElement { - const equatorial = catalogs.coordinates?.equatorial; - const redshift = catalogs.redshift; - - return ( -
-
Name
-
{name}
- {equatorial ? ( - <> -
RA
-
- - - -
-
Dec
-
- - - -
- - ) : null} - {redshift ? ( - <> -
Redshift
-
- - {redshift.z.toFixed(5)} - -
- - ) : null} -
- ); -} - interface PgcSelection { object: PgcObject; schema: Schema; @@ -495,7 +438,8 @@ export function AdminMergePgcPage(): ReactElement {

Merge PGC objects

Select a target PGC (survives) and a source PGC (records are - reassigned, then the source disappears). You probably will want to rerun import to layer 2 after this operation to update references. + reassigned, then the source disappears). You probably will want to + rerun import to layer 2 after this operation to update references.

diff --git a/src/pages/RecordCrossmatchDetails.tsx b/src/pages/RecordCrossmatchDetails.tsx index d9482e3..66e7cd9 100644 --- a/src/pages/RecordCrossmatchDetails.tsx +++ b/src/pages/RecordCrossmatchDetails.tsx @@ -1,4 +1,4 @@ -import { ReactElement, ReactNode, useEffect, useState } from "react"; +import { ReactElement, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import { AladinViewer } from "../components/core/Aladin"; import { Loading } from "../components/core/Loading"; @@ -18,7 +18,6 @@ import { PgcCandidate, Schema as AdminSchema, StatusesPayload, - Catalogs, } from "../clients/admin/types.gen"; import { Schema as BackendSchema } from "../clients/backend/types.gen"; import { getResource } from "../resources/resources"; @@ -30,11 +29,7 @@ import { useDataFetching } from "../hooks/useDataFetching"; import { adminClient } from "../clients/config"; import { Button } from "../components/core/Button"; import { isLoggedIn } from "../auth/token"; -import { - Declination, - QuantityWithError, - RightAscension, -} from "../components/core/Astronomy"; +import { ObjectSummary } from "../components/catalogs/ObjectSummary"; import classNames from "classnames"; import { MdAdd, MdClose } from "react-icons/md"; @@ -145,87 +140,6 @@ function convertCandidatesToAdditionalSources( : candidateSources; } -function ObjectSummary({ - catalogs, - schema, - name, - layout = "rows", -}: { - catalogs: Catalogs; - schema: BackendSchema; - name: ReactNode; - layout?: "rows" | "columnar"; -}): ReactElement { - const equatorial = catalogs?.coordinates?.equatorial; - const redshift = catalogs?.redshift; - - const nameField = ( - <> -
Name
-
{name}
- - ); - - const raField = equatorial ? ( - <> -
RA
-
- - - -
- - ) : null; - - const decField = equatorial ? ( - <> -
Dec
-
- - - -
- - ) : null; - - const redshiftField = redshift ? ( - <> -
Redshift
-
- - {redshift.z.toFixed(5)} - -
- - ) : null; - - if (layout === "columnar") { - return ( -
-
{nameField}
- {raField &&
{raField}
} - {decField &&
{decField}
} - {redshiftField &&
{redshiftField}
} -
- ); - } - - return ( -
- {nameField} - {raField} - {decField} - {redshiftField} -
- ); -} - type ResolutionChoice = "new" | number; interface ResolutionSelectorProps { diff --git a/vite.config.ts b/vite.config.ts index 6afa6f7..1a1b73c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,11 +8,11 @@ export default defineConfig({ server: { proxy: { "/api": { - target: "https://leda.sao.ru", + target: "https://leda.kraysent.dev", changeOrigin: true, }, "/admin": { - target: "https://leda.sao.ru", + target: "https://leda.kraysent.dev", changeOrigin: true, }, },