Skip to content

Commit 05c7298

Browse files
authored
feat(explorer): query execution time (#3444)
1 parent 54e5c06 commit 05c7298

File tree

9 files changed

+372
-18
lines changed

9 files changed

+372
-18
lines changed

.changeset/swift-poets-tan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/explorer": patch
3+
---
4+
5+
SQL query execution time in Explore table is now measured and displayed.

packages/explorer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@radix-ui/react-select": "^2.1.1",
5555
"@radix-ui/react-separator": "^1.1.0",
5656
"@radix-ui/react-slot": "^1.1.0",
57+
"@radix-ui/react-tooltip": "^1.1.6",
5758
"@radix-ui/themes": "^3.0.5",
5859
"@rainbow-me/rainbowkit": "^2.1.5",
5960
"@tanstack/react-query": "^5.51.3",

packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/Explorer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function Explorer() {
1717
const { worldAddress } = useParams();
1818
const { id: chainId } = useChain();
1919
const indexer = indexerForChainId(chainId);
20-
const [isLiveQuery, setIsLiveQuery] = useState(true);
20+
const [isLiveQuery, setIsLiveQuery] = useState(false);
2121
const [query, setQuery] = useQueryState("query", parseAsString.withDefault(""));
2222
const [selectedTableId] = useQueryState("tableId");
2323
const prevSelectedTableId = usePrevious(selectedTableId);

packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/SQLEditor.tsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import { useEffect, useRef, useState } from "react";
77
import { useForm } from "react-hook-form";
88
import { Table } from "@latticexyz/config";
99
import Editor from "@monaco-editor/react";
10+
import { Tooltip } from "../../../../../../components/Tooltip";
1011
import { Button } from "../../../../../../components/ui/Button";
1112
import { Form, FormField } from "../../../../../../components/ui/Form";
1213
import { cn } from "../../../../../../utils";
14+
import { useTableDataQuery } from "../../../../queries/useTableDataQuery";
1315
import { monacoOptions } from "./consts";
1416
import { useMonacoSuggestions } from "./useMonacoSuggestions";
1517
import { useQueryValidator } from "./useQueryValidator";
@@ -26,6 +28,11 @@ export function SQLEditor({ table, isLiveQuery, setIsLiveQuery }: Props) {
2628
const [isFocused, setIsFocused] = useState(false);
2729
const [query, setQuery] = useQueryState("query", { defaultValue: "" });
2830
const validateQuery = useQueryValidator(table);
31+
const { data: tableData } = useTableDataQuery({
32+
table,
33+
query,
34+
isLiveQuery,
35+
});
2936
useMonacoSuggestions(table);
3037

3138
const form = useForm({
@@ -107,15 +114,33 @@ export function SQLEditor({ table, isLiveQuery, setIsLiveQuery }: Props) {
107114
) : null}
108115
</div>
109116

110-
<div className="flex justify-end gap-2">
111-
<Button
112-
variant="ghost"
113-
size="icon"
114-
onClick={() => setIsLiveQuery(!isLiveQuery)}
115-
title={isLiveQuery ? "Pause live query" : "Start live query"}
116-
>
117-
{isLiveQuery ? <PlayIcon className="h-4 w-4" /> : <PauseIcon className="h-4 w-4" />}
118-
</Button>
117+
<div className="flex justify-end gap-4">
118+
{tableData ? (
119+
<>
120+
<span className="flex items-center gap-1.5 text-xs text-white/60">
121+
<Tooltip text="Execution time for the SQL query">
122+
<span className="flex items-center gap-1.5">
123+
<span
124+
className={cn("inline-block h-[6px] w-[6px] rounded-full bg-success", {
125+
"animate-pulse": isLiveQuery,
126+
})}
127+
/>
128+
<span>{tableData ? Math.round(tableData.queryDuration) : 0}ms</span>
129+
</span>
130+
</Tooltip>
131+
·
132+
<span>
133+
{tableData?.rows.length ?? 0} row{tableData?.rows.length !== 1 ? "s" : ""}
134+
</span>
135+
</span>
136+
137+
<Tooltip text={isLiveQuery ? "Pause live query" : "Start live query"}>
138+
<Button variant="outline" size="icon" onClick={() => setIsLiveQuery(!isLiveQuery)}>
139+
{isLiveQuery ? <PauseIcon className="h-4 w-4" /> : <PlayIcon className="h-4 w-4" />}
140+
</Button>
141+
</Tooltip>
142+
</>
143+
) : null}
119144

120145
<Button className="flex gap-2 pl-4 pr-3" type="submit">
121146
Run

packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import { Button } from "../../../../../../components/ui/Button";
1818
import { Input } from "../../../../../../components/ui/Input";
1919
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../../components/ui/Table";
2020
import { cn } from "../../../../../../utils";
21+
import { useChain } from "../../../../hooks/useChain";
2122
import { TData, TDataRow, useTableDataQuery } from "../../../../queries/useTableDataQuery";
23+
import { indexerForChainId } from "../../../../utils/indexerForChainId";
2224
import { EditableTableCell } from "./EditableTableCell";
2325
import { ExportButton } from "./ExportButton";
2426
import { typeSortingFn } from "./utils/typeSortingFn";
@@ -33,6 +35,8 @@ type Props = {
3335
};
3436

3537
export function TablesViewer({ table, query, isLiveQuery }: Props) {
38+
const { id: chainId } = useChain();
39+
const indexer = indexerForChainId(chainId);
3640
const {
3741
data: tableData,
3842
isLoading: isTDataLoading,
@@ -110,7 +114,11 @@ export function TablesViewer({ table, query, isLiveQuery }: Props) {
110114
});
111115

112116
return (
113-
<div className="!-mt-10 space-y-4">
117+
<div
118+
className={cn("space-y-4", {
119+
"!-mt-10": indexer.type === "hosted",
120+
})}
121+
>
114122
<div className="flex w-1/2 items-center gap-4">
115123
<Input
116124
placeholder="Filter..."

packages/explorer/src/app/(explorer)/queries/useTableDataQuery.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ export type TDataRow = Record<string, unknown>;
1616
export type TData = {
1717
columns: string[];
1818
rows: TDataRow[];
19+
queryDuration: number;
1920
};
2021

2122
export function useTableDataQuery({ table, query, isLiveQuery }: Props) {
2223
const { chainName, worldAddress } = useParams();
2324
const { id: chainId } = useChain();
2425
const decodedQuery = decodeURIComponent(query ?? "");
2526

26-
return useQuery<DozerResponse, Error, TData | undefined>({
27+
return useQuery<DozerResponse & { queryDuration: number }, Error, TData | undefined>({
2728
queryKey: ["tableData", chainName, worldAddress, decodedQuery],
2829
queryFn: async () => {
30+
const startTime = performance.now();
2931
const indexer = indexerForChainId(chainId);
3032
const response = await fetch(indexer.url, {
3133
method: "POST",
@@ -41,13 +43,15 @@ export function useTableDataQuery({ table, query, isLiveQuery }: Props) {
4143
});
4244

4345
const data = await response.json();
46+
const queryDuration = performance.now() - startTime;
47+
4448
if (!response.ok) {
4549
throw new Error(data.msg || "Network response was not ok");
4650
}
4751

48-
return data;
52+
return { ...data, queryDuration };
4953
},
50-
select: (data: DozerResponse): TData | undefined => {
54+
select: (data: DozerResponse & { queryDuration: number }): TData | undefined => {
5155
if (!table || !data?.result?.[0]) return undefined;
5256

5357
const indexer = indexerForChainId(chainId);
@@ -76,13 +80,14 @@ export function useTableDataQuery({ table, query, isLiveQuery }: Props) {
7680
return {
7781
columns,
7882
rows,
83+
queryDuration: data.queryDuration,
7984
};
8085
},
8186
retry: false,
8287
enabled: !!table && !!query,
8388
refetchInterval: (query) => {
8489
if (query.state.error) return false;
85-
else if (isLiveQuery) return false;
90+
else if (!isLiveQuery) return false;
8691
return 1000;
8792
},
8893
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Tooltip as RadixTooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/Tooltip";
2+
3+
type Props = {
4+
text: string;
5+
children: React.ReactNode;
6+
};
7+
8+
export function Tooltip({ text, children }: Props) {
9+
return (
10+
<TooltipProvider>
11+
<RadixTooltip>
12+
<TooltipTrigger>{children}</TooltipTrigger>
13+
<TooltipContent>
14+
<p className="text-xs">{text}</p>
15+
</TooltipContent>
16+
</RadixTooltip>
17+
</TooltipProvider>
18+
);
19+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5+
import { cn } from "../../utils";
6+
7+
const TooltipProvider = TooltipPrimitive.Provider;
8+
9+
const Tooltip = TooltipPrimitive.Root;
10+
11+
const TooltipTrigger = TooltipPrimitive.Trigger;
12+
13+
const TooltipContent = React.forwardRef<
14+
React.ElementRef<typeof TooltipPrimitive.Content>,
15+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
16+
>(({ className, sideOffset = 4, ...props }, ref) => (
17+
<TooltipPrimitive.Content
18+
ref={ref}
19+
sideOffset={sideOffset}
20+
className={cn(
21+
[
22+
"z-50",
23+
"overflow-hidden",
24+
"rounded-md",
25+
"border",
26+
"bg-popover",
27+
"text-popover-foreground",
28+
"px-3",
29+
"py-1.5",
30+
"text-sm",
31+
"shadow-md",
32+
"animate-in",
33+
"fade-in-0",
34+
"zoom-in-95",
35+
"data-[state=closed]:animate-out",
36+
"data-[state=closed]:fade-out-0",
37+
"data-[state=closed]:zoom-out-95",
38+
"data-[side=bottom]:slide-in-from-top-2",
39+
"data-[side=left]:slide-in-from-right-2",
40+
"data-[side=right]:slide-in-from-left-2",
41+
"data-[side=top]:slide-in-from-bottom-2",
42+
].join(" "),
43+
className,
44+
)}
45+
{...props}
46+
/>
47+
));
48+
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
49+
50+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

0 commit comments

Comments
 (0)