Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explore Endpoints: pages part 2 #807

Merged
merged 20 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from 17 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,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
186 changes: 147 additions & 39 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 @@ -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
40 changes: 40 additions & 0 deletions src/components/FormElements/XdrPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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;
// eslint-disable-next-line no-unused-vars
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can remove this! 🎉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!

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
Loading