Add logic for scripts

This commit is contained in:
Valtteri Karesto
2022-06-15 15:15:08 +03:00
parent f1a43ef758
commit fa13f7e282
4 changed files with 419 additions and 11 deletions

View File

@@ -0,0 +1,230 @@
import {
useRef,
useLayoutEffect,
ReactNode,
FC,
useState,
useCallback,
} from "react";
import { FileJs, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import NextLink from "next/link";
import Container from "./Container";
import LogText from "./LogText";
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";
import RunScript from "./RunScript";
interface ILogBox {
title: string;
clearLog?: () => void;
logs: ILog[];
renderNav?: () => ReactNode;
enhanced?: boolean;
}
const LogBox: FC<ILogBox> = ({
title,
clearLog,
logs,
children,
renderNav,
enhanced,
}) => {
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
const snap = useSnapshot(state);
useLayoutEffect(() => {
stayScrolled();
}, [stayScrolled, logs]);
return (
<Flex
as="div"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
flex: 1,
height: "100%",
}}
>
<Container
css={{
px: 0,
height: "100%",
}}
>
<Flex
fluid
css={{
height: "48px",
alignItems: "center",
fontSize: "$sm",
fontWeight: 300,
}}
>
<Heading
as="h3"
css={{
fontWeight: 300,
m: 0,
fontSize: "11px",
color: "$mauve12",
px: "$3",
textTransform: "uppercase",
alignItems: "center",
display: "inline-flex",
gap: "$3",
mr: "$3",
}}
>
<FileJs size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading>
<Flex css={{ gap: "$3" }}>
{snap.files
.filter((f) => f.name.endsWith(".js"))
.map((file) => (
<RunScript file={file} key={file.name} />
))}
</Flex>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
{clearLog && (
<Button ghost size="xs" onClick={clearLog}>
<Prohibit size="14px" />
</Button>
)}
</Flex>
</Flex>
<Box
as="pre"
ref={logRef}
css={{
margin: 0,
// display: "inline-block",
display: "flex",
flexDirection: "column",
width: "100%",
height: "calc(100% - 48px)", // 100% minus the logbox header height
overflowY: "auto",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
px: "$3",
pb: "$2",
whiteSpace: "normal",
}}
>
{logs?.map((log, index) => (
<Box
as="span"
key={log.type + index}
css={{
"@hover": {
"&:hover": {
backgroundColor: enhanced ? "$backgroundAlt" : undefined,
},
},
p: enhanced ? "$1" : undefined,
my: enhanced ? "$1" : undefined,
}}
>
<Log {...log} />
</Box>
))}
{children}
</Box>
</Container>
</Flex>
);
};
export const Log: FC<ILog> = ({
type,
timestring,
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]
);
let message: ReactNode;
if (typeof _message === "string") {
_message = _message.trim().replace(/\n /gi, "\n");
message = enrichAccounts(_message);
} else {
message = _message;
}
const jsonData = enrichAccounts(_jsonData);
return (
<>
<AccountDialog
setActiveAccountAddress={setDialogAccount}
activeAccountAddress={dialogAccount}
/>
<LogText variant={type}>
{timestring && (
<Text muted monospace>
{timestring}{" "}
</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>
<br />
</>
);
};
export default LogBox;

View File

@@ -0,0 +1,169 @@
import Handlebars from "handlebars";
import { Play, X } from "phosphor-react";
import { useEffect, useState } from "react";
import state, { IFile, ILog } from "../../state";
import Button from "../Button";
import Box from "../Box";
import Input from "../Input";
import Stack from "../Stack";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
} from "../Dialog";
import Flex from "../Flex";
import { useSnapshot } from "valtio";
const generateHtmlTemplate = (code: string) => {
return `
<html>
<head>
<script>
function log() {
var args = Array.from(arguments);
this.parent.window.postMessage({ type: 'log', args: args || [] }, '*');
};
function error() {
var args = Array.from(arguments);
this.parent.window.postMessage({ type: 'error', args: args || [] }, '*');
};
</script>
<script type="module">
log('Started running...');
${code}
</script>
</head>
<body>
kissa
</body>
</html>
`;
};
const RunScript: React.FC<{ file: IFile }> = ({ file }) => {
const snap = useSnapshot(state);
const parsed = Handlebars.parse(file.content);
const names = parsed.body
.filter((i) => i.type === "MustacheStatement")
// @ts-expect-error
.map((block) => block?.path?.original);
const defaultState: Record<string, string> = {};
names.forEach((name) => (defaultState[name] = ""));
const [fields, setFields] = useState<Record<string, string>>(defaultState);
const [iFrameCode, setIframeCode] = useState("");
const [isDialogOpen, setIsDialogOpen] = useState(false);
const runScript = () => {
const template = Handlebars.compile(file.content);
const code = template(fields);
setIframeCode(generateHtmlTemplate(code));
};
useEffect(() => {
const handleEvent = (e: any) => {
if (e.data.type === "log" || e.data.type === "error") {
console.log(e.data);
const data: ILog[] = e.data.args.map((msg: any) => ({
type: e.data.type,
message: msg.toString(),
}));
state.scriptLogs = [...snap.scriptLogs, ...data];
}
};
window.addEventListener("message", handleEvent);
return () => window.removeEventListener("message", handleEvent);
}, [snap.scriptLogs]);
return (
<>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
variant="primary"
onClick={() => {
setIframeCode("");
}}
>
{file.name} <Play weight="bold" size="16px" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Run {file.name} script</DialogTitle>
<DialogDescription>
You are about to run scripts provided by the developer of the hook,
make sure you know what you are doing.
<br />
<br />
{Object.keys(fields).length > 0
? `You also need to fill in following parameters to run the script`
: ""}
</DialogDescription>
<Stack css={{ width: "100%" }}>
{Object.keys(fields).map((key) => (
<Box key={key} css={{ width: "100%" }}>
<label>{key}</label>
<Input
type="text"
value={fields[key]}
css={{ mt: "$1" }}
onChange={(e) =>
setFields({ ...fields, [key]: e.target.value })
}
/>
</Box>
))}
<Flex
css={{ justifyContent: "flex-end", width: "100%", gap: "$3" }}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button
variant="primary"
isDisabled={
Object.entries(fields).length > 0 &&
Object.entries(fields).every(
([key, value]: [string, string]) => !value
)
}
onClick={() => {
state.scriptLogs = [];
runScript();
setIsDialogOpen(false);
}}
>
Run script
</Button>
</Flex>
</Stack>
<DialogClose asChild>
<Box
css={{
position: "absolute",
top: "$1",
right: "$1",
cursor: "pointer",
background: "$mauve1",
display: "flex",
borderRadius: "$full",
p: "$1",
}}
>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
{iFrameCode && (
<iframe
style={{ display: "none" }}
srcDoc={iFrameCode}
sandbox="allow-scripts allow-same-origin"
/>
)}
</>
);
};
export default RunScript;

