Skip to content
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
4 changes: 2 additions & 2 deletions configs/config.js
Original file line number Diff line number Diff line change
@@ -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"
};
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -45,6 +47,8 @@ function App() {
element={<DataCatalogPage />}
/>
<Route path="/login" element={<LoginPage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/admin/merge-pgc" element={<AdminMergePgcPage />} />
<Route path="/crossmatch" element={<CrossmatchResultsPage />} />
<Route
path="/records/:recordId/crossmatch"
Expand Down
100 changes: 100 additions & 0 deletions src/components/catalogs/ObjectSummary.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<>
<dt className="text-muted">Name</dt>
<dd>{name}</dd>
</>
);

const raField = equatorial ? (
<>
<dt className="text-muted">RA</dt>
<dd>
<QuantityWithError error={equatorial.e_ra} unit={eRaUnit}>
<RightAscension value={equatorial.ra} />
</QuantityWithError>
</dd>
</>
) : null;

const decField = equatorial ? (
<>
<dt className="text-muted">Dec</dt>
<dd>
<QuantityWithError error={equatorial.e_dec} unit={eDecUnit}>
<Declination value={equatorial.dec} />
</QuantityWithError>
</dd>
</>
) : null;

const redshiftField = redshift ? (
<>
<dt className="text-muted">Redshift</dt>
<dd>
<QuantityWithError error={redshift.e_z} decimalPlaces={5}>
{redshift.z.toFixed(5)}
</QuantityWithError>
</dd>
</>
) : null;

if (layout === "columnar") {
return (
<dl className="flex flex-wrap items-start gap-x-6 gap-y-1 text-sm">
<div className="min-w-0">{nameField}</div>
{raField && <div className="min-w-0">{raField}</div>}
{decField && <div className="min-w-0">{decField}</div>}
{redshiftField && <div className="min-w-0">{redshiftField}</div>}
</dl>
);
}

return (
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
{nameField}
{raField}
{decField}
{redshiftField}
</dl>
);
}
64 changes: 64 additions & 0 deletions src/components/core/SuggestibleInput.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>;
onFocus?: FocusEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
}

export function SuggestibleInput({
value,
onChange,
getSuggestions,
placeholder,
disabled,
className,
onKeyDown,
onFocus,
onBlur,
}: SuggestibleInputProps): ReactElement {
const suggestions = getSuggestions(value);

return (
<div className="relative w-full min-w-0">
<input
type="text"
value={value}
disabled={disabled}
placeholder={placeholder}
onChange={(event) => 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 ? (
<ul
className="absolute left-0 right-0 top-full z-10 mt-1 max-h-60 overflow-auto rounded border border-border bg-surface shadow-sm divide-y divide-border"
role="listbox"
>
{suggestions.map((node, index) => (
<li key={index} role="option">
{node}
</li>
))}
</ul>
) : null}
</div>
);
}
21 changes: 21 additions & 0 deletions src/components/ui/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { NavLink, useNavigate } from "react-router-dom";
import {
MdAccountTree,
MdAdminPanelSettings,
MdInfo,
MdLogin,
MdLogout,
Expand Down Expand Up @@ -33,6 +34,13 @@ const navItems = [
},
];

const adminNavItem = {
to: "/admin",
icon: <MdAdminPanelSettings size={20} />,
label: "Admin",
end: false,
};

const configuredProductionWeb = "https://leda.sao.ru";

function openCurrentPathOnOrigin(productionWebInput: string): void {
Expand Down Expand Up @@ -106,6 +114,19 @@ export function Navbar() {
</NavLink>
</AppTooltip>
))}
{isLoggedIn() ? (
<AppTooltip content={adminNavItem.label} placement="right">
<NavLink
to={adminNavItem.to}
end={adminNavItem.end}
className={({ isActive }) =>
sidebarRailControlClassName(isActive)
}
>
{adminNavItem.icon}
</NavLink>
</AppTooltip>
) : null}

<div className="mt-auto flex flex-col gap-2 items-center">
{showOpenProductionButton ? (
Expand Down
53 changes: 37 additions & 16 deletions src/components/ui/Searchbar.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -60,14 +61,42 @@ 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()) {
onSearchHandler(searchQuery);
}
}

function getSuggestions(value: string): ReactNode[] {
if (!focused) {
return [];
}
const suggestion = searchSuggestion(value);
if (!suggestion) {
return [];
}
const nodes: ReactNode[] = [
<div
key="primary"
className="px-2 py-2 text-left text-sm text-muted pointer-events-none"
>
{suggestion.primary}
</div>,
];
if (suggestion.secondary) {
nodes.push(
<div
key="secondary"
className="px-2 py-2 text-left text-sm text-muted pointer-events-none"
>
{suggestion.secondary}
</div>,
);
}
return nodes;
}

return (
<header
className={classNames(
Expand All @@ -93,13 +122,13 @@ export function SearchBar({
})}
>
<div className="flex items-start w-full">
<div className="relative flex-grow min-w-0">
<input
type="text"
placeholder="Search for an object..."
className="border border-border rounded px-2 py-1 w-full h-10 bg-surface-2 text-primary placeholder:text-muted"
<div className="flex-grow min-w-0">
<SuggestibleInput
value={searchQuery}
onChange={(e) => 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) => {
Expand All @@ -108,14 +137,6 @@ export function SearchBar({
}
}}
/>
{suggestion ? (
<div className="absolute left-0 right-0 top-full z-10 mt-0.5 w-full text-left text-sm text-muted bg-surface-2 border border-border rounded px-2 py-2 shadow-sm pointer-events-none">
<div>{suggestion.primary}</div>
{suggestion.secondary ? (
<div className="mt-1">{suggestion.secondary}</div>
) : null}
</div>
) : null}
</div>
<Button onClick={handleSubmit} className="ml-2 h-10 shrink-0">
Search
Expand Down
37 changes: 37 additions & 0 deletions src/pages/Admin.tsx
Original file line number Diff line number Diff line change
@@ -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 <Navigate to="/login" replace />;
}

return (
<div className="max-w-3xl">
<h2 className="text-3xl font-bold mb-4">Admin</h2>
<ul className="flex flex-col gap-4">
{tasks.map((task) => (
<li key={task.to}>
<Link href={task.to}>{task.title}</Link>
<p className="text-sm text-muted mt-1">{task.description}</p>
</li>
))}
</ul>
</div>
);
}
Loading
Loading