Readonly tx json view

This commit is contained in:
muzam1l
2022-04-22 18:46:35 +05:30
parent 587f09ec00
commit b3f2d0fb6d
5 changed files with 509 additions and 379 deletions

View File

@@ -1,377 +0,0 @@
import { Play } from "phosphor-react";
import { FC, useCallback, useEffect } from "react";
import { useSnapshot } from "valtio";
import transactionsData from "../content/transactions.json";
import state, { modifyTransaction } from "../state";
import { sendTransaction } from "../state/actions";
import Box from "./Box";
import Button from "./Button";
import Container from "./Container";
import { streamState } from "./DebugStream";
import Flex from "./Flex";
import Input from "./Input";
import Text from "./Text";
import Select from "./Select";
type TxFields = Omit<
typeof transactionsData[0],
"Account" | "Sequence" | "TransactionType"
>;
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
type SelectOption = {
value: string;
label: string;
};
export interface TransactionState {
selectedTransaction: SelectOption | null;
selectedAccount: SelectOption | null;
selectedDestAccount: SelectOption | null;
txIsLoading: boolean;
txIsDisabled: boolean;
txFields: TxFields;
}
export interface TransactionProps {
header: string;
state: TransactionState;
}
const Transaction: FC<TransactionProps> = ({
header,
state: {
selectedAccount,
selectedDestAccount,
selectedTransaction,
txFields,
txIsDisabled,
txIsLoading,
},
...props
}) => {
const { accounts } = useSnapshot(state);
const setState = useCallback(
(pTx?: Partial<TransactionState>) => {
modifyTransaction(header, pTx);
},
[header]
);
const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const destAccountOptions: SelectOption[] = accounts
.map(acc => ({
label: acc.name,
value: acc.address,
}))
.filter(acc => acc.value !== selectedAccount?.value);
useEffect(() => {
const transactionType = selectedTransaction?.value;
const account = accounts.find(
acc => acc.address === selectedAccount?.value
);
if (!account || !transactionType || txIsLoading) {
setState({ txIsDisabled: true });
} else {
setState({ txIsDisabled: false });
}
}, [txIsLoading, selectedTransaction, selectedAccount, accounts, setState]);
useEffect(() => {
let _txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === selectedTransaction?.value
);
if (!_txFields) return setState({ txFields: {} });
_txFields = { ..._txFields } as TxFields;
if (!_txFields.Destination) setState({ selectedDestAccount: null });
// @ts-ignore
delete _txFields.TransactionType;
// @ts-ignore
delete _txFields.Account;
// @ts-ignore
delete _txFields.Sequence;
setState({ txFields: _txFields });
}, [setState, selectedTransaction]);
const submitTest = useCallback(async () => {
const account = accounts.find(
acc => acc.address === selectedAccount?.value
);
const TransactionType = selectedTransaction?.value;
if (!account || !TransactionType || txIsDisabled) return;
setState({ txIsLoading: true });
// setTxIsError(null)
try {
let options = { ...txFields };
options.Destination = selectedDestAccount?.value;
(Object.keys(options) as (keyof TxFields)[]).forEach(field => {
let _value = options[field];
// convert currency
if (typeof _value === "object" && _value.type === "currency") {
if (+_value.value) {
options[field] = (+_value.value * 1000000 + "") as any;
} else {
options[field] = undefined; // 👇 💀
}
}
// handle type: `json`
if (typeof _value === "object" && _value.type === "json") {
if (typeof _value.value === "object") {
options[field] = _value.value as any;
} else {
try {
options[field] = JSON.parse(_value.value);
} catch (error) {
const message = `Input error for json field '${field}': ${
error instanceof Error ? error.message : ""
}`;
throw Error(message);
}
}
}
// delete unneccesary fields
if (!options[field]) {
delete options[field];
}
});
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
await sendTransaction(
account,
{
TransactionType,
...options,
},
{ logPrefix }
);
} catch (error) {
console.error(error);
if (error instanceof Error) {
state.transactionLogs.push({ type: "error", message: error.message });
}
}
setState({ txIsLoading: false });
}, [
header,
setState,
selectedAccount?.value,
selectedDestAccount?.value,
selectedTransaction?.value,
accounts,
txFields,
txIsDisabled,
]);
const resetState = useCallback(() => {
setState({});
}, [setState]);
const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc });
streamState.selectedAccount = acc;
};
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter(
k => !usualFields.includes(k)
) as OtherFields;
return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
<Container
css={{
p: "$3 01",
fontSize: "$sm",
height: "calc(100% - 45px)",
}}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
mt: "1px",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Transaction type:{" "}
</Text>
<Select
instanceId="transactionsType"
placeholder="Select transaction type"
options={transactionsOptions}
hideSelectedOptions
css={{ width: "70%" }}
value={selectedTransaction}
onChange={(tx: any) => setState({ selectedTransaction: tx })}
/>
</Flex>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
<Select
instanceId="from-account"
placeholder="Select your account"
css={{ width: "70%" }}
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/>
</Flex>
{txFields.Amount !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Amount (XRP):{" "}
</Text>
<Input
value={txFields.Amount.value}
onChange={e =>
setState({
txFields: {
...txFields,
Amount: { type: "currency", value: e.target.value },
},
})
}
css={{ width: "70%", flex: "inherit" }}
/>
</Flex>
)}
{txFields.Destination !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Destination account:{" "}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: "70%" }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
/>
</Flex>
)}
{otherFields.map(field => {
let _value = txFields[field];
let value = typeof _value === "object" ? _value.value : _value;
value =
typeof value === "object"
? JSON.stringify(value)
: value?.toLocaleString();
let isCurrency =
typeof _value === "object" && _value.type === "currency";
return (
<Flex
key={field}
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isCurrency ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e =>
setState({
txFields: {
...txFields,
[field]:
typeof _value === "object"
? { ..._value, value: e.target.value }
: e.target.value,
},
})
}
css={{ width: "70%", flex: "inherit" }}
/>
</Flex>
);
})}
</Flex>
</Container>
<Flex
row
css={{
justifyContent: "space-between",
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
mb: "$1"
}}
>
<Button outline>VIEW AS JSON</Button>
<Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}>
RESET
</Button>
<Button
variant="primary"
onClick={submitTest}
isLoading={txIsLoading}
disabled={txIsDisabled}
>
<Play weight="bold" size="16px" />
RUN TEST
</Button>
</Flex>
</Flex>
</Box>
);
};
export default Transaction;

