From 5643fa5f8d361efef0c3b34332d94b7e9e02d5e4 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 17:30:36 +0000 Subject: [PATCH] refactor: modularize lua editor --- .../src/components/editors/lua/LuaEditor.tsx | 682 +----------------- .../editors/lua/lua-editor/LuaEditor.tsx | 111 +++ .../lua-editor/code/LuaCodeEditorSection.tsx | 148 ++++ .../lua/lua-editor/code/useLuaMonacoConfig.ts | 97 +++ .../configuration/LuaScriptDetails.tsx | 125 ++++ .../configuration/LuaScriptsListCard.tsx | 69 ++ .../execution/LuaExecutionPreview.tsx | 68 ++ .../lua-editor/linting/LuaLintingControls.tsx | 30 + .../lua-editor/toolbar/LuaEditorToolbar.tsx | 36 + .../lua/lua-editor/useLuaEditorLogic.ts | 144 ++++ 10 files changed, 829 insertions(+), 681 deletions(-) create mode 100644 frontends/nextjs/src/components/editors/lua/lua-editor/LuaEditor.tsx create mode 100644 frontends/nextjs/src/components/editors/lua/lua-editor/code/LuaCodeEditorSection.tsx create mode 100644 frontends/nextjs/src/components/editors/lua/lua-editor/code/useLuaMonacoConfig.ts create mode 100644 frontends/nextjs/src/components/editors/lua/lua-editor/configuration/LuaScriptDetails.tsx create mode 100644 frontends/nextjs/src/components/editors/lua/lua-editor/configuration/LuaScriptsListCard.tsx create mode 100644 frontends/nextjs/src/components/editors/lua/lua-editor/execution/LuaExecutionPreview.tsx create mode 100644 frontends/nextjs/src/components/editors/lua/lua-editor/linting/LuaLintingControls.tsx create mode 100644 frontends/nextjs/src/components/editors/lua/lua-editor/toolbar/LuaEditorToolbar.tsx create mode 100644 frontends/nextjs/src/components/editors/lua/lua-editor/useLuaEditorLogic.ts diff --git a/frontends/nextjs/src/components/editors/lua/LuaEditor.tsx b/frontends/nextjs/src/components/editors/lua/LuaEditor.tsx index 90909ce67..4dc187a43 100644 --- a/frontends/nextjs/src/components/editors/lua/LuaEditor.tsx +++ b/frontends/nextjs/src/components/editors/lua/LuaEditor.tsx @@ -1,681 +1 @@ -import { useState, useEffect, useRef } from 'react' -import { Button } from '@/components/ui' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui' -import { Input } from '@/components/ui' -import { Label } from '@/components/ui' -import { Badge } from '@/components/ui' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui' -import { Plus, Trash, Play, CheckCircle, XCircle, FileCode, ArrowsOut, BookOpen, ShieldCheck } from '@phosphor-icons/react' -import { toast } from 'sonner' -import { executeLuaScriptWithProfile } from '@/lib/lua/execute-lua-script-with-profile' -import type { LuaExecutionResult } from '@/lib/lua-engine' -import { getLuaExampleCode, getLuaExamplesList } from '@/lib/lua-examples' -import type { LuaScript } from '@/lib/level-types' -import Editor from '@monaco-editor/react' -import { useMonaco } from '@monaco-editor/react' -import { LuaSnippetLibrary } from '@/components/editors/lua/LuaSnippetLibrary' -import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui' -import { securityScanner, type SecurityScanResult } from '@/lib/security-scanner' -import { SecurityWarningDialog } from '@/components/organisms/security/SecurityWarningDialog' - -interface LuaEditorProps { - scripts: LuaScript[] - onScriptsChange: (scripts: LuaScript[]) => void -} - -export function LuaEditor({ scripts, onScriptsChange }: LuaEditorProps) { - const [selectedScript, setSelectedScript] = useState( - scripts.length > 0 ? scripts[0].id : null - ) - const [testOutput, setTestOutput] = useState(null) - const [testInputs, setTestInputs] = useState>({}) - const [isExecuting, setIsExecuting] = useState(false) - const [isFullscreen, setIsFullscreen] = useState(false) - const [showSnippetLibrary, setShowSnippetLibrary] = useState(false) - const [securityScanResult, setSecurityScanResult] = useState(null) - const [showSecurityDialog, setShowSecurityDialog] = useState(false) - const editorRef = useRef(null) - const monaco = useMonaco() - - const currentScript = scripts.find(s => s.id === selectedScript) - - useEffect(() => { - if (monaco) { - monaco.languages.registerCompletionItemProvider('lua', { - provideCompletionItems: (model, position) => { - const word = model.getWordUntilPosition(position) - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn - } - - const suggestions: any[] = [ - { - label: 'context.data', - kind: monaco.languages.CompletionItemKind.Property, - insertText: 'context.data', - documentation: 'Access input parameters passed to the script', - range - }, - { - label: 'context.user', - kind: monaco.languages.CompletionItemKind.Property, - insertText: 'context.user', - documentation: 'Current user information (username, role, etc.)', - range - }, - { - label: 'context.kv', - kind: monaco.languages.CompletionItemKind.Property, - insertText: 'context.kv', - documentation: 'Key-value storage interface', - range - }, - { - label: 'context.log', - kind: monaco.languages.CompletionItemKind.Function, - insertText: 'context.log(${1:message})', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Log a message to the output console', - range - }, - { - label: 'log', - kind: monaco.languages.CompletionItemKind.Function, - insertText: 'log(${1:message})', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Log a message (shortcut for context.log)', - range - }, - { - label: 'print', - kind: monaco.languages.CompletionItemKind.Function, - insertText: 'print(${1:message})', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Print a message to output', - range - }, - { - label: 'return', - kind: monaco.languages.CompletionItemKind.Keyword, - insertText: 'return ${1:result}', - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'Return a value from the script', - range - }, - ] - - return { suggestions } - } - }) - - monaco.languages.setLanguageConfiguration('lua', { - comments: { - lineComment: '--', - blockComment: ['--[[', ']]'] - }, - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - autoClosingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '"', close: '"' }, - { open: "'", close: "'" } - ] - }) - } - }, [monaco]) - - useEffect(() => { - if (currentScript) { - const inputs: Record = {} - currentScript.parameters.forEach((param) => { - inputs[param.name] = param.type === 'number' ? 0 : param.type === 'boolean' ? false : '' - }) - setTestInputs(inputs) - } - }, [selectedScript, currentScript?.parameters.length]) - - const handleAddScript = () => { - const newScript: LuaScript = { - id: `lua_${Date.now()}`, - name: 'New Script', - code: '-- Lua script example\n-- Access input parameters via context.data\n-- Use log() or print() to output messages\n\nlog("Script started")\n\nif context.data then\n log("Received data:", context.data)\nend\n\nlocal result = {\n success = true,\n message = "Script executed successfully"\n}\n\nreturn result', - parameters: [], - } - onScriptsChange([...scripts, newScript]) - setSelectedScript(newScript.id) - toast.success('Script created') - } - - const handleDeleteScript = (scriptId: string) => { - onScriptsChange(scripts.filter(s => s.id !== scriptId)) - if (selectedScript === scriptId) { - setSelectedScript(scripts.length > 1 ? scripts[0].id : null) - } - toast.success('Script deleted') - } - - const handleUpdateScript = (updates: Partial) => { - if (!currentScript) return - - onScriptsChange( - scripts.map(s => s.id === selectedScript ? { ...s, ...updates } : s) - ) - } - - const handleTestScript = async () => { - if (!currentScript) return - - const scanResult = securityScanner.scanLua(currentScript.code) - setSecurityScanResult(scanResult) - - if (scanResult.severity === 'critical' || scanResult.severity === 'high') { - setShowSecurityDialog(true) - toast.warning('Security issues detected in script') - return - } - - if (scanResult.severity === 'medium' && scanResult.issues.length > 0) { - toast.warning(`${scanResult.issues.length} security warning(s) detected`) - } - - setIsExecuting(true) - setTestOutput(null) - - try { - const contextData: any = {} - currentScript.parameters.forEach((param) => { - contextData[param.name] = testInputs[param.name] - }) - - const result = await executeLuaScriptWithProfile(currentScript.code, { - data: contextData, - user: { username: 'test_user', role: 'god' }, - log: (...args: any[]) => console.log('[Lua]', ...args) - }, currentScript) - - setTestOutput(result) - - if (result.success) { - toast.success('Script executed successfully') - } else { - toast.error('Script execution failed') - } - - } catch (error) { - toast.error('Execution error: ' + (error instanceof Error ? error.message : String(error))) - setTestOutput({ - success: false, - error: error instanceof Error ? error.message : String(error), - logs: [] - }) - } finally { - setIsExecuting(false) - } - } - - const handleScanCode = () => { - if (!currentScript) return - - const scanResult = securityScanner.scanLua(currentScript.code) - setSecurityScanResult(scanResult) - setShowSecurityDialog(true) - - if (scanResult.safe) { - toast.success('No security issues detected') - } else { - toast.warning(`${scanResult.issues.length} security issue(s) detected`) - } - } - - const handleProceedWithExecution = () => { - setShowSecurityDialog(false) - if (!currentScript) return - - setIsExecuting(true) - setTestOutput(null) - - setTimeout(async () => { - try { - const contextData: any = {} - currentScript.parameters.forEach((param) => { - contextData[param.name] = testInputs[param.name] - }) - - const result = await executeLuaScriptWithProfile(currentScript.code, { - data: contextData, - user: { username: 'test_user', role: 'god' }, - log: (...args: any[]) => console.log('[Lua]', ...args) - }, currentScript) - - setTestOutput(result) - - if (result.success) { - toast.success('Script executed successfully') - } else { - toast.error('Script execution failed') - } - - } catch (error) { - toast.error('Execution error: ' + (error instanceof Error ? error.message : String(error))) - setTestOutput({ - success: false, - error: error instanceof Error ? error.message : String(error), - logs: [] - }) - } finally { - setIsExecuting(false) - } - }, 100) - } - - const handleAddParameter = () => { - if (!currentScript) return - - const newParam = { name: `param${currentScript.parameters.length + 1}`, type: 'string' } - handleUpdateScript({ - parameters: [...currentScript.parameters, newParam], - }) - } - - const handleDeleteParameter = (index: number) => { - if (!currentScript) return - - handleUpdateScript({ - parameters: currentScript.parameters.filter((_, i) => i !== index), - }) - } - - const handleUpdateParameter = (index: number, updates: { name?: string; type?: string }) => { - if (!currentScript) return - - handleUpdateScript({ - parameters: currentScript.parameters.map((p, i) => - i === index ? { ...p, ...updates } : p - ), - }) - } - - const handleInsertSnippet = (code: string) => { - if (!currentScript) return - - if (editorRef.current) { - const selection = editorRef.current.getSelection() - if (selection) { - editorRef.current.executeEdits('', [{ - range: selection, - text: code, - forceMoveMarkers: true - }]) - editorRef.current.focus() - } else { - const currentCode = currentScript.code - const newCode = currentCode ? currentCode + '\n\n' + code : code - handleUpdateScript({ code: newCode }) - } - } else { - const currentCode = currentScript.code - const newCode = currentCode ? currentCode + '\n\n' + code : code - handleUpdateScript({ code: newCode }) - } - - setShowSnippetLibrary(false) - } - - return ( -
- - -
- Lua Scripts - -
- Custom logic scripts -
- -
- {scripts.length === 0 ? ( -

- No scripts yet. Create one to start. -

- ) : ( - scripts.map((script) => ( -
setSelectedScript(script.id)} - > -
-
{script.name}
-
- {script.parameters.length} params -
-
- -
- )) - )} -
-
-
- - - {!currentScript ? ( - -
-

Select or create a script to edit

-
-
- ) : ( - <> - -
-
- Edit Script: {currentScript.name} - Write custom Lua logic -
-
- - -
-
-
- -
-
- - handleUpdateScript({ name: e.target.value })} - placeholder="validate_user" - className="font-mono" - /> -
-
- - handleUpdateScript({ returnType: e.target.value })} - placeholder="table, boolean, string..." - /> -
-
- -
- - handleUpdateScript({ description: e.target.value })} - placeholder="What this script does..." - /> -
- -
-
- - -
-
- {currentScript.parameters.length === 0 ? ( -

- No parameters defined -

- ) : ( - currentScript.parameters.map((param, index) => ( -
- handleUpdateParameter(index, { name: e.target.value })} - placeholder="paramName" - className="flex-1 font-mono text-sm" - /> - handleUpdateParameter(index, { type: e.target.value })} - placeholder="string" - className="w-32 text-sm" - /> - -
- )) - )} -
-
- - {currentScript.parameters.length > 0 && ( -
- -
- {currentScript.parameters.map((param) => ( -
- - { - const value = param.type === 'number' - ? parseFloat(e.target.value) || 0 - : param.type === 'boolean' - ? e.target.value === 'true' - : e.target.value - setTestInputs({ ...testInputs, [param.name]: value }) - }} - placeholder={`Enter ${param.type} value`} - className="flex-1 text-sm" - type={param.type === 'number' ? 'number' : 'text'} - /> - - {param.type} - -
- ))} -
-
- )} - -
-
- -
- - - - - - - Lua Snippet Library - - Browse and insert pre-built code templates - - -
- -
-
-
- - -
-
-
- handleUpdateScript({ code: value || '' })} - onMount={(editor) => { - editorRef.current = editor - }} - theme="vs-dark" - options={{ - minimap: { enabled: isFullscreen }, - fontSize: 14, - fontFamily: 'JetBrains Mono, monospace', - lineNumbers: 'on', - roundedSelection: true, - scrollBeyondLastLine: false, - automaticLayout: true, - tabSize: 2, - wordWrap: 'on', - quickSuggestions: true, - suggestOnTriggerCharacters: true, - acceptSuggestionOnEnter: 'on', - snippetSuggestions: 'inline', - parameterHints: { enabled: true }, - formatOnPaste: true, - formatOnType: true, - }} - /> -
-

