| import { useCallback, useEffect, useMemo, useState } from "react"; |
|
|
| import type { QueryOpenDetail } from "../apiContract"; |
| import { QUERY_OPEN_EVENT } from "../apiContract"; |
| import CustomerGrid from "../customer-grid/CustomerGrid"; |
| import { OverlayProvider } from "../customer-grid/OverlaySurface"; |
| import { queryCitationLabel } from "../customer-grid/queryPreview"; |
| import { DbHead, gridScopeFor } from "../shell/dbFrame"; |
| import { databaseEntries } from "../shell/nav"; |
| import type { NavEntry } from "../shell/nav"; |
| import { bindingFor, deleteQuery, fetchQueries, QUERY_BINDING_EVENT } from "./queryApi"; |
| import type { QueryCitation, SavedQuery } from "./queryApi"; |
| import { QueryEmpty, queryGroups, QueryProvenance, QueryRail } from "./queryParts"; |
| import type { QueryGroup } from "./queryParts"; |
| import "./query.css"; |
|
|
| export interface QueryPageProps { |
| granted: NavEntry[]; |
| |
| |
| |
| |
| |
| |
| hostedRail?: boolean; |
| |
| refreshToken?: number; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| selectedId?: string; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export default function QueryPage({ granted, hostedRail, refreshToken, selectedId }: QueryPageProps) { |
| const [views, setViews] = useState<SavedQuery[]>([]); |
| const [citations, setCitations] = useState<QueryCitation[]>([]); |
| const [activeId, setActiveId] = useState(""); |
| const [loaded, setLoaded] = useState(false); |
| const entries = databaseEntries(granted); |
| const entryOf = (key: string) => entries.find((entry) => entry.key === key); |
| const known = views.filter((view) => entryOf(view.source.database)); |
| const active = known.find((view) => view.id === activeId) ?? known[0] ?? null; |
|
|
| const groups = useMemo<QueryGroup[]>(() => queryGroups(views, entries), [views, entries]); |
|
|
| const reload = useCallback(async () => { |
| const result = await fetchQueries(); |
| setLoaded(true); |
| if (result.ok) { |
| setViews(result.value.views); |
| setCitations(result.value.citations); |
| } |
| }, []); |
|
|
| useEffect(() => { void reload(); }, [reload]); |
|
|
| |
| useEffect(() => { if (refreshToken) void reload(); }, [refreshToken, reload]); |
|
|
| |
| |
| |
| |
| useEffect(() => { if (selectedId) setActiveId(selectedId); }, [selectedId]); |
|
|
| useEffect(() => { |
| if (known.length && !known.some((view) => view.id === activeId)) setActiveId(known[0].id); |
| }, [known, activeId]); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const binding = useMemo( |
| () => (active ? bindingFor(active, citations) : undefined), [active, citations]); |
|
|
| useEffect(() => { |
| if (!binding) return; |
| |
| |
| |
| window.dispatchEvent(new CustomEvent(QUERY_BINDING_EVENT, { detail: binding })); |
| }, [binding]); |
|
|
| useEffect(() => { |
| const onOpen = (event: Event) => { |
| const qid = (event as CustomEvent<QueryOpenDetail>).detail?.qid; |
| if (!qid) return; |
| |
| |
| |
| |
| setActiveId(qid); |
| if (!views.some((view) => view.id === qid)) void reload(); |
| }; |
| window.addEventListener(QUERY_OPEN_EVENT, onOpen); |
| return () => window.removeEventListener(QUERY_OPEN_EVENT, onOpen); |
| }, [views, reload]); |
|
|
| |
| const upsert = useCallback((view: SavedQuery) => { |
| setViews((current) => [view, ...current.filter((row) => row.id !== view.id)]); |
| }, []); |
|
|
| const remove = useCallback(async (qid: string) => { |
| const result = await deleteQuery(qid); |
| if (!result.ok) return; |
| setViews((current) => current.filter((view) => view.id !== qid)); |
| setActiveId((current) => (current === qid ? "" : current)); |
| }, []); |
|
|
| if (!active) return <QueryEmpty loaded={loaded} unnamed={views.length - known.length} />; |
| const entry = entryOf(active.source.database); |
| const cited = citations.filter((citation) => active.citationIds.includes(citation.id)); |
| const provenance = cited.map(queryCitationLabel).join(" Β· ") |
| || `${active.source.label} Β· snapshot ${String(active.source.source_version)} Β· retrieved ${active.source.retrieved_at}`; |
| return ( |
| <div className="shell-db-frame"> |
| <DbHead label={active.source.label} {...(entry?.icon ? { icon: entry.icon } : {})} /> |
| <div className="shell-grid-host"> |
| <div className="qy-workspace"> |
| {hostedRail ? null : ( |
| <QueryRail groups={groups} activeId={active.id} onSelect={setActiveId} |
| onDelete={(id) => void remove(id)} onChanged={upsert} /> |
| )} |
| <div className="qy-surface"> |
| <QueryProvenance view={active} detail={provenance} /> |
| <div className="qy-grid-host"> |
| <OverlayProvider> |
| <CustomerGrid key={gridScopeFor(active.source.database)} |
| scope={gridScopeFor(active.source.database)} queryBinding={binding} /> |
| </OverlayProvider> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|