Skip to content

Commit

Permalink
Explore Endpoints: pages part 2 (#807)
Browse files Browse the repository at this point in the history
* All Assets

* Claimable Balances

* Effects + refactor endpoint URL template

* Fee Stats

* Ledgers

* Offers

* Operations

* Payments

* Transactions

* Update SDS

* Order book + asset object params

* Trade aggregations

* Fix PrettyJson records rendering

* Trades

* Cleanup

* Cleanup

* Fix linting
  • Loading branch information
quietbits authored Apr 3, 2024
1 parent 87649d2 commit d6f4c04
Show file tree
Hide file tree
Showing 14 changed files with 652 additions and 115 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"git-info": "rm -rf src/generated/ && mkdir src/generated/ && echo export default \"{\\\"commitHash\\\": \\\"$(git rev-parse --short HEAD)\\\", \\\"version\\\": \\\"$(git describe --tags --always)\\\"};\" > src/generated/gitInfo.ts"
},
"dependencies": {
"@stellar/design-system": "^2.0.0-beta.8",
"@stellar/design-system": "^2.0.0-beta.9",
"@stellar/stellar-sdk": "^11.3.0",
"@tanstack/react-query": "^5.28.8",
"@tanstack/react-query-devtools": "^5.28.8",
Expand Down
188 changes: 148 additions & 40 deletions src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Input,
Link,
Text,
Textarea,
} from "@stellar/design-system";
import { useQueryClient } from "@tanstack/react-query";

Expand Down Expand Up @@ -85,7 +86,7 @@ export default function ExploreEndpoints() {
const param = p.match(REGEX_TEMPLATE_PATH_PARAM_VALUE)?.[0];

if (param) {
return urlPathParamArr.push(param);
urlPathParamArr.push(param);
}
});

Expand Down Expand Up @@ -115,7 +116,12 @@ export default function ExploreEndpoints() {
refetch,
isSuccess,
isError,
} = useExploreEndpoint(requestUrl);
} = useExploreEndpoint(
requestUrl,
// There is only one endpoint request for POST, using params directly for
// simplicity.
pageData?.requestMethod === "POST" ? { tx: params.tx ?? "" } : undefined,
);

const responseEl = useRef<HTMLDivElement | null>(null);

