Compare commits

...

18 Commits

Author SHA1 Message Date
Joni Juup
cc03c64f0a added a fixed height for logbox header to logbox content box height can be calculated easily 2022-02-02 15:03:31 +02:00
Joni Juup
3647aa6274 adjusted gutter sizes and highlight style 2022-02-02 12:39:08 +02:00
Joni Juup
a2a58f0ba9 light mode support 2022-02-02 12:15:58 +02:00
Joni Juup
c544a03be4 fixed log overflow, resize sizing 2022-02-02 12:12:07 +02:00
Joni Juup
9a09da88ec add panel resizing to views 2022-02-01 16:44:51 +02:00
Joni Juup
5850551906 fixed merge conflicts 2022-02-01 15:47:24 +02:00
muzam
e35e520d24 minor fix 2022-02-01 19:07:29 +05:30
muzamil
8077fc5865 Merge pull request #66 from eqlabs/feat/tabs
Transaction tabs
2022-02-01 19:02:26 +05:30
muzam
bff01b4a9f Merge branch 'main' into feat/tabs 2022-02-01 19:00:46 +05:30
muzamil
de5380d6f3 Merge pull request #67 from eqlabs/feat/debug-stream
Per account debug stream
2022-02-01 18:51:20 +05:30
muzamil
eda2a9596a Merge pull request #52 from eqlabs/transactions
Implemented Transactions!
2022-02-01 18:49:33 +05:30
muzam
195d33b1db Merge branch 'test-page' into transactions 2022-02-01 18:46:10 +05:30
muzam
4f042ef3b7 File prefixed logs 2022-02-01 18:42:19 +05:30
muzam
e6f613ae0b Fix tabs overflow 2022-02-01 13:53:18 +05:30
muzam
9b822cfda4 Resuable tabs component and transaction tabs 2022-01-31 20:27:49 +05:30
muzam
b5b918d877 minor changes 2022-01-31 18:55:15 +05:30
Vaclav Barta
739918647d fixed tail match 2022-01-20 10:37:24 +01:00
Vaclav Barta
1f334d6253 proposed fix for #59 2022-01-20 10:18:44 +01:00
15 changed files with 635 additions and 158 deletions

View File

