diff --git a/client/www/components/dash/sandbox/Data.tsx b/client/www/components/dash/sandbox/Data.tsx new file mode 100644 index 000000000..cd24e7cd8 --- /dev/null +++ b/client/www/components/dash/sandbox/Data.tsx @@ -0,0 +1,31 @@ +import Json from '@uiw/react-json-view'; + +export function Data({ + data, + collapsed, +}: { + data: any; + collapsed?: boolean | number; +}) { + const isObject = typeof data === 'object' && data !== null; + + return ( +
+ {isObject ? ( + + ) : ( +
+          {JSON.stringify(data) ?? 'undefined'}
+        
+ )} +
+ ); +} diff --git a/client/www/components/dash/sandbox/OutputDetail.tsx b/client/www/components/dash/sandbox/OutputDetail.tsx new file mode 100644 index 000000000..b90bbfe1d --- /dev/null +++ b/client/www/components/dash/sandbox/OutputDetail.tsx @@ -0,0 +1,213 @@ +import { useEffect, useRef } from 'react'; +import clsx from 'clsx'; +import { ChevronLeftIcon } from '@heroicons/react/24/outline'; +import { Data } from './Data'; + +interface OutputItem { + type: 'log' | 'error' | 'query' | 'transaction' | 'eval'; + data: any; + execTimeMs?: number; +} + +export function OutputDetail({ + output, + defaultCollapsed, + onBack, +}: { + output: OutputItem; + defaultCollapsed: boolean; + onBack: () => void; +}) { + return ( +
+
+ +
+
+
+
+ {output.type}{' '} + {output.execTimeMs != null + ? ` - (${output.execTimeMs.toFixed(1)} ms)` + : ''} +
+ {output.type === 'log' && ( +
+ {output.data.map((d: any, i: number) => ( + + ))} +
+ )} + {output.type === 'error' && ( +
+
+                {output.data.message}
+              
+
+ )} + {output.type === 'query' && ( +
+
Result
+ +
Permissions Check
+
+ {/* TODO: Add virtualization here for large permission check lists */} + {output.data.response.checkResults.map((cr: any) => ( +
+
+ {Boolean(cr.check) ? ( + + Pass + + ) : ( + + Fail + + )} + {cr.entity} + {cr.id} +
+
Record
+ +
Check
+
+ + view + + + {cr.program?.['display-code'] ?? ( + none + )} + +
+ +
+ ))} +
+
+ )} + + {output.type === 'transaction' && ( +
+ {output.data.response['all-checks-ok?'] ? ( +

+ + Success + {' '} + All checks passed! +

+ ) : ( +

+ + Failed + {' '} + Some checks did not pass. +

+ )} + + {output.data.response['committed?'] ? null : ( +

+ + Dry run + {' '} + Changes were not written to the database. +

+ )} + +
Permissions Check
+ {/* TODO: Add virtualization here for large permission check lists */} + {output.data.response['check-results'].map((cr: any) => ( +
+
+ {cr['check-pass?'] ? ( + + Pass + + ) : ( + + Fail + + )} + + {cr.action} + + {cr.etype} + {cr.eid} +
+
Value
+ +
Check
+
+ + {cr.action} + + + {cr.program?.['display-code'] ?? ( + none + )} + +
+ +
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/client/www/components/dash/sandbox/OutputList.tsx b/client/www/components/dash/sandbox/OutputList.tsx new file mode 100644 index 000000000..a7ebe8603 --- /dev/null +++ b/client/www/components/dash/sandbox/OutputList.tsx @@ -0,0 +1,109 @@ +import clsx from 'clsx'; +import { ChevronLeftIcon } from '@heroicons/react/24/outline'; + +interface OutputItem { + type: 'log' | 'error' | 'query' | 'transaction' | 'eval'; + data: any; + execTimeMs?: number; +} + +export function OutputList({ + output, + onSelectOutput, +}: { + output: OutputItem[]; + onSelectOutput: (index: number) => void; +}) { + return ( +
+ {output.length === 0 ? ( +
+ No output yet. Run some code to see results. +
+ ) : ( + output.map((o, i) => + o.type === 'eval' ? ( +
+ ) : ( +
onSelectOutput(i)} + className={clsx( + 'transition-all border rounded bg-gray-50 shadow-sm hover:shadow cursor-pointer p-3', + { + 'border-sky-200 hover:border-sky-300': o.type === 'log', + 'border-red-200 hover:border-red-300': o.type === 'error', + 'border-teal-200 hover:border-teal-300': o.type === 'query', + 'border-purple-200 hover:border-purple-300': + o.type === 'transaction', + }, + )} + > +
+
+ + {o.type} + + {o.execTimeMs != null && ( + + {o.execTimeMs.toFixed(1)} ms + + )} +
+
+ {o.type === 'transaction' && ( + <> + {o.data.response['all-checks-ok?'] ? ( + + Success + + ) : ( + + Failed + + )} + {!o.data.response['committed?'] && ( + + Dry run + + )} + + )} + {o.type === 'query' && ( + + {o.data.response.checkResults.length} permission check(s) + + )} + {o.type === 'transaction' && ( + + {o.data.response['check-results'].length} permission + check(s) + + )} + +
+
+ {o.type === 'error' && ( +
+ {o.data.message} +
+ )} + {o.type === 'log' && ( +
+ {o.data.map((d: any) => JSON.stringify(d)).join(', ')} +
+ )} +
+ ), + ) + )} +
+ ); +} diff --git a/client/www/components/dash/Sandbox.tsx b/client/www/components/dash/sandbox/Sandbox.tsx similarity index 60% rename from client/www/components/dash/Sandbox.tsx rename to client/www/components/dash/sandbox/Sandbox.tsx index b93c2f744..98e6e7749 100644 --- a/client/www/components/dash/Sandbox.tsx +++ b/client/www/components/dash/sandbox/Sandbox.tsx @@ -17,6 +17,8 @@ import { ComboboxOptions, } from '@headlessui/react'; import { InstantReactWebDatabase } from '@instantdb/react'; +import { OutputList } from './OutputList'; +import { OutputDetail } from './OutputDetail'; let cachedSandboxValue = ''; @@ -31,7 +33,6 @@ export function Sandbox({ app: InstantApp; db: InstantReactWebDatabase; }) { - const consoleRef = useRef(null); const [sandboxCodeValue, setSandboxValue] = useLocalStorage( `__instant_sandbox_value:${app.id}`, cachedSandboxValue, @@ -42,9 +43,6 @@ export function Sandbox({ ); const [dangerouslyCommitTx, setDangerouslyCommitTx] = useState(false); const [appendResults, setAppendResults] = useState(false); - const [collapseQuery, setHideQuery] = useState(false); - const [collapseLog, setCollapseLog] = useState(false); - const [collapseTransaction, setCollapseTransaction] = useState(false); const [defaultCollapsed, setDefaultCollapsed] = useState(false); const [useAppPerms, setUseAppPerms] = useState(true); const [permsValue, setPermsValue] = useState(() => @@ -53,6 +51,9 @@ export function Sandbox({ const [output, setOutput] = useState([]); const [showRunning, setShowRunning] = useState(false); const [isExecuting, setIsExecuting] = useState(false); + const [selectedOutputIndex, setSelectedOutputIndex] = useState( + null, + ); function out( type: 'log' | 'error' | 'query' | 'transaction' | 'eval', @@ -82,11 +83,10 @@ export function Sandbox({ }); }, []); - useEffect(() => { - consoleRef.current?.scrollTo(0, consoleRef.current.scrollHeight); - }, [output]); - const exec = async () => { + // Reset the selected output. + setSelectedOutputIndex(null); + if (isExecuting) return; setIsExecuting(true); @@ -353,7 +353,13 @@ export function Sandbox({
Output -
@@ -370,212 +376,17 @@ export function Sandbox({ checked={defaultCollapsed} onChange={setDefaultCollapsed} /> - - -
-
- {output.map((o, i) => - o.type === 'eval' ? ( -
- ) : ( -
-
- {o.type}{' '} - {o.execTimeMs != null - ? ` - (${o.execTimeMs.toFixed(1)} ms)` - : ''} -
- {o.type === 'log' && !collapseLog && ( -
- {o.data.map((d: any, i: number) => ( - - ))} -
- )} - {o.type === 'error' && ( -
-
-                      {o.data.message}
-                    
-
- )} - {o.type === 'query' && !collapseQuery && ( -
-
Result
- -
Permissions Check
-
- {o.data.response.checkResults.map((cr: any) => ( -
-
- {Boolean(cr.check) ? ( - - Pass - - ) : ( - - Fail - - )} - {cr.entity} - {cr.id} -
-
Record
- -
Check
-
- - view - - - {cr.program?.['display-code'] ?? ( - none - )} - -
- -
- ))} -
-
- )} - - {o.type === 'transaction' && !collapseTransaction && ( -
- {o.data.response['all-checks-ok?'] ? ( -

- - Success - {' '} - All checks passed! -

- ) : ( -

- - Failed - {' '} - Some checks did not pass. -

- )} - - {o.data.response['committed?'] ? null : ( -

- - Dry run - {' '} - Changes were not written to the database. -

- )} - -
Permissions Check
- {o.data.response['check-results'].map((cr: any) => ( -
-
- {cr['check-pass?'] ? ( - - Pass - - ) : ( - - Fail - - )} - - {cr.action} - - {cr.etype} - {cr.eid} -
-
Value
- -
Check
-
- - {cr.action} - - - {cr.program?.['display-code'] ?? ( - none - )} - -
- -
- ))} -
- )} -
- ), - )} -
+ {selectedOutputIndex !== null && output[selectedOutputIndex] ? ( + setSelectedOutputIndex(null)} + /> + ) : ( + + )} ); @@ -658,36 +469,6 @@ function EmailInput({ ); } -function Data({ - data, - collapsed, -}: { - data: any; - collapsed?: boolean | number; -}) { - const isObject = typeof data === 'object' && data !== null; - - return ( -
- {isObject ? ( - - ) : ( -
-          {JSON.stringify(data) ?? 'undefined'}
-        
- )} -
- ); -} - function isJsSimpleKey(str: string) { return /^[a-zA-Z0-9_]+$/.test(str); } diff --git a/client/www/pages/_devtool/index.tsx b/client/www/pages/_devtool/index.tsx index afe6e36a9..897b9a1d6 100644 --- a/client/www/pages/_devtool/index.tsx +++ b/client/www/pages/_devtool/index.tsx @@ -5,7 +5,7 @@ import config from '@/lib/config'; import { useSchemaQuery } from '@/lib/hooks/explorer'; import { jsonFetch } from '@/lib/fetch'; import { APIResponse, signOut, useAuthToken, useTokenFetch } from '@/lib/auth'; -import { Sandbox } from '@/components/dash/Sandbox'; +import { Sandbox } from '@/components/dash/sandbox/Sandbox'; import { Explorer } from '@/components/dash/explorer/Explorer'; import { init } from '@instantdb/react'; import { useEffect, useState, useContext, useMemo } from 'react'; diff --git a/client/www/pages/dash/index.tsx b/client/www/pages/dash/index.tsx index 53431da16..9db5ea20f 100644 --- a/client/www/pages/dash/index.tsx +++ b/client/www/pages/dash/index.tsx @@ -72,7 +72,7 @@ import { import { AppAuth } from '@/components/dash/AppAuth'; import Billing from '@/components/dash/Billing'; import { QueryInspector } from '@/components/dash/explorer/QueryInspector'; -import { Sandbox } from '@/components/dash/Sandbox'; +import { Sandbox } from '@/components/dash/sandbox/Sandbox'; import PersonalAccessTokensScreen from '@/components/dash/PersonalAccessTokensScreen'; import { useForm } from '@/lib/hooks/useForm'; import useLocalStorage from '@/lib/hooks/useLocalStorage';