- Write Lua code. Access parameters via context.data. Use log() or print() for output. Press Ctrl+Space for autocomplete. -

-
- - {testOutput && ( - - -
- {testOutput.success ? ( - - ) : ( - - )} - - {testOutput.success ? 'Execution Successful' : 'Execution Failed'} - -
-
- - {testOutput.error && ( -
- -
-                          {testOutput.error}
-                        
-
- )} - - {testOutput.logs.length > 0 && ( -
- -
-                          {testOutput.logs.join('\n')}
-                        
-
- )} - - {testOutput.result !== null && testOutput.result !== undefined && ( -
- -
-                          {JSON.stringify(testOutput.result, null, 2)}
-                        
-
- )} -
-
- )} - -
-
-

Available in context:

-
    -
  • context.data - Input data
  • -
  • context.user - Current user info
  • -
  • context.kv - Key-value storage
  • -
  • context.log(msg) - Logging function
  • -
-
-
-
- - )} -
- - {securityScanResult && ( - setShowSecurityDialog(false)} - codeType="Lua script" - showProceedButton={true} - /> - )} -
- ) -} +export { LuaEditor } from './lua-editor/LuaEditor' diff --git a/frontends/nextjs/src/components/editors/lua/lua-editor/LuaEditor.tsx b/frontends/nextjs/src/components/editors/lua/lua-editor/LuaEditor.tsx new file mode 100644 index 000000000..ce9dfb0b6 --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/lua-editor/LuaEditor.tsx @@ -0,0 +1,111 @@ +import { Card, CardContent } from '@/components/ui' +import { LuaCodeEditorSection } from './code/LuaCodeEditorSection' +import { LuaScriptDetails } from './configuration/LuaScriptDetails' +import { LuaScriptsListCard } from './configuration/LuaScriptsListCard' +import { LuaExecutionPreview } from './execution/LuaExecutionPreview' +import { LuaLintingControls } from './linting/LuaLintingControls' +import { LuaEditorToolbar } from './toolbar/LuaEditorToolbar' +import { useLuaEditorLogic } from './useLuaEditorLogic' +import type { LuaScript } from '@/lib/level-types' + +interface LuaEditorProps { + scripts: LuaScript[] + onScriptsChange: (scripts: LuaScript[]) => void +} + +export const LuaEditor = ({ scripts, onScriptsChange }: LuaEditorProps) => { + const { + currentScript, + selectedScriptId, + testOutput, + testInputs, + isExecuting, + isFullscreen, + showSnippetLibrary, + securityScanResult, + showSecurityDialog, + setSelectedScriptId, + setIsFullscreen, + setShowSnippetLibrary, + setShowSecurityDialog, + handleAddScript, + handleDeleteScript, + handleUpdateScript, + handleAddParameter, + handleDeleteParameter, + handleUpdateParameter, + handleTestInputChange, + handleScanCode, + handleTestScript, + handleProceedWithExecution, + } = useLuaEditorLogic({ scripts, onScriptsChange }) + + if (!currentScript) { + return ( +
+ + + +
+

Select or create a script to edit

+
+
+
+
+ ) + } + + return ( +
+ + + + + + + setIsFullscreen(!isFullscreen)} + showSnippetLibrary={showSnippetLibrary} + onShowSnippetLibraryChange={setShowSnippetLibrary} + onUpdateScript={handleUpdateScript} + /> + + + + + +
+ ) +} diff --git a/frontends/nextjs/src/components/editors/lua/lua-editor/code/LuaCodeEditorSection.tsx b/frontends/nextjs/src/components/editors/lua/lua-editor/code/LuaCodeEditorSection.tsx new file mode 100644 index 000000000..8fe49f235 --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/lua-editor/code/LuaCodeEditorSection.tsx @@ -0,0 +1,148 @@ +import { useRef } from 'react' +import Editor, { useMonaco } from '@monaco-editor/react' +import { ArrowsOut, BookOpen, FileCode } from '@phosphor-icons/react' +import { toast } from 'sonner' +import { LuaSnippetLibrary } from '@/components/editors/lua/LuaSnippetLibrary' +import { getLuaExampleCode, getLuaExamplesList } from '@/lib/lua-examples' +import { Button } from '@/components/ui' +import { Label } from '@/components/ui' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui' +import type { LuaScript } from '@/lib/level-types' +import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui' +import { useLuaMonacoConfig } from './useLuaMonacoConfig' + +interface LuaCodeEditorSectionProps { + script: LuaScript + isFullscreen: boolean + onToggleFullscreen: () => void + showSnippetLibrary: boolean + onShowSnippetLibraryChange: (open: boolean) => void + onUpdateScript: (updates: Partial) => void +} + +export const LuaCodeEditorSection = ({ + script, + isFullscreen, + onToggleFullscreen, + showSnippetLibrary, + onShowSnippetLibraryChange, + onUpdateScript, +}: LuaCodeEditorSectionProps) => { + const editorRef = useRef(null) + const monaco = useMonaco() + + useLuaMonacoConfig(monaco) + + const handleInsertSnippet = (code: string) => { + if (editorRef.current) { + const selection = editorRef.current.getSelection() + if (selection) { + editorRef.current.executeEdits('', [{ + range: selection, + text: code, + forceMoveMarkers: true + }]) + editorRef.current.focus() + } + } + + if (!editorRef.current) { + const currentCode = script.code + const newCode = currentCode ? `${currentCode}\n\n${code}` : code + onUpdateScript({ code: newCode }) + } + + onShowSnippetLibraryChange(false) + } + + const handleExampleLoad = (value: string) => { + const exampleCode = getLuaExampleCode(value as any) + onUpdateScript({ code: exampleCode }) + toast.success('Example loaded') + } + + return ( +
+
+ +
+ + + + + + + Lua Snippet Library + + Browse and insert pre-built code templates + + +
+ +
+
+
+ + +
+
+
+ onUpdateScript({ code: value || '' })} + onMount={(editor) => { + editorRef.current = editor + }} + theme="vs-dark" + options={{ + minimap: { enabled: isFullscreen }, + fontSize: 14, + fontFamily: 'JetBrains Mono, monospace', + lineNumbers: 'on', + roundedSelection: true, + scrollBeyondLastLine: false, + automaticLayout: true, + tabSize: 2, + wordWrap: 'on', + quickSuggestions: true, + suggestOnTriggerCharacters: true, + acceptSuggestionOnEnter: 'on', + snippetSuggestions: 'inline', + parameterHints: { enabled: true }, + formatOnPaste: true, + formatOnType: true, + }} + /> +
+

+ Write Lua code. Access parameters via context.data. Use log() or print() for output. Press Ctrl+Space for autocomplete. +

+
+ ) +} diff --git a/frontends/nextjs/src/components/editors/lua/lua-editor/code/useLuaMonacoConfig.ts b/frontends/nextjs/src/components/editors/lua/lua-editor/code/useLuaMonacoConfig.ts new file mode 100644 index 000000000..41b7b0935 --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/lua-editor/code/useLuaMonacoConfig.ts @@ -0,0 +1,97 @@ +import { useEffect } from 'react' +import type { Monaco } from '@monaco-editor/react' + +export const useLuaMonacoConfig = (monaco: Monaco | null) => { + useEffect(() => { + if (!monaco) return + + monaco.languages.registerCompletionItemProvider('lua', { + provideCompletionItems: (model, position) => { + const word = model.getWordUntilPosition(position) + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn + } + + const suggestions: any[] = [ + { + label: 'context.data', + kind: monaco.languages.CompletionItemKind.Property, + insertText: 'context.data', + documentation: 'Access input parameters passed to the script', + range + }, + { + label: 'context.user', + kind: monaco.languages.CompletionItemKind.Property, + insertText: 'context.user', + documentation: 'Current user information (username, role, etc.)', + range + }, + { + label: 'context.kv', + kind: monaco.languages.CompletionItemKind.Property, + insertText: 'context.kv', + documentation: 'Key-value storage interface', + range + }, + { + label: 'context.log', + kind: monaco.languages.CompletionItemKind.Function, + insertText: 'context.log(${1:message})', + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + documentation: 'Log a message to the output console', + range + }, + { + label: 'log', + kind: monaco.languages.CompletionItemKind.Function, + insertText: 'log(${1:message})', + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + documentation: 'Log a message (shortcut for context.log)', + range + }, + { + label: 'print', + kind: monaco.languages.CompletionItemKind.Function, + insertText: 'print(${1:message})', + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + documentation: 'Print a message to output', + range + }, + { + label: 'return', + kind: monaco.languages.CompletionItemKind.Keyword, + insertText: 'return ${1:result}', + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + documentation: 'Return a value from the script', + range + }, + ] + + return { suggestions } + } + }) + + monaco.languages.setLanguageConfiguration('lua', { + comments: { + lineComment: '--', + blockComment: ['--[[', ']]'] + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" } + ] + }) + }, [monaco]) +} diff --git a/frontends/nextjs/src/components/editors/lua/lua-editor/configuration/LuaScriptDetails.tsx b/frontends/nextjs/src/components/editors/lua/lua-editor/configuration/LuaScriptDetails.tsx new file mode 100644 index 000000000..33f882784 --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/lua-editor/configuration/LuaScriptDetails.tsx @@ -0,0 +1,125 @@ +import { Plus, Trash } from '@phosphor-icons/react' +import { Badge, Button, CardContent, Input, Label } from '@/components/ui' +import type { LuaScript } from '@/lib/level-types' + +interface LuaScriptDetailsProps { + script: LuaScript + testInputs: Record + onUpdateScript: (updates: Partial) => void + onAddParameter: () => void + onDeleteParameter: (index: number) => void + onUpdateParameter: (index: number, updates: { name?: string; type?: string }) => void + onTestInputChange: (paramName: string, value: any) => void +} + +export const LuaScriptDetails = ({ + script, + testInputs, + onUpdateScript, + onAddParameter, + onDeleteParameter, + onUpdateParameter, + onTestInputChange, +}: LuaScriptDetailsProps) => ( + +
+
+ + onUpdateScript({ name: e.target.value })} + placeholder="validate_user" + className="font-mono" + /> +
+
+ + onUpdateScript({ returnType: e.target.value })} + placeholder="table, boolean, string..." + /> +
+
+ +
+ + onUpdateScript({ description: e.target.value })} + placeholder="What this script does..." + /> +
+ +
+
+ + +
+
+ {script.parameters.length === 0 ? ( +

+ No parameters defined +

+ ) : ( + script.parameters.map((param, index) => ( +
+ onUpdateParameter(index, { name: e.target.value })} + placeholder="paramName" + className="flex-1 font-mono text-sm" + /> + onUpdateParameter(index, { type: e.target.value })} + placeholder="string" + className="w-32 text-sm" + /> + +
+ )) + )} +
+
+ + {script.parameters.length > 0 && ( +
+ +
+ {script.parameters.map((param) => ( +
+ + { + const value = param.type === 'number' + ? parseFloat(e.target.value) || 0 + : param.type === 'boolean' + ? e.target.value === 'true' + : e.target.value + onTestInputChange(param.name, value) + }} + placeholder={`Enter ${param.type} value`} + className="flex-1 text-sm" + type={param.type === 'number' ? 'number' : 'text'} + /> + + {param.type} + +
+ ))} +
+
+ )} +
+) diff --git a/frontends/nextjs/src/components/editors/lua/lua-editor/configuration/LuaScriptsListCard.tsx b/frontends/nextjs/src/components/editors/lua/lua-editor/configuration/LuaScriptsListCard.tsx new file mode 100644 index 000000000..48314b442 --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/lua-editor/configuration/LuaScriptsListCard.tsx @@ -0,0 +1,69 @@ +import { Plus, Trash } from '@phosphor-icons/react' +import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui' +import type { LuaScript } from '@/lib/level-types' + +interface LuaScriptsListCardProps { + scripts: LuaScript[] + selectedScriptId: string | null + onAddScript: () => void + onDeleteScript: (id: string) => void + onSelectScript: (id: string) => void +} + +export const LuaScriptsListCard = ({ + scripts, + selectedScriptId, + onAddScript, + onDeleteScript, + onSelectScript, +}: LuaScriptsListCardProps) => ( + + +
+ Lua Scripts + +
+ Custom logic scripts +
+ +
+ {scripts.length === 0 ? ( +

+ No scripts yet. Create one to start. +

+ ) : ( + scripts.map((script) => ( +
onSelectScript(script.id)} + > +
+
{script.name}
+
+ {script.parameters.length} params +
+
+ +
+ )) + )} +
+
+
+) diff --git a/frontends/nextjs/src/components/editors/lua/lua-editor/execution/LuaExecutionPreview.tsx b/frontends/nextjs/src/components/editors/lua/lua-editor/execution/LuaExecutionPreview.tsx new file mode 100644 index 000000000..3488aa298 --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/lua-editor/execution/LuaExecutionPreview.tsx @@ -0,0 +1,68 @@ +import { CheckCircle, XCircle } from '@phosphor-icons/react' +import { Card, CardContent, CardHeader, CardTitle, Label } from '@/components/ui' +import type { LuaExecutionResult } from '@/lib/lua-engine' + +interface LuaExecutionPreviewProps { + result: LuaExecutionResult | null +} + +export const LuaExecutionPreview = ({ result }: LuaExecutionPreviewProps) => { + return ( +
+ {result && ( + + +
+ {result.success ? ( + + ) : ( + + )} + + {result.success ? 'Execution Successful' : 'Execution Failed'} + +
+
+ + {result.error && ( +
+ +
+                  {result.error}
+                
+
+ )} + + {result.logs.length > 0 && ( +
+ +
+                  {result.logs.join('\n')}
+                
+
+ )} + + {result.result !== null && result.result !== undefined && ( +
+ +
+                  {JSON.stringify(result.result, null, 2)}
+                
+
+ )} +
+
+ )} + +
+

Available in context:

+
    +
  • context.data - Input data
  • +
  • context.user - Current user info
  • +
  • context.kv - Key-value storage
  • +
  • context.log(msg) - Logging function
  • +
+
+
+ ) +} diff --git a/frontends/nextjs/src/components/editors/lua/lua-editor/linting/LuaLintingControls.tsx b/frontends/nextjs/src/components/editors/lua/lua-editor/linting/LuaLintingControls.tsx new file mode 100644 index 000000000..37b24ee6c --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/lua-editor/linting/LuaLintingControls.tsx @@ -0,0 +1,30 @@ +import { SecurityWarningDialog } from '@/components/organisms/security/SecurityWarningDialog' +import type { SecurityScanResult } from '@/lib/security-scanner' + +interface LuaLintingControlsProps { + scanResult: SecurityScanResult | null + showDialog: boolean + onDialogChange: (open: boolean) => void + onProceed: () => void +} + +export const LuaLintingControls = ({ + scanResult, + showDialog, + onDialogChange, + onProceed, +}: LuaLintingControlsProps) => { + if (!scanResult) return null + + return ( + onDialogChange(false)} + codeType="Lua script" + showProceedButton + /> + ) +} diff --git a/frontends/nextjs/src/components/editors/lua/lua-editor/toolbar/LuaEditorToolbar.tsx b/frontends/nextjs/src/components/editors/lua/lua-editor/toolbar/LuaEditorToolbar.tsx new file mode 100644 index 000000000..346785e90 --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/lua-editor/toolbar/LuaEditorToolbar.tsx @@ -0,0 +1,36 @@ +import { Play, ShieldCheck } from '@phosphor-icons/react' +import { Button, CardHeader, CardTitle, CardDescription } from '@/components/ui' +import type { LuaScript } from '@/lib/level-types' + +interface LuaEditorToolbarProps { + script: LuaScript + isExecuting: boolean + onScan: () => void + onTest: () => void +} + +export const LuaEditorToolbar = ({ + script, + isExecuting, + onScan, + onTest, +}: LuaEditorToolbarProps) => ( + +
+
+ Edit Script: {script.name} + Write custom Lua logic +
+
+ + +
+
+
+) diff --git a/frontends/nextjs/src/components/editors/lua/lua-editor/useLuaEditorLogic.ts b/frontends/nextjs/src/components/editors/lua/lua-editor/useLuaEditorLogic.ts new file mode 100644 index 000000000..68502f808 --- /dev/null +++ b/frontends/nextjs/src/components/editors/lua/lua-editor/useLuaEditorLogic.ts @@ -0,0 +1,144 @@ +import { useEffect, useMemo, useState } from 'react' +import { toast } from 'sonner' +import { executeLuaScriptWithProfile } from '@/lib/lua/execute-lua-script-with-profile' +import type { LuaExecutionResult } from '@/lib/lua-engine' +import type { LuaScript } from '@/lib/level-types' +import { securityScanner, type SecurityScanResult } from '@/lib/security-scanner' + +interface UseLuaEditorLogicProps { + scripts: LuaScript[] + onScriptsChange: (scripts: LuaScript[]) => void +} + +const defaultCode = '-- Lua script example\n-- Access input parameters via context.data\n-- Use log() or print() to output messages\n\nlog("Script started")\n\nif context.data then\n log("Received data:", context.data)\nend\n\nlocal result = {\n success = true,\n message = "Script executed successfully"\n}\n\nreturn result' + +export const useLuaEditorLogic = ({ scripts, onScriptsChange }: UseLuaEditorLogicProps) => { + const [selectedScriptId, setSelectedScriptId] = useState(scripts.length > 0 ? scripts[0].id : null) + const [testOutput, setTestOutput] = useState(null) + const [testInputs, setTestInputs] = useState>({}) + const [isExecuting, setIsExecuting] = useState(false) + const [isFullscreen, setIsFullscreen] = useState(false) + const [showSnippetLibrary, setShowSnippetLibrary] = useState(false) + const [securityScanResult, setSecurityScanResult] = useState(null) + const [showSecurityDialog, setShowSecurityDialog] = useState(false) + + const currentScript = useMemo(() => scripts.find((script) => script.id === selectedScriptId), [scripts, selectedScriptId]) + + useEffect(() => { + if (scripts.length > 0 && !selectedScriptId) setSelectedScriptId(scripts[0].id) + }, [scripts, selectedScriptId]) + + useEffect(() => { + if (!currentScript) return + const inputs: Record = {} + currentScript.parameters.forEach((param) => { + inputs[param.name] = param.type === 'number' ? 0 : param.type === 'boolean' ? false : '' + }) + setTestInputs(inputs) + }, [currentScript?.parameters.length, selectedScriptId]) + + const handleAddScript = () => { + const newScript: LuaScript = { id: `lua_${Date.now()}`, name: 'New Script', code: defaultCode, parameters: [] } + onScriptsChange([...scripts, newScript]) + setSelectedScriptId(newScript.id) + toast.success('Script created') + } + + const handleDeleteScript = (scriptId: string) => { + onScriptsChange(scripts.filter((s) => s.id !== scriptId)) + if (selectedScriptId === scriptId) setSelectedScriptId(scripts.length > 1 ? scripts[0].id : null) + toast.success('Script deleted') + } + + const handleUpdateScript = (updates: Partial) => { + if (!currentScript) return + onScriptsChange(scripts.map((script) => (script.id === currentScript.id ? { ...script, ...updates } : script))) + } + + const handleAddParameter = () => currentScript && handleUpdateScript({ parameters: [...currentScript.parameters, { name: `param${currentScript.parameters.length + 1}`, type: 'string' }] }) + const handleDeleteParameter = (index: number) => currentScript && handleUpdateScript({ parameters: currentScript.parameters.filter((_, i) => i !== index) }) + const handleUpdateParameter = (index: number, updates: { name?: string; type?: string }) => currentScript && handleUpdateScript({ parameters: currentScript.parameters.map((p, i) => (i === index ? { ...p, ...updates } : p)) }) + const handleTestInputChange = (paramName: string, value: any) => setTestInputs({ ...testInputs, [paramName]: value }) + + const executeScript = async () => { + if (!currentScript) return + setIsExecuting(true) + setTestOutput(null) + try { + const contextData: any = {} + currentScript.parameters.forEach((param) => { + contextData[param.name] = testInputs[param.name] + }) + const result = await executeLuaScriptWithProfile(currentScript.code, { data: contextData, user: { username: 'test_user', role: 'god' }, log: (...args: any[]) => console.log('[Lua]', ...args) }, currentScript) + setTestOutput(result) + toast[result.success ? 'success' : 'error'](result.success ? 'Script executed successfully' : 'Script execution failed') + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + toast.error('Execution error: ' + message) + setTestOutput({ success: false, error: message, logs: [] }) + } finally { + setIsExecuting(false) + } + } + + const runSecurityScan = () => { + if (!currentScript) return null + const scanResult = securityScanner.scanLua(currentScript.code) + setSecurityScanResult(scanResult) + return scanResult + } + + const handleTestScript = async () => { + if (!currentScript) return + const scanResult = runSecurityScan() + if (!scanResult) return + if (scanResult.severity === 'critical' || scanResult.severity === 'high') { + setShowSecurityDialog(true) + toast.warning('Security issues detected in script') + return + } + if (scanResult.severity === 'medium' && scanResult.issues.length > 0) { + toast.warning(`${scanResult.issues.length} security warning(s) detected`) + } + await executeScript() + } + + const handleScanCode = () => { + const scanResult = runSecurityScan() + if (!scanResult) return + setShowSecurityDialog(true) + if (scanResult.safe) toast.success('No security issues detected') + else toast.warning(`${scanResult.issues.length} security issue(s) detected`) + } + + const handleProceedWithExecution = async () => { + setShowSecurityDialog(false) + await executeScript() + } + + return { + currentScript, + selectedScriptId, + testOutput, + testInputs, + isExecuting, + isFullscreen, + showSnippetLibrary, + securityScanResult, + showSecurityDialog, + setSelectedScriptId, + setIsFullscreen, + setShowSnippetLibrary, + setShowSecurityDialog, + handleAddScript, + handleDeleteScript, + handleUpdateScript, + handleAddParameter, + handleDeleteParameter, + handleUpdateParameter, + handleTestInputChange, + handleScanCode, + handleTestScript, + handleProceedWithExecution, + } +}