View File

@@ -0,0 +1,141 @@
import { Play } from "phosphor-react";
import { FC, useCallback, useState } from "react";
import { useSnapshot } from "valtio";
import state from "../../state";
import {
modifyTransaction,
prepareTransaction,
TransactionState,
} from "../../state/transactions";
import { sendTransaction } from "../../state/actions";
import Box from "../Box";
import Button from "../Button";
import Flex from "../Flex";
import { TxJson } from "./json";
import { TxUI } from "./ui";
export interface TransactionProps {
header: string;
state: TransactionState;
}
const Transaction: FC<TransactionProps> = ({
header,
state: txState,
...props
}) => {
const { accounts } = useSnapshot(state);
const {
selectedAccount,
selectedDestAccount,
selectedTransaction,
txFields,
txIsDisabled,
txIsLoading,
} = txState;
const [viewType, setViewType] = useState<"ui" | "json">("ui");
const setState = useCallback(
(pTx?: Partial<TransactionState>) => {
modifyTransaction(header, pTx);
},
[header]
);
const prepareOptions = useCallback(() => {
const TransactionType = selectedTransaction?.value;
const Destination = selectedDestAccount?.value;
const Account = selectedAccount?.value;
return prepareTransaction({
...txFields,
TransactionType,
Destination,
Account,
});
}, [
selectedAccount?.value,
selectedDestAccount?.value,
selectedTransaction?.value,
txFields,
]);
const submitTest = useCallback(async () => {
const account = accounts.find(
acc => acc.address === selectedAccount?.value
);
const TransactionType = selectedTransaction?.value;
if (!account || !TransactionType || txIsDisabled) return;
setState({ txIsLoading: true });
try {
const options = prepareOptions();
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
await sendTransaction(account, options, { logPrefix });
} catch (error) {
console.error(error);
if (error instanceof Error) {
state.transactionLogs.push({ type: "error", message: error.message });
}
}
setState({ txIsLoading: false });
}, [
accounts,
selectedTransaction?.value,
txIsDisabled,
setState,
prepareOptions,
selectedAccount?.value,
header,
]);
const resetState = useCallback(() => {
setState({});
}, [setState]);
return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
{viewType === "json" ? (
<TxJson prepareOptions={prepareOptions} />
) : (
<TxUI state={txState} setState={setState} />
)}
<Flex
row
css={{
justifyContent: "space-between",
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
mb: "$1",
}}
>
<Button
onClick={() => setViewType(viewType === "ui" ? "json" : "ui")}
outline
>
{viewType === "ui" ? "VIEW AS JSON" : "EXIT JSON VIEW"}
</Button>
<Flex row>
<Button onClick={resetState} outline css={{ mr: "$3" }}>
RESET
</Button>
<Button
variant="primary"
onClick={submitTest}
isLoading={txIsLoading}
disabled={txIsDisabled}
>
<Play weight="bold" size="16px" />
RUN TEST
</Button>
</Flex>
</Flex>
</Box>
);
};
export default Transaction;

