320 lines
9.7 KiB
TypeScript
320 lines
9.7 KiB
TypeScript
import { Play, X } from 'phosphor-react'
|
|
import { HTMLInputTypeAttribute, useCallback, useEffect, useState } from 'react'
|
|
import state, { IAccount, IFile, ILog } from '../../state'
|
|
import Button from '../Button'
|
|
import Box from '../Box'
|
|
import Input, { Label } from '../Input'
|
|
import Stack from '../Stack'
|
|
import {
|
|
Dialog,
|
|
DialogTrigger,
|
|
DialogContent,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogClose
|
|
} from '../Dialog'
|
|
import Flex from '../Flex'
|
|
import { useSnapshot } from 'valtio'
|
|
import Select from '../Select'
|
|
import Text from '../Text'
|
|
import { saveFile } from '../../state/actions/saveFile'
|
|
import { getErrors, getTags } from '../../utils/comment-parser'
|
|
import toast from 'react-hot-toast'
|
|
|
|
const generateHtmlTemplate = async (code: string, data?: Record<string, any>) => {
|
|
let processString: string | undefined
|
|
const process = { env: { NODE_ENV: 'production' } } as any
|
|
if (data) {
|
|
Object.keys(data).forEach(key => {
|
|
process.env[key] = data[key]
|
|
})
|
|
}
|
|
|
|
processString = JSON.stringify(process)
|
|
|
|
const libs = (await import("xrpl-accountlib/dist/browser.hook-bundle.js")).default;
|
|
return `
|
|
<html>
|
|
<head>
|
|
<script>
|
|
var log = console.log;
|
|
var errorLog = console.error;
|
|
var infoLog = console.info;
|
|
var warnLog = console.warn
|
|
console.log = function(){
|
|
var args = Array.from(arguments);
|
|
parent.window.postMessage({ type: 'log', args: args || [] }, '*');
|
|
log.apply(console, args);
|
|
}
|
|
console.error = function(){
|
|
var args = Array.from(arguments);
|
|
parent.window.postMessage({ type: 'error', args: args || [] }, '*');
|
|
errorLog.apply(console, args);
|
|
}
|
|
console.info = function(){
|
|
var args = Array.from(arguments);
|
|
parent.window.postMessage({ type: 'info', args: args || [] }, '*');
|
|
infoLog.apply(console, args);
|
|
}
|
|
console.warn = function(){
|
|
var args = Array.from(arguments);
|
|
parent.window.postMessage({ type: 'warning', args: args || [] }, '*');
|
|
warnLog.apply(console, args);
|
|
}
|
|
|
|
|
|
var process = '${processString || '{}'}';
|
|
process = JSON.parse(process);
|
|
window.process = process
|
|
|
|
function windowErrorHandler(event) {
|
|
event.preventDefault() // to prevent automatically logging to console
|
|
console.error(event.error?.toString())
|
|
}
|
|
|
|
window.addEventListener('error', windowErrorHandler);
|
|
</script>
|
|
<script>
|
|
${libs}
|
|
</script>
|
|
<script type="module">
|
|
${code}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
</body>
|
|
</html>
|
|
`
|
|
}
|
|
|
|
type Fields = Record<
|
|
string,
|
|
{
|
|
name: string
|
|
value: string
|
|
type?: 'Account' | `Account.${keyof IAccount}` | HTMLInputTypeAttribute
|
|
description?: string
|
|
required?: boolean
|
|
}
|
|
>
|
|
|
|
const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
|
|
const snap = useSnapshot(state)
|
|
const [templateError, setTemplateError] = useState('')
|
|
const [fields, setFields] = useState<Fields>({})
|
|
const [iFrameCode, setIframeCode] = useState('')
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
|
|
const getFields = useCallback(() => {
|
|
const inputTags = ['input', 'param', 'arg', 'argument']
|
|
const tags = getTags(content)
|
|
.filter(tag => inputTags.includes(tag.tag))
|
|
.filter(tag => !!tag.name)
|
|
|
|
let _fields = tags.map(tag => ({
|
|
name: tag.name,
|
|
value: tag.default || '',
|
|
type: tag.type,
|
|
description: tag.description,
|
|
required: !tag.optional
|
|
}))
|
|
|
|
const fields: Fields = _fields.reduce((acc, field) => {
|
|
acc[field.name] = field
|
|
return acc
|
|
}, {} as Fields)
|
|
|
|
const error = getErrors(content)
|
|
if (error) setTemplateError(error.message)
|
|
else setTemplateError('')
|
|
|
|
return fields
|
|
}, [content])
|
|
|
|
const runScript = useCallback(async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
let data: any = {}
|
|
Object.keys(fields).forEach(key => {
|
|
data[key] = fields[key].value
|
|
})
|
|
const template = await generateHtmlTemplate(content, data)
|
|
|
|
setIframeCode(template)
|
|
|
|
state.scriptLogs = [{ type: 'success', message: 'Started running...' }]
|
|
} catch (err) {
|
|
state.scriptLogs = [
|
|
...snap.scriptLogs,
|
|
// @ts-expect-error
|
|
{ type: 'error', message: err?.message || 'Could not parse template' }
|
|
]
|
|
}
|
|
setIsLoading(false);
|
|
}, [content, fields, snap.scriptLogs])
|
|
|
|
useEffect(() => {
|
|
const handleEvent = (e: any) => {
|
|
if (e.data.type === 'log' || e.data.type === 'error') {
|
|
const data: ILog[] = e.data.args.map((msg: any) => ({
|
|
type: e.data.type,
|
|
message: typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2)
|
|
}))
|
|
state.scriptLogs = [...snap.scriptLogs, ...data]
|
|
}
|
|
}
|
|
window.addEventListener('message', handleEvent)
|
|
return () => window.removeEventListener('message', handleEvent)
|
|
}, [snap.scriptLogs])
|
|
|
|
useEffect(() => {
|
|
const defaultFields = getFields() || {}
|
|
setFields(defaultFields)
|
|
}, [content, setFields, getFields])
|
|
|
|
const accOptions = snap.accounts?.map(acc => ({
|
|
...acc,
|
|
label: acc.name,
|
|
value: acc.address
|
|
}))
|
|
|
|
const isDisabled = Object.values(fields).some(field => field.required && !field.value)
|
|
|
|
const handleRun = useCallback(() => {
|
|
if (isDisabled) return toast.error('Please fill in all the required fields.')
|
|
|
|
state.scriptLogs = []
|
|
runScript()
|
|
setIsDialogOpen(false)
|
|
}, [isDisabled, runScript])
|
|
|
|
return (
|
|
<>
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => {
|
|
saveFile(false)
|
|
setIframeCode('')
|
|
}}
|
|
>
|
|
<Play weight="bold" size="16px" /> {name}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogTitle>Run {name} script</DialogTitle>
|
|
<DialogDescription>
|
|
<Box>
|
|
You are about to run scripts provided by the developer of the hook, make sure you
|
|
trust the author before you continue.
|
|
</Box>
|
|
{templateError && (
|
|
<Box
|
|
as="span"
|
|
css={{
|
|
display: 'block',
|
|
color: '$error',
|
|
mt: '$3',
|
|
whiteSpace: 'pre'
|
|
}}
|
|
>
|
|
{templateError}
|
|
</Box>
|
|
)}
|
|
{Object.keys(fields).length > 0 && (
|
|
<Box css={{ mt: '$4', mb: 0 }}>
|
|
Fill in the following parameters to run the script.
|
|
</Box>
|
|
)}
|
|
</DialogDescription>
|
|
|
|
<Stack css={{ width: '100%' }}>
|
|
{Object.keys(fields).map(key => {
|
|
const { name, value, type, description, required } = fields[key]
|
|
|
|
const isAccount = type?.startsWith('Account')
|
|
const isAccountSecret = type === 'Account.secret'
|
|
|
|
const accountField = (isAccount && type?.split('.')[1]) || 'address'
|
|
|
|
return (
|
|
<Box key={name} css={{ width: '100%' }}>
|
|
<Label css={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<span>
|
|
{description || name} {required && <Text error>*</Text>}
|
|
</span>
|
|
{isAccountSecret && (
|
|
<Text error small css={{ alignSelf: 'end' }}>
|
|
can access account secret key
|
|
</Text>
|
|
)}
|
|
</Label>
|
|
{isAccount ? (
|
|
<Select
|
|
css={{ mt: '$1' }}
|
|
options={accOptions}
|
|
onChange={(val: any) => {
|
|
setFields({
|
|
...fields,
|
|
[key]: {
|
|
...fields[key],
|
|
value: val[accountField]
|
|
}
|
|
})
|
|
}}
|
|
value={accOptions.find((acc: any) => acc[accountField] === value)}
|
|
/>
|
|
) : (
|
|
<Input
|
|
type={type || 'text'}
|
|
value={value}
|
|
css={{ mt: '$1' }}
|
|
onChange={e => {
|
|
setFields({
|
|
...fields,
|
|
[key]: { ...fields[key], value: e.target.value }
|
|
})
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
)
|
|
})}
|
|
<Flex css={{ justifyContent: 'flex-end', width: '100%', gap: '$3' }}>
|
|
<DialogClose asChild>
|
|
<Button outline>Cancel</Button>
|
|
</DialogClose>
|
|
<Button variant="primary" isDisabled={isDisabled || isLoading} isLoading={isLoading} onClick={handleRun}>
|
|
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" />
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default RunScript
|