Skip to content

Commit 4bc7677

Browse files
committed
[TOOL-3564] Playground: Select multiple chains in insight playground
1 parent 6636277 commit 4bc7677

File tree

4 files changed

+531
-14
lines changed

4 files changed

+531
-14
lines changed

apps/playground-web/src/app/insight/[blueprint_slug]/blueprint-playground.client.tsx

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
"use client";
2-
3-
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
42
import { CodeClient, CodeLoading } from "@/components/code/code.client";
53
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
64
import { Spinner } from "@/components/ui/Spinner/Spinner";
@@ -35,6 +33,7 @@ import {
3533
useForm,
3634
} from "react-hook-form";
3735
import { z } from "zod";
36+
import { MultiNetworkSelector } from "../../../components/blocks/NetworkSelectors";
3837
import type { BlueprintParameter, BlueprintPathMetadata } from "../utils";
3938

4039
export function BlueprintPlayground(props: {
@@ -156,14 +155,16 @@ export function BlueprintPlaygroundUI(props: {
156155
}, [parameters]);
157156

158157
const defaultValues = useMemo(() => {
159-
const values: Record<string, string | number> = {};
158+
const values: Record<string, string | number | string[] | number[]> = {};
160159
for (const param of parameters) {
161160
if (param.schema && "type" in param.schema && param.schema.default) {
162161
values[param.name] = param.schema.default;
163162
} else if (param.name === "filter_block_timestamp_gte") {
164163
values[param.name] = Math.floor(
165164
(Date.now() - 3 * 30 * 24 * 60 * 60 * 1000) / 1000,
166165
);
166+
} else if (param.name === "chain") {
167+
values[param.name] = [];
167168
} else {
168169
values[param.name] = "";
169170
}
@@ -407,7 +408,7 @@ function RequestConfigSection(props: {
407408
}
408409

409410
type ParametersForm = UseFormReturn<{
410-
[x: string]: string | number;
411+
[x: string]: string | number | string[] | number[];
411412
}>;
412413

413414
function ParameterSection(props: {
@@ -485,13 +486,15 @@ function ParameterSection(props: {
485486
</div>
486487
<div className="relative">
487488
{param.name === "chain" ? (
488-
<SingleNetworkSelector
489-
chainId={
490-
field.value ? Number(field.value) : undefined
489+
<MultiNetworkSelector
490+
selectedBadgeClassName="bg-background"
491+
selectedChainIds={
492+
props.form.watch("chain") as number[]
491493
}
492-
onChange={(chainId) => {
493-
field.onChange({
494-
target: { value: chainId.toString() },
494+
onChange={(chainIds) => {
495+
props.form.setValue("chain", chainIds, {
496+
shouldValidate: true,
497+
shouldDirty: true,
495498
});
496499
}}
497500
className="rounded-none border-0 border-t lg:border-none"
@@ -502,11 +505,14 @@ function ParameterSection(props: {
502505
: undefined
503506
}
504507
/>
505-
) : (
508+
) : field.value && !Array.isArray(field.value) ? (
506509
<>
507510
<ParameterInput
508511
param={param}
509-
field={field}
512+
field={{
513+
...field,
514+
value: field.value,
515+
}}
510516
showTip={showTip}
511517
hasError={hasError}
512518
placeholder={placeholder}
@@ -549,7 +555,7 @@ function ParameterSection(props: {
549555
</ToolTipLabel>
550556
)}
551557
</>
552-
)}
558+
) : null}
553559
</div>
554560
</div>
555561
<FormMessage className="mt-0 border-destructive-text border-t px-3 py-2" />
@@ -780,6 +786,26 @@ function openAPIV3ParamToZodFormSchema(
780786
return isRequired ? booleanSchema : booleanSchema.optional();
781787
}
782788

789+
case "array": {
790+
if ("type" in schema.items) {
791+
let itemSchema: z.ZodTypeAny | undefined = undefined;
792+
if (schema.items.type === "number") {
793+
itemSchema = z.number();
794+
} else if (schema.items.type === "integer") {
795+
itemSchema = z.number().int();
796+
} else if (schema.items.type === "string") {
797+
itemSchema = z.string();
798+
}
799+
800+
if (itemSchema) {
801+
return isRequired
802+
? z.array(itemSchema)
803+
: z.array(itemSchema).optional();
804+
}
805+
}
806+
break;
807+
}
808+
783809
// everything else - just accept it as a string;
784810
default: {
785811
const stringSchema = z.string();
@@ -840,7 +866,13 @@ function createBlueprintUrl(options: {
840866
for (const parameter of queryParams) {
841867
const value = values[parameter.name];
842868
if (value) {
843-
searchParams.append(parameter.name, value);
869+
if (Array.isArray(value)) {
870+
for (const v of value) {
871+
searchParams.append(parameter.name, v);
872+
}
873+
} else {
874+
searchParams.append(parameter.name, value);
875+
}
844876
}
845877
}
846878

apps/playground-web/src/components/blocks/NetworkSelectors.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
44
import { useCallback, useMemo } from "react";
55
import { useAllChainsData } from "../../app/hooks/chains";
66
import { ChainIcon } from "./ChainIcon";
7+
import { MultiSelect } from "./multi-select";
78
import { SelectWithSearch } from "./select-with-search";
89

910
function cleanChainName(chainName: string) {
@@ -12,6 +13,122 @@ function cleanChainName(chainName: string) {
1213

1314
type Option = { label: string; value: string };
1415

16+
export function MultiNetworkSelector(props: {
17+
selectedChainIds: number[];
18+
onChange: (chainIds: number[]) => void;
19+
disableChainId?: boolean;
20+
className?: string;
21+
priorityChains?: number[];
22+
popoverContentClassName?: string;
23+
chainIds?: number[];
24+
selectedBadgeClassName?: string;
25+
}) {
26+
const { allChains, idToChain } = useAllChainsData().data;
27+
28+
const chainsToShow = useMemo(() => {
29+
if (!props.chainIds) {
30+
return allChains;
31+
}
32+
const chainIdSet = new Set(props.chainIds);
33+
return allChains.filter((chain) => chainIdSet.has(chain.chainId));
34+
}, [allChains, props.chainIds]);
35+
36+
const options = useMemo(() => {
37+
let sortedChains = chainsToShow;
38+
39+
if (props.priorityChains) {
40+
const priorityChainsSet = new Set();
41+
for (const chainId of props.priorityChains || []) {
42+
priorityChainsSet.add(chainId);
43+
}
44+
45+
const priorityChains = (props.priorityChains || [])
46+
.map((chainId) => {
47+
return idToChain.get(chainId);
48+
})
49+
.filter((v) => !!v);
50+
51+
const otherChains = chainsToShow.filter(
52+
(chain) => !priorityChainsSet.has(chain.chainId),
53+
);
54+
55+
sortedChains = [...priorityChains, ...otherChains];
56+
}
57+
58+
return sortedChains.map((chain) => {
59+
return {
60+
label: cleanChainName(chain.name),
61+
value: String(chain.chainId),
62+
};
63+
});
64+
}, [chainsToShow, props.priorityChains, idToChain]);
65+
66+
const searchFn = useCallback(
67+
(option: Option, searchValue: string) => {
68+
const chain = idToChain.get(Number(option.value));
69+
if (!chain) {
70+
return false;
71+
}
72+
73+
if (Number.isInteger(Number.parseInt(searchValue))) {
74+
return String(chain.chainId).startsWith(searchValue);
75+
}
76+
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
77+
},
78+
[idToChain],
79+
);
80+
81+
const renderOption = useCallback(
82+
(option: Option) => {
83+
const chain = idToChain.get(Number(option.value));
84+
if (!chain) {
85+
return option.label;
86+
}
87+
88+
return (
89+
<div className="flex justify-between gap-4">
90+
<span className="flex grow gap-2 truncate text-left">
91+
<ChainIcon
92+
className="size-5"
93+
ipfsSrc={chain.icon?.url}
94+
loading="lazy"
95+
/>
96+
{cleanChainName(chain.name)}
97+
</span>
98+
99+
{!props.disableChainId && (
100+
<Badge variant="outline" className="gap-2">
101+
<span className="text-muted-foreground">Chain ID</span>
102+
{chain.chainId}
103+
</Badge>
104+
)}
105+
</div>
106+
);
107+
},
108+
[idToChain, props.disableChainId],
109+
);
110+
111+
return (
112+
<MultiSelect
113+
searchPlaceholder="Search by Name or Chain Id"
114+
selectedValues={props.selectedChainIds.map(String)}
115+
options={options}
116+
onSelectedValuesChange={(chainIds) => {
117+
props.onChange(chainIds.map(Number));
118+
}}
119+
placeholder={
120+
allChains.length === 0 ? "Loading Chains..." : "Select Chains"
121+
}
122+
disabled={allChains.length === 0}
123+
overrideSearchFn={searchFn}
124+
renderOption={renderOption}
125+
className={props.className}
126+
popoverContentClassName={props.popoverContentClassName}
127+
selectedBadgeClassName={props.selectedBadgeClassName}
128+
/>
129+
);
130+
}
131+
15132
export function SingleNetworkSelector(props: {
16133
chainId: number | undefined;
17134
onChange: (chainId: number) => void;

0 commit comments

Comments
 (0)