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
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"use client";

import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { CodeClient, CodeLoading } from "@/components/code/code.client";
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { Spinner } from "@/components/ui/Spinner/Spinner";
Expand Down Expand Up @@ -35,6 +33,7 @@ import {
useForm,
} from "react-hook-form";
import { z } from "zod";
import { MultiNetworkSelector } from "../../../components/blocks/NetworkSelectors";
import type { BlueprintParameter, BlueprintPathMetadata } from "../utils";

export function BlueprintPlayground(props: {
Expand Down Expand Up @@ -156,14 +155,16 @@ export function BlueprintPlaygroundUI(props: {
}, [parameters]);

const defaultValues = useMemo(() => {
const values: Record<string, string | number> = {};
const values: Record<string, string | number | string[] | number[]> = {};
for (const param of parameters) {
if (param.schema && "type" in param.schema && param.schema.default) {
values[param.name] = param.schema.default;
} else if (param.name === "filter_block_timestamp_gte") {
values[param.name] = Math.floor(
(Date.now() - 3 * 30 * 24 * 60 * 60 * 1000) / 1000,
);
} else if (param.name === "chain") {
values[param.name] = [];
} else {
values[param.name] = "";
}
Expand Down Expand Up @@ -407,7 +408,7 @@ function RequestConfigSection(props: {
}

type ParametersForm = UseFormReturn<{
[x: string]: string | number;
[x: string]: string | number | string[] | number[];
}>;

function ParameterSection(props: {
Expand Down Expand Up @@ -485,13 +486,15 @@ function ParameterSection(props: {
</div>
<div className="relative">
{param.name === "chain" ? (
<SingleNetworkSelector
chainId={
field.value ? Number(field.value) : undefined
<MultiNetworkSelector
selectedBadgeClassName="bg-background"
selectedChainIds={
props.form.watch("chain") as number[]
}
onChange={(chainId) => {
field.onChange({
target: { value: chainId.toString() },
onChange={(chainIds) => {
props.form.setValue("chain", chainIds, {
shouldValidate: true,
shouldDirty: true,
});
}}
className="rounded-none border-0 border-t lg:border-none"
Expand All @@ -502,11 +505,14 @@ function ParameterSection(props: {
: undefined
}
/>
) : (
) : !Array.isArray(field.value) ? (
<>
<ParameterInput
param={param}
field={field}
field={{
...field,
value: field.value,
}}
showTip={showTip}
hasError={hasError}
placeholder={placeholder}
Expand Down Expand Up @@ -549,7 +555,7 @@ function ParameterSection(props: {
</ToolTipLabel>
)}
</>
)}
) : null}
</div>
</div>
<FormMessage className="mt-0 border-destructive-text border-t px-3 py-2" />
Expand Down Expand Up @@ -780,6 +786,26 @@ function openAPIV3ParamToZodFormSchema(
return isRequired ? booleanSchema : booleanSchema.optional();
}

case "array": {
if ("type" in schema.items) {
let itemSchema: z.ZodTypeAny | undefined = undefined;
if (schema.items.type === "number") {
itemSchema = z.number();
} else if (schema.items.type === "integer") {
itemSchema = z.number().int();
} else if (schema.items.type === "string") {
itemSchema = z.string();
}

if (itemSchema) {
return isRequired
? z.array(itemSchema)
: z.array(itemSchema).optional();
}
}
break;
}

// everything else - just accept it as a string;
default: {
const stringSchema = z.string();
Expand Down Expand Up @@ -840,7 +866,13 @@ function createBlueprintUrl(options: {
for (const parameter of queryParams) {
const value = values[parameter.name];
if (value) {
searchParams.append(parameter.name, value);
if (Array.isArray(value)) {
for (const v of value) {
searchParams.append(parameter.name, v);
}
} else {
searchParams.append(parameter.name, value);
}
}
}

Expand Down
117 changes: 117 additions & 0 deletions apps/playground-web/src/components/blocks/NetworkSelectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
import { useCallback, useMemo } from "react";
import { useAllChainsData } from "../../app/hooks/chains";
import { ChainIcon } from "./ChainIcon";
import { MultiSelect } from "./multi-select";
import { SelectWithSearch } from "./select-with-search";

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

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

export function MultiNetworkSelector(props: {
selectedChainIds: number[];
onChange: (chainIds: number[]) => void;
disableChainId?: boolean;
className?: string;
priorityChains?: number[];
popoverContentClassName?: string;
chainIds?: number[];
selectedBadgeClassName?: string;
}) {
const { allChains, idToChain } = useAllChainsData().data;

const chainsToShow = useMemo(() => {
if (!props.chainIds) {
return allChains;
}
const chainIdSet = new Set(props.chainIds);
return allChains.filter((chain) => chainIdSet.has(chain.chainId));
}, [allChains, props.chainIds]);

const options = useMemo(() => {
let sortedChains = chainsToShow;

if (props.priorityChains) {
const priorityChainsSet = new Set();
for (const chainId of props.priorityChains || []) {
priorityChainsSet.add(chainId);
}

const priorityChains = (props.priorityChains || [])
.map((chainId) => {
return idToChain.get(chainId);
})
.filter((v) => !!v);

const otherChains = chainsToShow.filter(
(chain) => !priorityChainsSet.has(chain.chainId),
);

sortedChains = [...priorityChains, ...otherChains];
}

return sortedChains.map((chain) => {
return {
label: cleanChainName(chain.name),
value: String(chain.chainId),
};
});
}, [chainsToShow, props.priorityChains, idToChain]);

const searchFn = useCallback(
(option: Option, searchValue: string) => {
const chain = idToChain.get(Number(option.value));
if (!chain) {
return false;
}

if (Number.isInteger(Number.parseInt(searchValue))) {
return String(chain.chainId).startsWith(searchValue);
}
return chain.name.toLowerCase().includes(searchValue.toLowerCase());
},
[idToChain],
);

const renderOption = useCallback(
(option: Option) => {
const chain = idToChain.get(Number(option.value));
if (!chain) {
return option.label;
}

return (
<div className="flex justify-between gap-4">
<span className="flex grow gap-2 truncate text-left">
<ChainIcon
className="size-5"
ipfsSrc={chain.icon?.url}
loading="lazy"
/>
{cleanChainName(chain.name)}
</span>

{!props.disableChainId && (
<Badge variant="outline" className="gap-2">
<span className="text-muted-foreground">Chain ID</span>
{chain.chainId}
</Badge>
)}
</div>
);
},
[idToChain, props.disableChainId],
);

return (
<MultiSelect
searchPlaceholder="Search by Name or Chain Id"
selectedValues={props.selectedChainIds.map(String)}
options={options}
onSelectedValuesChange={(chainIds) => {
props.onChange(chainIds.map(Number));
}}
placeholder={
allChains.length === 0 ? "Loading Chains..." : "Select Chains"
}
disabled={allChains.length === 0}
overrideSearchFn={searchFn}
renderOption={renderOption}
className={props.className}
popoverContentClassName={props.popoverContentClassName}
selectedBadgeClassName={props.selectedBadgeClassName}
/>
);
}

export function SingleNetworkSelector(props: {
chainId: number | undefined;
onChange: (chainId: number) => void;
Expand Down
Loading
Loading