View File

@@ -0,0 +1,56 @@
import Editor, { loader } from "@monaco-editor/react";
import { FC, useMemo } from 'react';
import { useTheme } from "next-themes";
import dark from "../../theme/editor/amy.json";
import light from "../../theme/editor/xcode_default.json";
import { useSnapshot } from 'valtio';
import state from '../../state';
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
interface JsonProps {
prepareOptions?: () => any
}
export const TxJson: FC<JsonProps> = ({ prepareOptions }) => {
const { editorSettings } = useSnapshot(state);
const { theme } = useTheme();
const value = useMemo(() => {
return JSON.stringify(
prepareOptions?.() || {},
null,
editorSettings.tabSize
);
}, [editorSettings.tabSize, prepareOptions])
return (
<Editor
className="hooks-editor"
language={"json"}
height="calc(100% - 45px)"
beforeMount={monaco => {
monaco.editor.defineTheme("dark", dark as any);
monaco.editor.defineTheme("light", light as any);
}}
value={value}
onMount={(editor, monaco) => {
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14,
readOnly: true,
});
}}
theme={theme === "dark" ? "dark" : "light"}
/>
);
};

View File

@@ -0,0 +1,243 @@
import { FC, useEffect } from "react";
import Container from "../Container";
import Flex from "../Flex";
import Input from "../Input";
import Select from "../Select";
import Text from "../Text";
import {
SelectOption,
TransactionState,
transactionsData,
TxFields,
OtherFields,
} from "../../state/transactions";
import { useSnapshot } from "valtio";
import state from "../../state";
import { streamState } from "../DebugStream";
interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => void;
state: TransactionState;
}
export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
const { accounts } = useSnapshot(state);
const {
selectedAccount,
selectedDestAccount,
selectedTransaction,
txFields,
txIsLoading,
} = txState;
const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc });
streamState.selectedAccount = acc;
};
const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const destAccountOptions: SelectOption[] = accounts
.map(acc => ({
label: acc.name,
value: acc.address,
}))
.filter(acc => acc.value !== selectedAccount?.value);
useEffect(() => {
const transactionType = selectedTransaction?.value;
const account = accounts.find(
acc => acc.address === selectedAccount?.value
);
if (!account || !transactionType || txIsLoading) {
setState({ txIsDisabled: true });
} else {
setState({ txIsDisabled: false });
}
}, [txIsLoading, selectedTransaction, selectedAccount, accounts, setState]);
useEffect(() => {
const txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === selectedTransaction?.value
);
if (!txFields) return setState({ txFields: {} });
const _txFields = Object.keys(txFields)
.filter(key => !["TransactionType", "Account", "Sequence"].includes(key))
.reduce<TxFields>(
(tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf),
{}
);
if (!_txFields.Destination) setState({ selectedDestAccount: null });
setState({ txFields: _txFields });
}, [setState, selectedTransaction]);
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter(
k => !usualFields.includes(k)
) as OtherFields;
return (
<Container
css={{
p: "$3 01",
fontSize: "$sm",
height: "calc(100% - 45px)",
}}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
mt: "1px",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Transaction type:{" "}
</Text>
<Select
instanceId="transactionsType"
placeholder="Select transaction type"
options={transactionsOptions}
hideSelectedOptions
css={{ width: "70%" }}
value={selectedTransaction}
onChange={(tx: any) => setState({ selectedTransaction: tx })}
/>
</Flex>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
<Select
instanceId="from-account"
placeholder="Select your account"
css={{ width: "70%" }}
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/>
</Flex>
{txFields.Amount !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Amount (XRP):{" "}
</Text>
<Input
value={txFields.Amount.value}
onChange={e =>
setState({
txFields: {
...txFields,
Amount: { type: "currency", value: e.target.value },
},
})
}
css={{ width: "70%", flex: "inherit" }}
/>
</Flex>
)}
{txFields.Destination !== undefined && (
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
Destination account:{" "}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: "70%" }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
/>
</Flex>
)}
{otherFields.map(field => {
let _value = txFields[field];
let value = typeof _value === "object" ? _value.value : _value;
value =
typeof value === "object"
? JSON.stringify(value)
: value?.toLocaleString();
let isCurrency =
typeof _value === "object" && _value.type === "currency";
return (
<Flex
key={field}
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isCurrency ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e =>
setState({
txFields: {
...txFields,
[field]:
typeof _value === "object"
? { ..._value, value: e.target.value }
: e.target.value,
},
})
}
css={{ width: "70%", flex: "inherit" }}
/>
</Flex>
);
})}
</Flex>
</Container>
);
};