Expand All @@ -138,18 +144,34 @@ export default function ExploreEndpoints() {
// Checking if there are any errors
isValid = isEmptyObject(formError);

// When non-native asset is selected, code and issuer fields are required
if (params.asset) {
const assetObj = parseJsonString(params.asset);
// Asset components
const assetParams = [
params.asset,
params.selling_asset,
params.buying_asset,
params.base_asset,
params.counter_asset,
];

assetParams.forEach((aParam) => {
// No need to keep checking if one field is invalid
if (!isValidReqAssetFields) {
return;
}

// When non-native asset is selected, code and issuer fields are required
if (aParam) {
const assetObj = parseJsonString(aParam);

if (
["issued", "credit_alphanum4", "credit_alphanum12"].includes(
assetObj.type,
)
) {
isValidReqAssetFields = Boolean(assetObj.code && assetObj.issuer);
if (
["issued", "credit_alphanum4", "credit_alphanum12"].includes(
assetObj.type,
)
) {
isValidReqAssetFields = Boolean(assetObj.code && assetObj.issuer);
}
}
}
});

return isValidReqAssetFields && isValidReqFields && isValid;
};
Expand All @@ -158,7 +180,6 @@ export default function ExploreEndpoints() {
() =>
queryClient.resetQueries({
queryKey: ["exploreEndpoint", "response"],
exact: true,
}),
[queryClient],
);
Expand Down Expand Up @@ -192,6 +213,35 @@ export default function ExploreEndpoints() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const mapParamToValue = (key: string, value: string) => {
const [param, prop] = (value as string)?.split(".") || [];
const mappedValue = parseJsonString(params?.[param])?.[prop];

return mappedValue ? { [key]: mappedValue } : {};
};

// Persist mapped custom template props to params
useEffect(() => {
const paramMapping = pageData?.custom?.paramMapping;

if (paramMapping) {
const mappedParams = Object.entries(paramMapping).reduce(
(res, [key, value]) => {
const mappedVal = mapParamToValue(key, value as string);

return { ...res, ...mappedVal };
},
{} as AnyObject,
);

if (!isEmptyObject(mappedParams)) {
updateParams(mappedParams);
}
}
// Run this only once when page loads
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
if (currentPage) {
updateCurrentEndpoint(currentPage);
Expand Down Expand Up @@ -248,6 +298,11 @@ export default function ExploreEndpoints() {
};

const baseUrl = `${endpointNetwork.horizonUrl}${parseUrlPath(urlPath)}`;

if (pageData?.requestMethod === "POST") {
return baseUrl;
}

const searchParams = new URLSearchParams();
const templateParams = urlParams?.split(",");

Expand Down Expand Up @@ -305,44 +360,72 @@ export default function ExploreEndpoints() {
}, delay);
};

const renderPostPayload = () => {
if (pageData?.requestMethod === "POST") {
return (
<div className="Endpoints__txTextarea">
<Textarea
id="tx"
fieldSize="md"
label="Payload"
value={JSON.stringify({ tx: params.tx ?? "" }, null, 2)}
rows={5}
disabled
spellCheck={false}
/>
</div>
);
}

return null;
};

const renderEndpointUrl = () => {
if (!pageData) {
return null;
}

return (
<div className="Endpoints__urlBar">
<Input
id="endpoint-url"
fieldSize="md"
value={requestUrl}
readOnly
disabled
leftElement={
<div className="Endpoints__input__requestType">
{pageData.requestMethod}
</div>
}
/>
<Button
size="md"
variant="secondary"
type="submit"
disabled={!isSubmitEnabled()}
isLoading={isLoading || isFetching}
>
Submit
</Button>
<CopyText textToCopy={requestUrl}>
<Button size="md" variant="tertiary" icon={<Icon.Copy01 />}></Button>
</CopyText>
</div>
<>
<div className="Endpoints__urlBar">
<Input
id="endpoint-url"
fieldSize="md"
value={requestUrl}
readOnly
disabled
leftElement={
<div className="Endpoints__input__requestType">
{pageData.requestMethod}
</div>
}
/>
<Button
size="md"
variant="secondary"
type="submit"
disabled={!isSubmitEnabled()}
isLoading={isLoading || isFetching}
>
Submit
</Button>
<CopyText textToCopy={requestUrl}>
<Button
size="md"
variant="tertiary"
icon={<Icon.Copy01 />}
type="button"
></Button>
</CopyText>
</div>
</>
);
};

const renderFields = () => {
const allFields = sanitizeArray([
...urlPathParams.split(","),
...(pageData?.custom?.renderComponents || []),
...urlParams.split(","),
]);

Expand All @@ -353,6 +436,8 @@ export default function ExploreEndpoints() {
return (
<div className="Endpoints__content">
<div className="PageBody__content">
{renderPostPayload()}

{allFields.map((f) => {
const component = formComponentTemplate(f, pageData.custom?.[f]);

Expand All @@ -363,10 +448,29 @@ export default function ExploreEndpoints() {
// formatting (sanitizing object or array, for exmaple).
// Error check needs the original value.
const handleChange = (value: any, storeValue: any) => {
resetQuery();
if (isSuccess || isError) {
resetQuery();
}

// Mapping custom value to template params
const mappedParams = pageData?.custom?.paramMapping
? Object.entries(pageData.custom.paramMapping).reduce(
(res, [key, val]) => {
const [param, prop] = (val as string)?.split(".") || [];

if (param === f) {
return { ...res, [key]: value?.[prop] };
}

return res;
},
{} as AnyObject,
)
: {};

updateParams({
[f]: storeValue,
...mappedParams,
});

const error = component.validate?.(value, isRequired);
Expand All @@ -383,7 +487,11 @@ export default function ExploreEndpoints() {
switch (f) {
case "asset":
case "selling":
case "selling_asset":
case "buying":
case "buying_asset":
case "base_asset":
case "counter_asset":
return component.render({
value: params[f],
error: formError[f],
Expand Down
1 change: 0 additions & 1 deletion src/components/FormElements/PositiveIntPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ interface PositiveIntPickerProps extends Omit<InputProps, "fieldSize"> {
value: string;
placeholder?: string;
error: string | undefined;
// eslint-disable-next-line no-unused-vars
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

Expand Down
1 change: 0 additions & 1 deletion src/components/FormElements/TextPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ interface TextPickerProps extends Omit<InputProps, "fieldSize"> {
value: string;
placeholder?: string;
error: string | undefined;
// eslint-disable-next-line no-unused-vars
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

Expand Down
39 changes: 39 additions & 0 deletions src/components/FormElements/XdrPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from "react";
import { Textarea, TextareaProps } from "@stellar/design-system";

interface XdrPickerProps extends Omit<TextareaProps, "fieldSize"> {
id: string;
fieldSize?: "sm" | "md" | "lg";
labelSuffix?: string | React.ReactNode;
label: string;
value: string;
placeholder?: string;
error: string | undefined;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
}

export const XdrPicker = ({
id,
fieldSize = "md",
labelSuffix,
label,
value,
error,
onChange,
...props
}: XdrPickerProps) => {
return (
<Textarea
id={id}
fieldSize={fieldSize}
label={label}
labelSuffix={labelSuffix}
placeholder="Ex: AAAAABbxCy3mLg3hiTqX4VUEEp60pFOrJNxYM1JtxXTwXhY2AAAAZAAAAAMAAAAGAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAJAAAAAAAAAAHwXhY2AAAAQCPAo8QwsZe9FA0sz/deMdhlu6/zrk7SgkBG22ApvtpETBhnGkX4trSFDz8sVlKqvweqGUVgvjUyM0AcHxyXZQw="
value={value}
error={error}
rows={5}
onChange={onChange}
{...props}
/>
);
};
2 changes: 1 addition & 1 deletion src/components/NetworkIndicator/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
}

&[data-network="mainnet"] {
--NetworkSelector-dot-color: var(--sds-clr-green-09);
--NetworkSelector-dot-color: var(--sds-clr-lime-09);
}
}
}
24 changes: 23 additions & 1 deletion src/components/PrettyJson/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,29 @@ export const PrettyJson = ({ json }: { json: AnyObject }) => {
<Key>{key}</Key>
<Bracket char="[" />
</div>
<div>{value.map((v) => render(v, key))}</div>
<div>
{value.map((v, index) => {
if (typeof v === "object") {
return (
<div
key={`${keyProp}-${index}`}
className="PrettyJson__nested"
>
<div className="PrettyJson__inline">
<Bracket char="{" />
</div>
<div>{render(v)}</div>
<div>
<Bracket char="}" />
<Comma />
</div>
</div>
);
}

return render(v, key);
})}
</div>
<div>
<Bracket char="]" />
<Comma />
Expand Down
Loading

0 comments on commit d6f4c04

Please sign in to comment.