Compare commits

..

9 Commits

Author SHA1 Message Date
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
4f042ef3b7 File prefixed logs 2022-02-01 18:42:19 +05:30
muzam
17c67822a9 First draft of debug stream 2022-02-01 17:05:53 +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
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
11 changed files with 352 additions and 50 deletions

View File

@@ -350,7 +350,7 @@ const Accounts: FC<AccountProps> = props => {
borderBottom: props.card ? "1px solid $mauve6" : undefined,
"@hover": {
"&:hover": {
background: "$mauve3",
background: "$backgroundAlt",
},
},
}}

View File

@@ -0,0 +1,86 @@
import { useEffect, useState } from "react";
import { useSnapshot } from "valtio";
import { Select } from ".";
import state from "../state";
import LogBox from "./LogBox";
import Text from "./Text";
const DebugStream = () => {
const snap = useSnapshot(state);
const accountOptions = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState<typeof accountOptions[0] | null>(null);
const renderNav = () => (
<>
<Text css={{ mx: "$2", fontSize: "inherit" }}>Account: </Text>
<Select
instanceId="debugStreamAccount"
placeholder="Select account"
options={accountOptions}
hideSelectedOptions
value={selectedAccount}
onChange={acc => setSelectedAccount(acc as any)}
css={{ width: "30%" }}
/>
</>
);
useEffect(() => {
const account = selectedAccount?.value;
if (!account) {
return;
}
const socket = new WebSocket(`wss://hooks-testnet-debugstream.xrpl-labs.com/${account}`);
const onOpen = () => {
state.debugLogs = [];
state.debugLogs.push({
type: "success",
message: `Debug stream opened for account ${account}`,
});
};
const onError = () => {
state.debugLogs.push({
type: "error",
message: "Something went wrong in establishing connection!",
});
setSelectedAccount(null);
};
const onMessage = (event: any) => {
if (!event.data) return;
state.debugLogs.push({
type: "log",
message: event.data,
});
};
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onError);
socket.addEventListener("error", onError);
socket.addEventListener("message", onMessage);
return () => {
socket.removeEventListener("open", onOpen);
socket.removeEventListener("close", onError);
socket.removeEventListener("message", onMessage);
socket.close();
};
}, [selectedAccount]);
return (
<LogBox
enhanced
renderNav={renderNav}
title="Debug stream"
logs={snap.debugLogs}
clearLog={() => (state.debugLogs = [])}
/>
);
};
export default DebugStream;

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

@@ -1,4 +1,4 @@
import React, { useRef, useLayoutEffect } from "react";
import React, { useRef, useLayoutEffect, ReactNode } from "react";
import { Notepad, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import NextLink from "next/link";
@@ -17,9 +17,11 @@ interface ILogBox {
title: string;
clearLog?: () => void;
logs: ILog[];
renderNav?: () => ReactNode;
enhanced?: boolean;
}
const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children }) => {
const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhanced }) => {
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
@@ -39,7 +41,7 @@ const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children }) => {
}}
>
<Container css={{ px: 0, flexShrink: 1 }}>
<Flex css={{ py: "$3" }}>
<Flex css={{ py: "$3", alignItems: "center", fontSize: "$sm", fontWeight: 300 }}>
<Heading
as="h3"
css={{
@@ -56,6 +58,7 @@ const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children }) => {
>
<Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading>
{renderNav?.()}
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
{clearLog && (
<Button ghost size="xs" onClick={clearLog}>
@@ -84,10 +87,18 @@ const LogBox: React.FC<ILogBox> = ({ title, clearLog, logs, children }) => {
}}
>
{logs?.map((log, index) => (
<Box as="span" key={log.type + index}>
{/* <LogText capitalize variant={log.type}>
{log.type}:{" "}
</LogText> */}
<Box
as="span"
key={log.type + index}
css={{
"@hover": {
"&:hover": {
backgroundColor: enhanced ? "$backgroundAlt" : undefined,
},
},
p: "$2 $1",
}}
>
<LogText variant={log.type}>
{log.message}{" "}
{log.link && (

View File

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

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

@@ -4,9 +4,13 @@ import dynamic from "next/dynamic";
import { useSnapshot } from "valtio";
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"), {
ssr: false,
});
const LogBox = dynamic(() => import("../../components/LogBox"), {
ssr: false,
});
@@ -18,7 +22,11 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
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 => ({
@@ -118,10 +126,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) {
@@ -130,9 +143,10 @@ const Transaction = () => {
}
setTxIsLoading(false);
}, [
selectedAccount,
selectedDestAccount,
selectedTransaction,
header,
selectedAccount?.value,
selectedDestAccount?.value,
selectedTransaction?.value,
snap.accounts,
txFields,
txIsDisabled,
@@ -150,7 +164,7 @@ const Transaction = () => {
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)" }}>
<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" }}>
@@ -280,21 +294,30 @@ 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' }}>
<Flex
row
fluid
css={{ justifyContent: "center", mb: "$2", height: "40vh", minHeight: "300px", p: "$3 $2" }}
>
<Box css={{ width: "55%", px: "$2" }}>
<Tabs>
{/* TODO Dynamic tabs */}
<Tab header="test1.json">
<Transaction />
</Tab>
<Tab header="test2.json">
<Transaction />
</Tab>
<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%' }}>
<Box css={{ width: "45%", mx: "$2", height: "100%" }}>
<Accounts card hideDeployBtn showHookStats />
</Box>
</Flex>
@@ -302,13 +325,13 @@ const Test = () => {
<Flex row fluid css={{ borderBottom: "1px solid $mauve8" }}>
<Box css={{ width: "50%", borderRight: "1px solid $mauve8" }}>
<LogBox
title="From Log"
title="Development Log"
logs={snap.transactionLogs}
clearLog={() => (state.transactionLogs = [])}
/>
</Box>
<Box css={{ width: "50%" }}>
<LogBox title="To Log" logs={[]} clearLog={() => {}} />
<DebugStream />
</Box>
</Flex>
</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

@@ -50,6 +50,7 @@ export interface IState {
logs: ILog[];
deployLogs: ILog[];
transactionLogs: ILog[];
debugLogs: ILog[];
editorCtx?: typeof monaco.editor;
editorSettings: {
tabSize: number;
@@ -72,6 +73,7 @@ let initialState: IState = {
logs: [],
deployLogs: [],
transactionLogs: [],
debugLogs: [],
editorCtx: undefined,
gistId: undefined,
gistOwner: undefined,

View File

@@ -1,6 +1,7 @@
// stitches.config.ts
import type Stitches from '@stitches/react';
import { createStitches } from '@stitches/react';
import {
gray,
blue,
@@ -47,6 +48,7 @@ export const {
...yellow,
...purple,
background: "$gray1",
backgroundAlt: "$gray4",
text: "$gray12",
primary: "$plum",
white: "white",
@@ -304,7 +306,8 @@ export const darkTheme = createTheme('dark', {
...pinkDark,
...yellowDark,
...purpleDark,
deep: 'rgb(10, 10, 10)'
deep: 'rgb(10, 10, 10)',
// backgroundA: transparentize(0.1, grayDark.gray1),
},
});