Compare commits
11 Commits
feat/add-h
...
feat/debug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db0ffe999e | ||
|
|
ce91182c7b | ||
|
|
2e3a0e557e | ||
|
|
6b9a9ef978 | ||
|
|
bc5bb5be39 | ||
|
|
0fe83811b9 | ||
|
|
c6359aa853 | ||
|
|
c9c818c8f3 | ||
|
|
c521246393 | ||
|
|
8936b34361 | ||
|
|
5993d2762f |
@@ -27,7 +27,7 @@ const labelStyle = css({
|
||||
mb: "$0.5",
|
||||
});
|
||||
|
||||
const AccountDialog = ({
|
||||
export const AccountDialog = ({
|
||||
activeAccountAddress,
|
||||
setActiveAccountAddress,
|
||||
}: {
|
||||
|
||||
@@ -1,84 +1,136 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { proxy, ref, useSnapshot } from "valtio";
|
||||
import { Select } from ".";
|
||||
import state from "../state";
|
||||
import state, { ILog } from "../state";
|
||||
import { extractJSON } from "../utils/json";
|
||||
import LogBox from "./LogBox";
|
||||
import Text from "./Text";
|
||||
|
||||
interface ISelect<T = string> {
|
||||
label: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
const streamState = proxy({
|
||||
selectedAccount: null as ISelect | null,
|
||||
logs: [] as ILog[],
|
||||
socket: undefined as WebSocket | undefined,
|
||||
});
|
||||
|
||||
const DebugStream = () => {
|
||||
const snap = useSnapshot(state);
|
||||
const { selectedAccount, logs, socket } = useSnapshot(streamState);
|
||||
const { accounts } = useSnapshot(state);
|
||||
|
||||
const accountOptions = snap.accounts.map(acc => ({
|
||||
const accountOptions = 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"
|
||||
instanceId="DSAccount"
|
||||
placeholder="Select account"
|
||||
options={accountOptions}
|
||||
hideSelectedOptions
|
||||
value={selectedAccount}
|
||||
onChange={acc => setSelectedAccount(acc as any)}
|
||||
css={{ width: "30%" }}
|
||||
onChange={acc => (streamState.selectedAccount = acc as any)}
|
||||
css={{ width: "100%" }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const prepareLog = useCallback((str: any): ILog => {
|
||||
if (typeof str !== "string") throw Error("Unrecognized debug log stream!");
|
||||
|
||||
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))\ ?([\s\S]*)/m);
|
||||
const [_, tm, msg] = match || [];
|
||||
|
||||
const extracted = extractJSON(msg);
|
||||
const timestamp = isNaN(Date.parse(tm || ""))
|
||||
? tm
|
||||
: new Date(tm).toLocaleTimeString();
|
||||
|
||||
const message = !extracted
|
||||
? msg
|
||||
: msg.slice(0, extracted.start) + msg.slice(extracted.end + 1);
|
||||
|
||||
const jsonData = extracted
|
||||
? JSON.stringify(extracted.result, null, 2)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
type: "log",
|
||||
message,
|
||||
timestamp,
|
||||
jsonData,
|
||||
defaultCollapsed: true,
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const account = selectedAccount?.value;
|
||||
if (!account) {
|
||||
return;
|
||||
if (account && (!socket || !socket.url.endsWith(account))) {
|
||||
socket?.close();
|
||||
streamState.socket = ref(
|
||||
new WebSocket(
|
||||
`wss://hooks-testnet-debugstream.xrpl-labs.com/${account}`
|
||||
)
|
||||
);
|
||||
} else if (!account && socket) {
|
||||
socket.close();
|
||||
streamState.socket = undefined;
|
||||
}
|
||||
const socket = new WebSocket(`wss://hooks-testnet-debugstream.xrpl-labs.com/${account}`);
|
||||
}, [selectedAccount?.value, socket]);
|
||||
|
||||
useEffect(() => {
|
||||
const account = selectedAccount?.value;
|
||||
const socket = streamState.socket;
|
||||
if (!socket) return;
|
||||
|
||||
const onOpen = () => {
|
||||
state.debugLogs = [];
|
||||
state.debugLogs.push({
|
||||
streamState.logs = [];
|
||||
streamState.logs.push({
|
||||
type: "success",
|
||||
message: `Debug stream opened for account ${account}`,
|
||||
});
|
||||
};
|
||||
const onError = () => {
|
||||
state.debugLogs.push({
|
||||
streamState.logs.push({
|
||||
type: "error",
|
||||
message: "Something went wrong in establishing connection!",
|
||||
message: "Something went wrong! Check your connection and try again.",
|
||||
});
|
||||
setSelectedAccount(null);
|
||||
};
|
||||
const onClose = (e: CloseEvent) => {
|
||||
streamState.logs.push({
|
||||
type: "error",
|
||||
message: `Connection was closed. [code: ${e.code}]`,
|
||||
});
|
||||
streamState.selectedAccount = null;
|
||||
};
|
||||
const onMessage = (event: any) => {
|
||||
if (!event.data) return;
|
||||
state.debugLogs.push({
|
||||
type: "log",
|
||||
message: event.data,
|
||||
});
|
||||
streamState.logs.push(prepareLog(event.data));
|
||||
};
|
||||
|
||||
socket.addEventListener("open", onOpen);
|
||||
socket.addEventListener("close", onError);
|
||||
socket.addEventListener("close", onClose);
|
||||
socket.addEventListener("error", onError);
|
||||
socket.addEventListener("message", onMessage);
|
||||
|
||||
return () => {
|
||||
socket.removeEventListener("open", onOpen);
|
||||
socket.removeEventListener("close", onError);
|
||||
socket.removeEventListener("close", onClose);
|
||||
socket.removeEventListener("message", onMessage);
|
||||
|
||||
socket.close();
|
||||
socket.removeEventListener("error", onError);
|
||||
};
|
||||
}, [selectedAccount]);
|
||||
|
||||
}, [prepareLog, selectedAccount?.value, socket]);
|
||||
return (
|
||||
<LogBox
|
||||
enhanced
|
||||
renderNav={renderNav}
|
||||
title="Debug stream"
|
||||
logs={snap.debugLogs}
|
||||
clearLog={() => (state.debugLogs = [])}
|
||||
logs={logs}
|
||||
clearLog={() => (streamState.logs = [])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,14 @@ import { styled } from "../stitches.config";
|
||||
const StyledLink = styled("a", {
|
||||
color: "CurrentColor",
|
||||
textDecoration: "underline",
|
||||
cursor: 'pointer',
|
||||
variants: {
|
||||
highlighted: {
|
||||
true: {
|
||||
color: '$blue9'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default StyledLink;
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React, { useRef, useLayoutEffect, ReactNode } from "react";
|
||||
import { useRef, useLayoutEffect, ReactNode, FC, useState, useCallback } from "react";
|
||||
import { Notepad, Prohibit } from "phosphor-react";
|
||||
import useStayScrolled from "react-stay-scrolled";
|
||||
import NextLink from "next/link";
|
||||
|
||||
import Container from "./Container";
|
||||
import Box from "./Box";
|
||||
import Flex from "./Flex";
|
||||
import LogText from "./LogText";
|
||||
import { ILog } from "../state";
|
||||
import Text from "./Text";
|
||||
import Button from "./Button";
|
||||
import Heading from "./Heading";
|
||||
import Link from "./Link";
|
||||
import state, { ILog } from "../state";
|
||||
import { Pre, Link, Heading, Button, Text, Flex, Box } from ".";
|
||||
import regexifyString from "regexify-string";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { AccountDialog } from "./Accounts";
|
||||
|
||||
interface ILogBox {
|
||||
title: string;
|
||||
@@ -21,14 +19,7 @@ interface ILogBox {
|
||||
enhanced?: boolean;
|
||||
}
|
||||
|
||||
const LogBox: React.FC<ILogBox> = ({
|
||||
title,
|
||||
clearLog,
|
||||
logs,
|
||||
children,
|
||||
renderNav,
|
||||
enhanced,
|
||||
}) => {
|
||||
const LogBox: FC<ILogBox> = ({ title, clearLog, logs, children, renderNav, enhanced }) => {
|
||||
const logRef = useRef<HTMLPreElement>(null);
|
||||
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
|
||||
|
||||
@@ -55,6 +46,7 @@ const LogBox: React.FC<ILogBox> = ({
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
fluid
|
||||
css={{
|
||||
height: "48px",
|
||||
alignItems: "center",
|
||||
@@ -78,7 +70,15 @@ const LogBox: React.FC<ILogBox> = ({
|
||||
>
|
||||
<Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
|
||||
</Heading>
|
||||
{renderNav?.()}
|
||||
<Flex
|
||||
row
|
||||
align="center"
|
||||
css={{
|
||||
width: "50%", // TODO make it max without breaking layout!
|
||||
}}
|
||||
>
|
||||
{renderNav?.()}
|
||||
</Flex>
|
||||
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
|
||||
{clearLog && (
|
||||
<Button ghost size="xs" onClick={clearLog}>
|
||||
@@ -117,17 +117,11 @@ const LogBox: React.FC<ILogBox> = ({
|
||||
backgroundColor: enhanced ? "$backgroundAlt" : undefined,
|
||||
},
|
||||
},
|
||||
p: enhanced ? "$2 $1" : undefined,
|
||||
p: enhanced ? "$1" : undefined,
|
||||
my: enhanced ? "$1" : undefined,
|
||||
}}
|
||||
>
|
||||
<LogText variant={log.type}>
|
||||
{log.message}{" "}
|
||||
{log.link && (
|
||||
<NextLink href={log.link} shallow passHref>
|
||||
<Link as="a">{log.linkText}</Link>
|
||||
</NextLink>
|
||||
)}
|
||||
</LogText>
|
||||
<Log {...log} />
|
||||
</Box>
|
||||
))}
|
||||
{children}
|
||||
@@ -137,4 +131,74 @@ const LogBox: React.FC<ILogBox> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const Log: FC<ILog> = ({
|
||||
type,
|
||||
timestamp: timestamp,
|
||||
message: _message,
|
||||
link,
|
||||
linkText,
|
||||
defaultCollapsed,
|
||||
jsonData: _jsonData,
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(!defaultCollapsed);
|
||||
const { accounts } = useSnapshot(state);
|
||||
const [dialogAccount, setDialogAccount] = useState<string | null>(null);
|
||||
|
||||
const enrichAccounts = useCallback(
|
||||
(str?: string): ReactNode => {
|
||||
if (!str || !accounts.length) return null;
|
||||
|
||||
const pattern = `(${accounts.map(acc => acc.address).join("|")})`;
|
||||
const res = regexifyString({
|
||||
pattern: new RegExp(pattern, "gim"),
|
||||
decorator: (match, idx) => {
|
||||
const name = accounts.find(acc => acc.address === match)?.name;
|
||||
return (
|
||||
<Link
|
||||
key={match + idx}
|
||||
as="a"
|
||||
onClick={() => setDialogAccount(match)}
|
||||
title={match}
|
||||
highlighted
|
||||
>
|
||||
{name || match}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
input: str,
|
||||
});
|
||||
|
||||
return <>{res}</>;
|
||||
},
|
||||
[accounts]
|
||||
);
|
||||
_message = _message.trim().replace(/\n /gi, "\n");
|
||||
const message = enrichAccounts(_message);
|
||||
const jsonData = enrichAccounts(_jsonData);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountDialog
|
||||
setActiveAccountAddress={setDialogAccount}
|
||||
activeAccountAddress={dialogAccount}
|
||||
/>
|
||||
<LogText variant={type}>
|
||||
{timestamp && <Text muted monospace>{timestamp} </Text>}
|
||||
<Pre>{message} </Pre>
|
||||
{link && (
|
||||
<NextLink href={link} shallow passHref>
|
||||
<Link as="a">{linkText}</Link>
|
||||
</NextLink>
|
||||
)}
|
||||
{jsonData && (
|
||||
<Link onClick={() => setExpanded(!expanded)} as="a">
|
||||
{expanded ? "Collapse" : "Expand"}
|
||||
</Link>
|
||||
)}
|
||||
{expanded && jsonData && <Pre block>{jsonData}</Pre>}
|
||||
</LogText>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogBox;
|
||||
|
||||
27
components/Pre.tsx
Normal file
27
components/Pre.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { styled } from "../stitches.config";
|
||||
|
||||
const Pre = styled("span", {
|
||||
m: 0,
|
||||
wordBreak: "break-all",
|
||||
fontFamily: '$monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
variants: {
|
||||
fluid: {
|
||||
true: {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
line: {
|
||||
true: {
|
||||
whiteSpace: 'pre-line'
|
||||
}
|
||||
},
|
||||
block: {
|
||||
true: {
|
||||
display: 'block'
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default Pre;
|
||||
@@ -14,6 +14,11 @@ const Text = styled("span", {
|
||||
true: {
|
||||
color: '$mauve9'
|
||||
}
|
||||
},
|
||||
monospace: {
|
||||
true: {
|
||||
fontFamily: '$monospace'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from "./Tabs";
|
||||
export * from "./AlertDialog";
|
||||
export { default as Box } from "./Box";
|
||||
export { default as Button } from "./Button";
|
||||
export { default as Pre } from "./Pre";
|
||||
export { default as ButtonGroup } from "./ButtonGroup";
|
||||
export { default as DeployFooter } from "./DeployFooter";
|
||||
export * from "./Dialog";
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"react-stay-scrolled": "^7.4.0",
|
||||
"react-time-ago": "^7.1.9",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"regexify-string": "^1.0.17",
|
||||
"valtio": "^1.2.5",
|
||||
"vscode-languageserver": "^7.0.0",
|
||||
"vscode-uri": "^3.0.2",
|
||||
@@ -63,4 +64,4 @@
|
||||
"eslint-config-next": "11.1.2",
|
||||
"typescript": "4.4.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,11 @@ export interface IAccount {
|
||||
export interface ILog {
|
||||
type: "error" | "warning" | "log" | "success";
|
||||
message: string;
|
||||
jsonData?: any,
|
||||
timestamp?: string;
|
||||
link?: string;
|
||||
linkText?: string;
|
||||
defaultCollapsed?: boolean
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
@@ -51,7 +54,6 @@ export interface IState {
|
||||
logs: ILog[];
|
||||
deployLogs: ILog[];
|
||||
transactionLogs: ILog[];
|
||||
debugLogs: ILog[];
|
||||
editorCtx?: typeof monaco.editor;
|
||||
editorSettings: {
|
||||
tabSize: number;
|
||||
@@ -74,7 +76,6 @@ let initialState: IState = {
|
||||
logs: [],
|
||||
deployLogs: [],
|
||||
transactionLogs: [],
|
||||
debugLogs: [],
|
||||
editorCtx: undefined,
|
||||
gistId: undefined,
|
||||
gistOwner: undefined,
|
||||
|
||||
21
utils/json.ts
Normal file
21
utils/json.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export const extractJSON = (str?: string) => {
|
||||
if (!str) return
|
||||
let firstOpen = 0, firstClose = 0, candidate = '';
|
||||
firstOpen = str.indexOf('{', firstOpen + 1);
|
||||
do {
|
||||
firstClose = str.lastIndexOf('}');
|
||||
if (firstClose <= firstOpen) {
|
||||
return;
|
||||
}
|
||||
do {
|
||||
candidate = str.substring(firstOpen, firstClose + 1);
|
||||
try {
|
||||
let result = JSON.parse(candidate);
|
||||
return { result, start: firstOpen < 0 ? 0 : firstOpen, end: firstClose }
|
||||
}
|
||||
catch (e) { }
|
||||
firstClose = str.substring(0, firstClose).lastIndexOf('}');
|
||||
} while (firstClose > firstOpen);
|
||||
firstOpen = str.indexOf('{', firstOpen + 1);
|
||||
} while (firstOpen != -1);
|
||||
}
|
||||
@@ -3989,6 +3989,11 @@ regenerator-runtime@^0.13.4:
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
||||
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
|
||||
|
||||
regexify-string@^1.0.17:
|
||||
version "1.0.17"
|
||||
resolved "https://registry.yarnpkg.com/regexify-string/-/regexify-string-1.0.17.tgz#b9e571b51c8ec566eb82b7121744dae0d8e829de"
|
||||
integrity sha512-mmD0AUNaY/piGW2AyACWdQOjIAwNuWz+KIvxfBZPDdCBAexiROeQxdxTaYAWcIxwtUAOwojdTta6CMMil84jXw==
|
||||
|
||||
regexp.prototype.flags@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26"
|
||||
|
||||
Reference in New Issue
Block a user