View File

@@ -1,6 +1,28 @@
import { proxy } from 'valtio';
import { TransactionState } from '../components/Transaction';
import { deepEqual } from '../utils/object';
import transactionsData from "../content/transactions.json";
export type SelectOption = {
value: string;
label: string;
};
export interface TransactionState {
selectedTransaction: SelectOption | null;
selectedAccount: SelectOption | null;
selectedDestAccount: SelectOption | null;
txIsLoading: boolean;
txIsDisabled: boolean;
txFields: TxFields;
}
export type TxFields = Omit<
typeof transactionsData[0],
"Account" | "Sequence" | "TransactionType"
>;
export type OtherFields = (keyof Omit<TxFields, "Destination">)[];
export const defaultTransaction: TransactionState = {
selectedTransaction: null,
@@ -50,10 +72,55 @@ export const modifyTransaction = (
return;
}
if (deepEqual(partialTx, {})) {
tx.state = { ...defaultTransaction }
console.log({ tx: tx.state, is: tx.state === defaultTransaction })
}
Object.keys(partialTx).forEach(k => {
// Typescript mess here, but is definetly safe!
const s = tx.state as any;
const p = partialTx as any;
if (!deepEqual(s[k], p[k])) s[k] = p[k];
});
};
};
export const prepareTransaction = (data: any) => {
let options = { ...data };
// options.Destination = selectedDestAccount?.value;
(Object.keys(options)).forEach(field => {
let _value = options[field];
// convert currency
if (typeof _value === "object" && _value.type === "currency") {
if (+_value.value) {
options[field] = (+_value.value * 1000000 + "") as any;
} else {
options[field] = undefined; // 👇 💀
}
}
// handle type: `json`
if (typeof _value === "object" && _value.type === "json") {
if (typeof _value.value === "object") {
options[field] = _value.value as any;
} else {
try {
options[field] = JSON.parse(_value.value);
} catch (error) {
const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ""
}`;
throw Error(message);
}
}
}
// delete unneccesary fields
if (!options[field]) {
delete options[field];
}
});
return options
}
export { transactionsData }