View File

@@ -6,6 +6,7 @@ import Transaction from "../../components/Transaction";
import state from "../../state"; import state from "../../state";
import { getSplit, saveSplit } from "../../state/actions/persistSplits"; import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTransaction } from "../../state"; import { transactionsState, modifyTransaction } from "../../state";
import LogBoxForScripts from "../../components/LogBoxForScripts";
const DebugStream = dynamic(() => import("../../components/DebugStream"), { const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false, ssr: false,
@@ -21,16 +22,16 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
const Test = () => { const Test = () => {
const { transactionLogs } = useSnapshot(state); const { transactionLogs } = useSnapshot(state);
const { transactions, activeHeader } = useSnapshot(transactionsState); const { transactions, activeHeader } = useSnapshot(transactionsState);
const snap = useSnapshot(state);
return ( return (
<Container css={{ px: 0 }}> <Container css={{ px: 0 }}>
<Split <Split
direction="vertical" direction="vertical"
sizes={getSplit("testVertical") || [50, 50]} sizes={getSplit("testVertical") || [50, 20, 30]}
gutterSize={4} gutterSize={4}
gutterAlign="center" gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }} style={{ height: "calc(100vh - 60px)" }}
onDragEnd={e => saveSplit("testVertical", e)} onDragEnd={(e) => saveSplit("testVertical", e)}
> >
<Flex <Flex
row row
@@ -52,7 +53,7 @@ const Test = () => {
width: "100%", width: "100%",
height: "100%", height: "100%",
}} }}
onDragEnd={e => saveSplit("testHorizontal", e)} onDragEnd={(e) => saveSplit("testHorizontal", e)}
> >
<Box css={{ width: "55%", px: "$2" }}> <Box css={{ width: "55%", px: "$2" }}>
<Tabs <Tabs
@@ -64,17 +65,14 @@ const Test = () => {
keepAllAlive keepAllAlive
forceDefaultExtension forceDefaultExtension
defaultExtension=".json" defaultExtension=".json"
onCreateNewTab={header => modifyTransaction(header, {})} onCreateNewTab={(header) => modifyTransaction(header, {})}
onCloseTab={(idx, header) => onCloseTab={(idx, header) =>
header && modifyTransaction(header, undefined) header && modifyTransaction(header, undefined)
} }
> >
{transactions.map(({ header, state }) => ( {transactions.map(({ header, state }) => (
<Tab key={header} header={header}> <Tab key={header} header={header}>
<Transaction <Transaction state={state} header={header} />
state={state}
header={header}
/>
</Tab> </Tab>
))} ))}
</Tabs> </Tabs>
@@ -84,8 +82,17 @@ const Test = () => {
</Box> </Box>
</Split> </Split>
</Flex> </Flex>
<Flex
<Flex row fluid> as="div"
css={{
borderTop: "1px solid $mauve6",
background: "$mauve1",
flexDirection: "column",
}}
>
<LogBoxForScripts title="Helper scripts" logs={snap.scriptLogs} />
</Flex>
<Flex>
<Split <Split
direction="horizontal" direction="horizontal"
sizes={[50, 50]} sizes={[50, 50]}

View File

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