@@ -36,11 +36,13 @@ const AccountDialog = ({
}) => {
const snap = useSnapshot(state);
const [showSecret, setShowSecret] = useState(false);
const activeAccount = snap.accounts.find(account => account.address === activeAccountAddress);
const activeAccount = snap.accounts.find(
(account) => account.address === activeAccountAddress
);
return (
<Dialog
open={Boolean(activeAccountAddress)}
onOpenChange={open => {
onOpenChange={(open) => {
setShowSecret(false);
!open && setActiveAccountAddress(null);
}}
@@ -135,7 +137,7 @@ const AccountDialog = ({
}}
ghost
size="xs"
onClick={() => setShowSecret(curr => !curr)}
onClick={() => setShowSecret((curr) => !curr)}
>
{showSecret ? "Hide" : "Show"}
</Button>
@@ -181,7 +183,11 @@ const AccountDialog = ({
target="_blank"
rel="noreferrer noopener"
>
<Button size="sm" ghost css={{ color: "$green11 !important", mt: "$3" }}>
<Button
size="sm"
ghost
css={{ color: "$green11 !important", mt: "$3" }}
>
<ArrowSquareOut size="15px" />
</Button>
</a>
@@ -197,8 +203,10 @@ const AccountDialog = ({
>
{activeAccount && activeAccount?.hooks?.length > 0
? activeAccount?.hooks
.map(i => {
return `${i?.substring(0, 6)}...${i?.substring(i.length - 4)}`;
.map((i) => {
return `${i?.substring(0, 6)}...${i?.substring(
i.length - 4
)}`;
})
.join(", ")
: ""}
@@ -223,13 +231,15 @@ interface AccountProps {
showHookStats?: boolean;
}
const Accounts: FC<AccountProps> = props => {
const Accounts: FC<AccountProps> = (props) => {
const snap = useSnapshot(state);
const [activeAccountAddress, setActiveAccountAddress] = useState<string | null>(null);
const [activeAccountAddress, setActiveAccountAddress] = useState<
string | null
>(null);
useEffect(() => {
const fetchAccInfo = async () => {
if (snap.clientStatus === "online") {
const requests = snap.accounts.map(acc =>
const requests = snap.accounts.map((acc) =>
snap.client?.send({
id: acc.address,
command: "account_info",
@@ -241,13 +251,15 @@ const Accounts: FC<AccountProps> = props => {
const address = res?.account_data?.Account as string;
const balance = res?.account_data?.Balance as string;
const sequence = res?.account_data?.Sequence as number;
const accountToUpdate = state.accounts.find(acc => acc.address === address);
const accountToUpdate = state.accounts.find(
(acc) => acc.address === address
);
if (accountToUpdate) {
accountToUpdate.xrp = balance;
accountToUpdate.sequence = sequence;
}
});
const objectRequests = snap.accounts.map(acc => {
const objectRequests = snap.accounts.map((acc) => {
return snap.client?.send({
id: `${acc.address}-hooks`,
command: "account_objects",
@@ -257,7 +269,9 @@ const Accounts: FC<AccountProps> = props => {
const objectResponses = await Promise.all(objectRequests);
objectResponses.forEach((res: any) => {
const address = res?.account as string;
const accountToUpdate = state.accounts.find(acc => acc.address === address);
const accountToUpdate = state.accounts.find(
(acc) => acc.address === address
);
if (accountToUpdate) {
accountToUpdate.hooks = res.account_objects
.filter((ac: any) => ac?.LedgerEntryType === "Hook")
@@ -289,18 +303,19 @@ const Accounts: FC<AccountProps> = props => {
display: "flex",
backgroundColor: props.card ? "$deep" : "$mauve1",
position: "relative",
width: "100%",
flex: "1",
height: "100%",
flexShrink: 0,
borderTop: "1px solid $mauve6",
borderRight: "1px solid $mauve6",
borderLeft: "1px solid $mauve6",
borderBottom: "1px solid $mauve6",
border: "1px solid $mauve6",
borderRadius: props.card ? "$md" : undefined,
}}
>
<Container css={{ p: 0, flexShrink: 1, height: "100%" }}>
<Flex css={{ py: "$3", borderBottom: props.card ? "1px solid $mauve6" : undefined }}>
<Flex
css={{
py: "$3",
borderBottom: props.card ? "1px solid $mauve6" : undefined,
}}
>
<Heading
as="h3"
css={{
@@ -338,7 +353,7 @@ const Accounts: FC<AccountProps> = props => {
overflowY: "auto",
}}
>
{snap.accounts.map(account => (
{snap.accounts.map((account) => (
<Flex
column
key={account.address + account.name}
@@ -363,7 +378,12 @@ const Accounts: FC<AccountProps> = props => {
>
<Box>
<Text>{account.name} </Text>
<Text css={{ color: "$mauve9" }}>
<Text
css={{
color: "$mauve9",
wordBreak: "break-word",
}}
>
{account.address} (
{Dinero({
amount: Number(account?.xrp || "0"),
@@ -386,10 +406,11 @@ const Accounts: FC<AccountProps> = props => {
isLoading={account.isLoading}
disabled={
account.isLoading ||
!snap.files.filter(file => file.compiledWatContent).length
!snap.files.filter((file) => file.compiledWatContent)
.length
}
variant="secondary"
onClick={e => {
onClick={(e) => {
e.stopPropagation();
deployHook(account);
}}
@@ -432,7 +453,7 @@ const ImportAccountDialog = () => {
name="secret"
type="password"
value={value}
onChange={e => setValue(e.target.value)}
onChange={(e) => setValue(e.target.value)}
/>
</DialogDescription>

View File

@@ -71,7 +71,7 @@ const HooksEditor = () => {
keepCurrentModel
defaultLanguage={snap.files?.[snap.active]?.language}
language={snap.files?.[snap.active]?.language}
path={`file://work/c/${snap.files?.[snap.active]?.name}`}
path={`file:///work/c/${snap.files?.[snap.active]?.name}`}
defaultValue={snap.files?.[snap.active]?.content}
beforeMount={monaco => {
if (!snap.editorCtx) {
@@ -79,7 +79,7 @@ const HooksEditor = () => {
monaco.editor.createModel(
file.content,
file.language,
monaco.Uri.parse(`file://work/c/${file.name}`)
monaco.Uri.parse(`file:///work/c/${file.name}`)
)
);
}

View File

@@ -21,7 +21,14 @@ interface ILogBox {
enhanced?: boolean;
}
const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhanced }) => {
const LogBox: React.FC<ILogBox> = ({
title,
clearLog,
logs,
children,
renderNav,
enhanced,
}) => {
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
@@ -38,10 +45,23 @@ const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children, renderNav,
background: "$mauve1",
position: "relative",
flex: 1,
height: "100%",
}}
>
<Container css={{ px: 0, flexShrink: 1 }}>
<Flex css={{ py: "$3", alignItems: "center", fontSize: "$sm", fontWeight: 300 }}>
<Container
css={{
px: 0,
height: "100%",
}}
>
<Flex
css={{
height: "48px",
alignItems: "center",
fontSize: "$sm",
fontWeight: 300,
}}
>
<Heading
as="h3"
css={{
@@ -67,6 +87,7 @@ const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children, renderNav,
)}
</Flex>
</Flex>
<Box
as="pre"
ref={logRef}
@@ -76,14 +97,14 @@ const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children, renderNav,
display: "flex",
flexDirection: "column",
width: "100%",
height: "160px",
height: "calc(100% - 48px)", // 100% minus the logbox header height
overflowY: "auto",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
px: "$3",
pb: "$2",
whiteSpace: "normal",
overflowY: "auto",
}}
>
{logs?.map((log, index) => (
@@ -96,7 +117,7 @@ const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children, renderNav,
backgroundColor: enhanced ? "$backgroundAlt" : undefined,
},
},
p: "$2 $1",
p: enhanced ? "$2 $1" : undefined,
}}
>
<LogText variant={log.type}>

View File

@@ -4,7 +4,7 @@ const Text = styled("span", {
fontFamily: "$monospace",
lineHeight: "$body",
color: "$text",
wordWrap: 'break-word',
wordWrap: "break-word",
variants: {
variant: {
log: {

View File

@@ -27,7 +27,7 @@ import {
DialogTrigger,
} from "./Dialog";
import PanelBox from "./PanelBox";
import { templateFileIds } from '../state/constants';
import { templateFileIds } from "../state/constants";
const Navigation = () => {
const router = useRouter();
@@ -43,6 +43,7 @@ const Navigation = () => {
borderBottom: "1px solid $mauve6",
position: "relative",
zIndex: 2003,
height: "60px",
}}
>
<Container
@@ -83,8 +84,12 @@ const Navigation = () => {
<Spinner />
) : (
<>
<Heading css={{ lineHeight: 1 }}>{snap.files?.[0]?.name || "XRPL Hooks"}</Heading>
<Text css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}>
<Heading css={{ lineHeight: 1 }}>
{snap.files?.[0]?.name || "XRPL Hooks"}
</Heading>
<Text
css={{ fontSize: "$xs", color: "$mauve10", lineHeight: 1 }}
>
{snap.files.length > 0 ? "Gist: " : "Playground"}
<Text css={{ color: "$mauve12" }}>
{snap.files.length > 0 &&
@@ -96,7 +101,10 @@ const Navigation = () => {
</Flex>
{router.isReady && (
<ButtonGroup css={{ marginLeft: "auto" }}>
<Dialog open={snap.mainModalOpen} onOpenChange={open => (state.mainModalOpen = open)}>
<Dialog
open={snap.mainModalOpen}
onOpenChange={(open) => (state.mainModalOpen = open)}
>
<DialogTrigger asChild>
<Button outline>
<FolderOpen size="15px" />
@@ -157,9 +165,12 @@ const Navigation = () => {
mb: "$7",
}}
>
Hooks add smart contract functionality to the XRP Ledger.
Hooks add smart contract functionality to the XRP
Ledger.
</Text>
<Flex css={{ flexDirection: "column", gap: "$2", mt: "$2" }}>
<Flex
css={{ flexDirection: "column", gap: "$2", mt: "$2" }}
>
<Text
css={{
display: "inline-flex",
@@ -244,27 +255,54 @@ const Navigation = () => {
},
}}
>
<PanelBox as="a" href={`/develop/${templateFileIds.starter}`}>
<PanelBox
as="a"
href={`/develop/${templateFileIds.starter}`}
>
<Heading>Starter</Heading>
<Text>Just an empty starter with essential imports</Text>
<Text>
Just an empty starter with essential imports
</Text>
</PanelBox>
<PanelBox as="a" href={`/develop/${templateFileIds.starter}`}>
<PanelBox
as="a"
href={`/develop/${templateFileIds.starter}`}
>
<Heading>Firewall</Heading>
<Text>This Hook essentially checks a blacklist of accounts</Text>
<Text>
This Hook essentially checks a blacklist of accounts
</Text>
</PanelBox>
<PanelBox as="a" href={`/develop/${templateFileIds.accept}`}>
<PanelBox
as="a"
href={`/develop/${templateFileIds.accept}`}
>
<Heading>Accept</Heading>
<Text>This hook just accepts any transaction coming through it</Text>
<Text>
This hook just accepts any transaction coming through
it
</Text>
</PanelBox>
<PanelBox as="a" href={`/develop/${templateFileIds.notary}`}>
<PanelBox
as="a"
href={`/develop/${templateFileIds.notary}`}
>
<Heading>Notary</Heading>
<Text>Collecting signatures for multi-sign transactions</Text>
<Text>
Collecting signatures for multi-sign transactions
</Text>
</PanelBox>
<PanelBox as="a" href={`/develop/${templateFileIds.carbon}`}>
<PanelBox
as="a"
href={`/develop/${templateFileIds.carbon}`}
>
<Heading>Carbon</Heading>
<Text>Send a percentage of sum to an address</Text>
</PanelBox>
<PanelBox as="a" href={`/develop/${templateFileIds.peggy}`}>
<PanelBox
as="a"
href={`/develop/${templateFileIds.peggy}`}
>
<Heading>Peggy</Heading>
<Text>An oracle based stabe coin hook</Text>
</PanelBox>
@@ -313,18 +351,42 @@ const Navigation = () => {
}}
>
<ButtonGroup>
<Link href={gistId ? `/develop/${gistId}` : "/develop"} passHref shallow>
<Button as="a" outline={!router.pathname.includes("/develop")} uppercase>
<Link
href={gistId ? `/develop/${gistId}` : "/develop"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/develop")}
uppercase
>
Develop
</Button>
</Link>
<Link href={gistId ? `/deploy/${gistId}` : "/deploy"} passHref shallow>
<Button as="a" outline={!router.pathname.includes("/deploy")} uppercase>
<Link
href={gistId ? `/deploy/${gistId}` : "/deploy"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/deploy")}
uppercase
>
Deploy
</Button>
</Link>
<Link href={gistId ? `/test/${gistId}` : "/test"} passHref shallow>
<Button as="a" outline={!router.pathname.includes("/test")} uppercase>
<Link
href={gistId ? `/test/${gistId}` : "/test"}
passHref
shallow
>
<Button
as="a"
outline={!router.pathname.includes("/test")}
uppercase
>
Test
</Button>
</Link>

View File

@@ -1,5 +1,5 @@
import { FC } from "react";
import { gray, grayDark } from "@radix-ui/colors";
import { mauve, mauveDark } from "@radix-ui/colors";
import { useTheme } from "next-themes";
import { styled } from '../stitches.config';
import dynamic from 'next/dynamic';
@@ -11,11 +11,11 @@ const Select: FC<Props> = props => {
const isDark = theme === "dark";
const colors: any = {
// primary: pink.pink9,
primary: isDark ? grayDark.gray4 : gray.gray4,
secondary: isDark ? grayDark.gray8 : gray.gray8,
background: isDark ? "rgb(10, 10, 10)" : "rgb(244, 244, 244)",
searchText: isDark ? grayDark.gray12 : gray.gray12,
placeholder: isDark ? grayDark.gray11 : gray.gray11,
primary: isDark ? mauveDark.mauve4 : mauve.mauve4,
secondary: isDark ? mauveDark.mauve8 : mauve.mauve8,
background: isDark ? "rgb(10, 10, 10)" : "rgb(245, 245, 245)",
searchText: isDark ? mauveDark.mauve12 : mauve.mauve12,
placeholder: isDark ? mauveDark.mauve11 : mauve.mauve11,
};
colors.outline = colors.background;
colors.selected = colors.secondary;

View File

@@ -1,24 +1,60 @@
import { useEffect, useState } from "react";
import React, { useEffect, useState, Fragment, isValidElement, useCallback } from "react";
import type { ReactNode, ReactElement } from "react";
import { Button, Stack } from ".";
import { Box, Button, Flex, Input, Stack, Text } from ".";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
} from "./Dialog";
import { Plus, X } from "phosphor-react";
import { styled } from "../stitches.config";
const ErrorText = styled(Text, {
color: "$red9",
mt: "$1",
display: "block",
});
interface TabProps {
header?: string;
children: ReactNode;
}
// TODO customise strings shown
interface Props {
activeIndex?: number;
activeHeader?: string;
headless?: boolean;
children: ReactElement<TabProps>[];
keepAllAlive?: boolean;
defaultExtension?: string;
forceDefaultExtension?: boolean;
onCreateNewTab?: (name: string) => any;
onCloseTab?: (index: number, header?: string) => any;
}
export const Tab = (props: TabProps) => null;
export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) => {
export const Tabs = ({
children,
activeIndex,
activeHeader,
headless,
keepAllAlive = false,
onCreateNewTab,
onCloseTab,
defaultExtension = "",
forceDefaultExtension,
}: Props) => {
const [active, setActive] = useState(activeIndex || 0);
const tabProps: TabProps[] = children.map(elem => elem.props);
const tabs: TabProps[] = children.map(elem => elem.props);
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
const [tabname, setTabname] = useState("");
const [newtabError, setNewtabError] = useState<string | null>(null);
useEffect(() => {
if (activeIndex) setActive(activeIndex);
@@ -26,10 +62,57 @@ export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) =
useEffect(() => {
if (activeHeader) {
const idx = tabProps.findIndex(tab => tab.header === activeHeader);
const idx = tabs.findIndex(tab => tab.header === activeHeader);
setActive(idx);
}
}, [activeHeader, tabProps]);
}, [activeHeader, tabs]);
// when filename changes, reset error
useEffect(() => {
setNewtabError(null);
}, [tabname, setNewtabError]);
const validateTabname = useCallback(
(tabname: string): { error: string | null } => {
if (tabs.find(tab => tab.header === tabname)) {
return { error: "Name already exists." };
}
return { error: null };
},
[tabs]
);
const handleCreateTab = useCallback(() => {
// add default extension in case omitted
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = _tabname + defaultExtension;
}
const chk = validateTabname(_tabname);
if (chk.error) {
setNewtabError(`Error: ${chk.error}`);
return;
}
setIsNewtabDialogOpen(false);
setTabname("");
// switch to new tab?
setActive(tabs.length);
onCreateNewTab?.(_tabname);
}, [tabname, defaultExtension, validateTabname, onCreateNewTab, tabs.length]);
const handleCloseTab = useCallback(
(idx: number) => {
if (idx <= active && active !== 0) {
setActive(active - 1);
}
onCloseTab?.(idx, tabs[idx].header);
},
[active, onCloseTab, tabs]
);
return (
<>
@@ -40,11 +123,13 @@ export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) =
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
width: "100%",
overflow: "auto",
}}
>
{tabProps.map((prop, idx) => (
{tabs.map((tab, idx) => (
<Button
key={prop.header}
key={tab.header}
role="tab"
tabIndex={idx}
onClick={() => setActive(idx)}
@@ -59,12 +144,99 @@ export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) =
},
}}
>
{prop.header || idx}
{tab.header || idx}
{onCloseTab && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
handleCloseTab(idx);
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
))}
{onCreateNewTab && (
<Dialog open={isNewtabDialogOpen} onOpenChange={setIsNewtabDialogOpen}>
<DialogTrigger asChild>
<Button ghost size="sm" css={{ alignItems: "center", px: "$2", mr: "$3" }}>
<Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new tab</DialogTitle>
<DialogDescription>
<label>Tabname</label>
<Input
value={tabname}
onChange={e => setTabname(e.target.value)}
onKeyPress={e => {
if (e.key === "Enter") {
handleCreateTab();
}
}}
/>
<ErrorText>{newtabError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleCreateTab}>
Create
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)}
</Stack>
)}
{tabProps[active].children}
{keepAllAlive ? (
tabs.map((tab, idx) => {
// TODO Maybe rule out fragments as children
if (!isValidElement(tab.children)) {
if (active !== idx) return null;
return tab.children;
}
let key = tab.children.key || tab.header || idx;
let { children } = tab;
let { style, ...props } = children.props;
return (
<children.type
key={key}
{...props}
style={{ ...style, display: active !== idx ? "none" : undefined }}
/>
);
})
) : (
<Fragment key={tabs[active].header || active}>{tabs[active].children}</Fragment>
)}
</>
);
};

View File

@@ -41,6 +41,7 @@
"react-hot-toast": "^2.1.1",
"react-new-window": "^0.2.1",
"react-select": "^5.2.1",
"react-split": "^2.0.14",
"react-stay-scrolled": "^7.4.0",
"reconnecting-websocket": "^4.4.0",
"valtio": "^1.2.5",

View File

@@ -1,8 +1,8 @@
import React from "react";
import dynamic from "next/dynamic";
import { Flex, Box } from "../../components";
import { useSnapshot } from "valtio";
import state from "../../state";
import Split from "react-split";
const DeployEditor = dynamic(() => import("../../components/DeployEditor"), {
ssr: false,
@@ -19,23 +19,41 @@ const LogBox = dynamic(() => import("../../components/LogBox"), {
const Deploy = () => {
const snap = useSnapshot(state);
return (
<>
<main style={{ display: "flex", flex: 1, height: 'calc(100vh - 30vh - 60px)' }}>
<Split
direction="vertical"
gutterSize={4}
gutterAlign="center"
sizes={[40, 60]}
style={{ height: "calc(100vh - 60px)" }}
>
<main style={{ display: "flex", flex: 1, position: "relative" }}>
<DeployEditor />
</main>
<Flex css={{ flexDirection: "row", width: "100%", minHeight: '225px', height: '30vh' }}>
<Box css={{ width: "100%" }}>
<Split
direction="horizontal"
sizes={[50, 50]}
minSize={[320, 160]}
gutterSize={4}
gutterAlign="center"
style={{
display: "flex",
flexDirection: "row",
width: "100%",
height: "100%",
}}
>
<div style={{ alignItems: "stretch", display: "flex" }}>
<Accounts />
</Box>
<Box css={{ width: "100%" }}>
</div>
<div>
<LogBox
title="Deploy Log"
logs={snap.deployLogs}
clearLog={() => (state.deployLogs = [])}
/>
</Box>
</Flex>
</>
</div>
</Split>
</Split>
);
};

View File

@@ -2,6 +2,7 @@ import dynamic from "next/dynamic";
import { useSnapshot } from "valtio";
import Hotkeys from "react-hot-keys";
import { Play } from "phosphor-react";
import Split from "react-split";
import type { NextPage } from "next";
import { compileCode } from "../../state/actions";
@@ -19,8 +20,16 @@ const LogBox = dynamic(() => import("../../components/LogBox"), {
const Home: NextPage = () => {
const snap = useSnapshot(state);
return (
<>
<Split
direction="vertical"
sizes={[70, 30]}
minSize={[100, 100]}
gutterAlign="center"
gutterSize={4}
style={{ height: "calc(100vh - 60px)" }}
>
<main style={{ display: "flex", flex: 1, position: "relative" }}>
<HooksEditor />
{snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() ===
@@ -65,7 +74,7 @@ const Home: NextPage = () => {
logs={snap.logs}
/>
</Box>
</>
</Split>
);
};

View File

@@ -1,10 +1,21 @@
import { Container, Flex, Box, Tabs, Tab, Input, Select, Text, Button } from "../../components";
import {
Container,
Flex,
Box,
Tabs,
Tab,
Input,
Select,
Text,
Button,
} from "../../components";
import { Play } from "phosphor-react";
import dynamic from "next/dynamic";
import { useSnapshot } from "valtio";
import Split from "react-split";
import state from "../../state";
import { sendTransaction } from "../../state/actions";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useState, FC } from "react";
import transactionsData from "../../content/transactions.json";
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
@@ -19,13 +30,20 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
});
// type SelectOption<T> = { value: T, label: string };
type TxFields = Omit<typeof transactionsData[0], "Account" | "Sequence" | "TransactionType">;
type TxFields = Omit<
typeof transactionsData[0],
"Account" | "Sequence" | "TransactionType"
>;
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
const Transaction = () => {
interface Props {
header?: string;
}
const Transaction: FC<Props> = ({ header, ...props }) => {
const snap = useSnapshot(state);
const transactionsOptions = transactionsData.map(tx => ({
const transactionsOptions = transactionsData.map((tx) => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
@@ -33,18 +51,20 @@ const Transaction = () => {
typeof transactionsOptions[0] | null
>(null);
const accountOptions = snap.accounts.map(acc => ({
const accountOptions = snap.accounts.map((acc) => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState<typeof accountOptions[0] | null>(null);
const [selectedAccount, setSelectedAccount] = useState<
typeof accountOptions[0] | null
>(null);
const destAccountOptions = snap.accounts
.map(acc => ({
.map((acc) => ({
label: acc.name,
value: acc.address,
}))
.filter(acc => acc.value !== selectedAccount?.value);
.filter((acc) => acc.value !== selectedAccount?.value);
const [selectedDestAccount, setSelectedDestAccount] = useState<
typeof destAccountOptions[0] | null
>(null);
@@ -55,7 +75,9 @@ const Transaction = () => {
useEffect(() => {
const transactionType = selectedTransaction?.value;
const account = snap.accounts.find(acc => acc.address === selectedAccount?.value);
const account = snap.accounts.find(
(acc) => acc.address === selectedAccount?.value
);
if (!account || !transactionType || txIsLoading) {
setTxIsDisabled(true);
} else {
@@ -65,7 +87,7 @@ const Transaction = () => {
useEffect(() => {
let _txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === selectedTransaction?.value
(tx) => tx.TransactionType === selectedTransaction?.value
);
if (!_txFields) return setTxFields({});
_txFields = { ..._txFields } as TxFields;
@@ -81,7 +103,9 @@ const Transaction = () => {
}, [selectedTransaction, setSelectedDestAccount]);
const submitTest = useCallback(async () => {
const account = snap.accounts.find(acc => acc.address === selectedAccount?.value);
const account = snap.accounts.find(
(acc) => acc.address === selectedAccount?.value
);
const TransactionType = selectedTransaction?.value;
if (!account || !TransactionType || txIsDisabled) return;
@@ -91,7 +115,7 @@ const Transaction = () => {
let options = { ...txFields };
options.Destination = selectedDestAccount?.value;
(Object.keys(options) as (keyof TxFields)[]).forEach(field => {
(Object.keys(options) as (keyof TxFields)[]).forEach((field) => {
let _value = options[field];
// convert currency
if (typeof _value === "object" && _value.type === "currency") {
@@ -122,10 +146,15 @@ const Transaction = () => {
delete options[field];
}
});
await sendTransaction(account, {
TransactionType,
...options,
});
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
await sendTransaction(
account,
{
TransactionType,
...options,
},
{ logPrefix }
);
} catch (error) {
console.error(error);
if (error instanceof Error) {
@@ -134,9 +163,10 @@ const Transaction = () => {
}
setTxIsLoading(false);
}, [
selectedAccount,
selectedDestAccount,
selectedTransaction,
header,
selectedAccount?.value,
selectedDestAccount?.value,
selectedTransaction?.value,
snap.accounts,
txFields,
txIsDisabled,
@@ -152,12 +182,20 @@ const Transaction = () => {
}, []);
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
const otherFields = Object.keys(txFields).filter(k => !usualFields.includes(k)) as OtherFields;
const otherFields = Object.keys(txFields).filter(
(k) => !usualFields.includes(k)
) as OtherFields;
return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }}>
<Container css={{ p: "$3 0", fontSize: "$sm", height: "calc(100% - 28px)" }}>
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
<Container
css={{ p: "$3 0", fontSize: "$sm", height: "calc(100% - 28px)" }}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex row fluid css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}>
<Flex
row
fluid
css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}
>
<Text muted css={{ mr: "$3" }}>
Transaction type:{" "}
</Text>
@@ -168,10 +206,14 @@ const Transaction = () => {
hideSelectedOptions
css={{ width: "70%" }}
value={selectedTransaction}
onChange={tt => setSelectedTransaction(tt as any)}
onChange={(tt) => setSelectedTransaction(tt as any)}
/>
</Flex>
<Flex row fluid css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}>
<Flex
row
fluid
css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}
>
<Text muted css={{ mr: "$3" }}>
Account:{" "}
</Text>
@@ -181,17 +223,25 @@ const Transaction = () => {
css={{ width: "70%" }}
options={accountOptions}
value={selectedAccount}
onChange={acc => setSelectedAccount(acc as any)}
onChange={(acc) => setSelectedAccount(acc as any)}
/>
</Flex>
{txFields.Amount !== undefined && (
<Flex row fluid css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
}}
>
<Text muted css={{ mr: "$3" }}>
Amount (XRP):{" "}
</Text>
<Input
value={txFields.Amount.value}
onChange={e =>
onChange={(e) =>
setTxFields({
...txFields,
Amount: { type: "currency", value: e.target.value },
@@ -203,7 +253,15 @@ const Transaction = () => {
</Flex>
)}
{txFields.Destination !== undefined && (
<Flex row fluid css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
}}
>
<Text muted css={{ mr: "$3" }}>
Destination account:{" "}
</Text>
@@ -214,28 +272,36 @@ const Transaction = () => {
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={acc => setSelectedDestAccount(acc as any)}
onChange={(acc) => setSelectedDestAccount(acc as any)}
/>
</Flex>
)}
{otherFields.map(field => {
{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";
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" }}
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isCurrency ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e =>
onChange={(e) =>
setTxFields({
...txFields,
[field]:
@@ -284,41 +350,94 @@ const Transaction = () => {
const Test = () => {
const snap = useSnapshot(state);
const [tabHeaders, setTabHeaders] = useState<string[]>(["test1.json"]);
return (
<Container css={{ py: "$3", px: 0 }}>
<Flex
row
fluid
css={{ justifyContent: "center", mb: "$2", height: "40vh", minHeight: "300px", p: "$3 $2" }}
<Container css={{ px: 0 }}>
<Split
direction="vertical"
sizes={[50, 50]}
gutterSize={4}
gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }}
>
<Box css={{ width: "60%", px: "$2", maxWidth: "800px", height: "100%", overflow: "auto" }}>
<Tabs>
{/* TODO Dynamic tabs */}
<Tab header="test1.json">
<Transaction />
</Tab>
<Tab header="test2.json">
<Transaction />
</Tab>
</Tabs>
</Box>
<Box css={{ width: "40%", mx: "$2", height: "100%", maxWidth: "750px" }}>
<Accounts card hideDeployBtn showHookStats />
</Box>
</Flex>
<Flex
row
fluid
css={{
justifyContent: "center",
p: "$3 $2",
}}
>
<Split
direction="horizontal"
sizes={[50, 50]}
minSize={[180, 320]}
gutterSize={4}
gutterAlign="center"
style={{
display: "flex",
flexDirection: "row",
width: "100%",
height: "100%",
}}
>
<Box css={{ width: "55%", px: "$2" }}>
<Tabs
keepAllAlive
forceDefaultExtension
defaultExtension=".json"
onCreateNewTab={(name) =>
setTabHeaders(tabHeaders.concat(name))
}
onCloseTab={(index) =>
setTabHeaders(tabHeaders.filter((_, idx) => idx !== index))
}
>
{tabHeaders.map((header) => (
<Tab key={header} header={header}>
<Transaction header={header} />
</Tab>
))}
</Tabs>
</Box>
<Box css={{ width: "45%", mx: "$2", height: "100%" }}>
<Accounts card hideDeployBtn showHookStats />
</Box>
</Split>
</Flex>
<Flex row fluid css={{ borderBottom: "1px solid $mauve8" }}>
<Box css={{ width: "50%", borderRight: "1px solid $mauve8" }}>
<LogBox
title="Development Log"
logs={snap.transactionLogs}
clearLog={() => (state.transactionLogs = [])}
/>
</Box>
<Box css={{ width: "50%" }}>
<DebugStream />
</Box>
</Flex>
<Flex row fluid>
<Split
direction="horizontal"
sizes={[50, 50]}
minSize={[320, 160]}
gutterSize={4}
gutterAlign="center"
style={{
display: "flex",
flexDirection: "row",
width: "100%",
height: "100%",
}}
>
<Box
css={{
borderRight: "1px solid $mauve8",
height: "100%",
}}
>
<LogBox
title="Development Log"
logs={snap.transactionLogs}
clearLog={() => (state.transactionLogs = [])}
/>
</Box>
<Box css={{ height: "100%" }}>
<DebugStream />
</Box>
</Split>
</Flex>
</Split>
</Container>
);
};

View File

@@ -4,8 +4,9 @@ import state from '../index';
// Saves the current editor content to global state
export const saveFile = (showToast: boolean = true) => {
const editorModels = state.editorCtx?.getModels();
const sought = '/' + state.files[state.active].name;
const currentModel = editorModels?.find((editorModel) => {
return editorModel.uri.path === `/c/${state.files[state.active].name}`;
return editorModel.uri.path.endsWith(sought);
});
if (state.files.length > 0) {
state.files[state.active].content = currentModel?.getValue() || "";
@@ -13,4 +14,4 @@ export const saveFile = (showToast: boolean = true) => {
if (showToast) {
toast.success("Saved successfully", { position: "bottom-center" });
}
};
};

View File

@@ -10,8 +10,11 @@ interface TransactionOptions {
Destination?: string
[index: string]: any
}
interface OtherOptions {
logPrefix?: string
}
export const sendTransaction = async (account: IAccount, txOptions: TransactionOptions) => {
export const sendTransaction = async (account: IAccount, txOptions: TransactionOptions, options?: OtherOptions) => {
if (!state.client) throw Error('XRPL client not initalized')
const { Fee = "1000", ...opts } = txOptions
@@ -21,7 +24,7 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
Fee, // TODO auto-fillable
...opts
};
console.log({ tx });
const { logPrefix = '' } = options || {}
try {
const signedAccount = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, signedAccount);
@@ -32,19 +35,19 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
if (response.engine_result === "tesSUCCESS") {
state.transactionLogs.push({
type: 'success',
message: `Transaction success [${response.engine_result}]: ${response.engine_result_message}`
message: `${logPrefix}[${response.engine_result}] ${response.engine_result_message}`
})
} else {
state.transactionLogs.push({
type: "error",
message: `[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
message: `${logPrefix}[${response.error || response.engine_result}] ${response.error_exception || response.engine_result_message}`,
});
}
} catch (err) {
console.error(err);
state.transactionLogs.push({
type: "error",
message: err instanceof Error ? `Error: ${err.message}` : 'Something went wrong, try again later',
message: err instanceof Error ? `${logPrefix}Error: ${err.message}` : `${logPrefix}Something went wrong, try again later`,
});
}
};

View File

@@ -6,8 +6,36 @@ body,
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-y: hidden;
}
* {
box-sizing: border-box;
}
.gutter {
position: relative;
transition: border-color 0.3s, background-color 0.3s;
}
.gutter-vertical {
margin-top: -4px;
}
.gutter-horizontal {
margin-left: -4px;
}
.gutter-vertical:hover {
cursor: row-resize;
background-color: rgba(255, 255, 255, 0.25);
}
html.light .gutter-vertical:hover {
background-color: rgba(0, 0, 0, 0.25);
}
.gutter-horizontal:hover {
cursor: col-resize;
background-color: rgba(255, 255, 255, 0.25);
}
html.light .gutter-horizontal:hover {
background-color: rgba(0, 0, 0, 0.25);
}

View File

@@ -3688,6 +3688,15 @@ progress@^2.0.0:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
prop-types@^15.5.7:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.13.1"
prop-types@^15.6.0, prop-types@^15.6.2:
version "15.8.0"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.0.tgz#d237e624c45a9846e469f5f31117f970017ff588"
@@ -3855,6 +3864,14 @@ react-select@^5.2.1:
prop-types "^15.6.0"
react-transition-group "^4.3.0"
react-split@^2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/react-split/-/react-split-2.0.14.tgz#ef198259bf43264d605f792fb3384f15f5b34432"
integrity sha512-bKWydgMgaKTg/2JGQnaJPg51T6dmumTWZppFgEbbY0Fbme0F5TuatAScCLaqommbGQQf/ZT1zaejuPDriscISA==
dependencies:
prop-types "^15.5.7"
split.js "^1.6.0"
react-stay-scrolled@^7.4.0:
version "7.4.0"
resolved "https://registry.yarnpkg.com/react-stay-scrolled/-/react-stay-scrolled-7.4.0.tgz#cb109b8dfd7834e5406d9c9322035ab40c398368"
@@ -4259,6 +4276,11 @@ source-map@^0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
split.js@^1.6.0:
version "1.6.5"
resolved "https://registry.yarnpkg.com/split.js/-/split.js-1.6.5.tgz#f7f61da1044c9984cb42947df4de4fadb5a3f300"
integrity sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"