From 499fdef661e80172d89c0a06233015cb678e652c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Fri, 3 Apr 2026 08:49:00 +0300 Subject: [PATCH] feat: implement code editors for PHP, Nginx, and Apache, and add IPC handlers for service configuration management --- service_events.json | 157 +++++++++++++ src/main/ipc/index.ts | 30 +++ src/main/services/PhpManagerService.ts | 12 +- .../src/components/ApacheCodeEditor.tsx | 6 +- .../components/Dashboard/DashboardDetails.tsx | 69 +++--- .../components/Dashboard/DashboardSidebar.tsx | 26 +-- .../src/components/NginxCodeEditor.tsx | 6 +- .../src/components/PhpIniCodeEditor.tsx | 206 ++--------------- .../src/components/SearchableCodeEditor.tsx | 209 ++++++++++++++++++ 9 files changed, 479 insertions(+), 242 deletions(-) create mode 100644 src/renderer/src/components/SearchableCodeEditor.tsx diff --git a/service_events.json b/service_events.json index 029566b..a0a68bb 100644 --- a/service_events.json +++ b/service_events.json @@ -1,4 +1,161 @@ [ + { + "service": "php:5.6.40", + "action": "start", + "status": "success", + "id": "lm8lauin4", + "timestamp": 1775194498184 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "fv2jk93kp", + "timestamp": 1775194498166 + }, + { + "service": "php:8.2.30", + "action": "start", + "status": "success", + "id": "ajlbnlqlt", + "timestamp": 1775194498146 + }, + { + "service": "php:8.5.4", + "action": "start", + "status": "success", + "id": "7ky21n7ej", + "timestamp": 1775194498128 + }, + { + "service": "mariadb:11.4.2", + "action": "start", + "status": "success", + "id": "ivnjwq509", + "timestamp": 1775194497945 + }, + { + "service": "mariadb:10.11.8", + "action": "start", + "status": "success", + "id": "xkegjk3qq", + "timestamp": 1775194497925 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "cs9w16ouo", + "timestamp": 1775194497146 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "9njsd2gqb", + "timestamp": 1775194497081 + }, + { + "service": "php:5.6.40", + "action": "start", + "status": "success", + "id": "5je86oerx", + "timestamp": 1775194323027 + }, + { + "service": "php:7.4.33", + "action": "start", + "status": "success", + "id": "2mhrdvjef", + "timestamp": 1775194323008 + }, + { + "service": "php:8.2.30", + "action": "start", + "status": "success", + "id": "o32ayujie", + "timestamp": 1775194322987 + }, + { + "service": "php:8.5.4", + "action": "start", + "status": "success", + "id": "ypehqtkno", + "timestamp": 1775194322969 + }, + { + "service": "mariadb:10.11.8", + "action": "start", + "status": "success", + "id": "lo6hi46hi", + "timestamp": 1775194322796 + }, + { + "service": "mariadb:11.4.2", + "action": "start", + "status": "success", + "id": "583vl27oq", + "timestamp": 1775194322774 + }, + { + "service": "apache", + "action": "start", + "status": "success", + "id": "sg7xaxnph", + "timestamp": 1775194321996 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "y3kdqv4im", + "timestamp": 1775194321881 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "j3che13xf", + "timestamp": 1775194289720 + }, + { + "service": "nginx", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "kky7b35zb", + "timestamp": 1775194287876 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "gge4posew", + "timestamp": 1775194278036 + }, + { + "service": "nginx", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "ttyn4sulo", + "timestamp": 1775194276079 + }, + { + "service": "nginx", + "action": "start", + "status": "success", + "id": "cq91o7h9b", + "timestamp": 1775194257169 + }, + { + "service": "nginx", + "action": "exit", + "status": "failure", + "details": "Exit code: 1", + "id": "yyp3j341e", + "timestamp": 1775194255191 + }, { "service": "php:7.4.33", "action": "start", diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 9022a06..ddcd359 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -1068,6 +1068,36 @@ export function registerIpcHandlers(): void { return { success: true, settings } }) + ipcMain.handle('config:service-file:get', async (_event, serviceId: string) => { + try { + if (serviceId && serviceId.startsWith('php:')) { + const version = serviceId.split(':')[1] + const filePath = path.join(configService.getBasePath(), 'bin', `php-${version}`, 'php.ini') + if (fs.existsSync(filePath)) { + return { success: true, content: fs.readFileSync(filePath, 'utf8'), path: filePath } + } + return { success: false, message: 'php.ini dosyası bulunamadı' } + } + return { success: false, message: 'Bu servis türü için doğrudan dosya düzenleme desteklenmiyor' } + } catch (error: any) { + return { success: false, message: error.message } + } + }) + + ipcMain.handle('config:service-file:save', async (_event, { serviceId, content }) => { + try { + if (serviceId && serviceId.startsWith('php:')) { + const version = serviceId.split(':')[1] + const filePath = path.join(configService.getBasePath(), 'bin', `php-${version}`, 'php.ini') + fs.writeFileSync(filePath, content, 'utf8') + return { success: true, message: 'Ayarlar kaydedildi, bir sonraki başlatmada geçerli olacak.' } + } + return { success: false, message: 'Servis türü desteklenmiyor' } + } catch (error: any) { + return { success: false, message: error.message } + } + }) + ipcMain.handle('services:clear-events', async () => { try { serviceEventLogger.clear() diff --git a/src/main/services/PhpManagerService.ts b/src/main/services/PhpManagerService.ts index 85909f3..90d3f9e 100644 --- a/src/main/services/PhpManagerService.ts +++ b/src/main/services/PhpManagerService.ts @@ -190,10 +190,20 @@ export class PhpManagerService { if (fs.existsSync(iniPath)) { const content = fs.readFileSync(iniPath, 'utf8') const lines = content.split(/\r?\n/) + const essentialExts = ['openssl', 'curl', 'mbstring', 'fileinfo', 'zip', 'gd', 'pdo_mysql', 'mysqli'] + let changed = false + for (const ext of extensions) { - // Check for extension=php_NAME.dll or extension=NAME (newer PHP) + // Check if already enabled const pattern = new RegExp(`^\\s*extension\\s*=\\s*(?:php_)?${ext.name}(?:\\.dll)?\\s*$`, 'i') ext.enabled = lines.some(line => pattern.test(line.trim())) + + // Auto-enable essential ones if found but not enabled + if (!ext.enabled && essentialExts.includes(ext.name.toLowerCase())) { + await this.toggleExtension(version, ext.name, true) + ext.enabled = true + changed = true + } } } diff --git a/src/renderer/src/components/ApacheCodeEditor.tsx b/src/renderer/src/components/ApacheCodeEditor.tsx index 9b796b4..6c1cfce 100644 --- a/src/renderer/src/components/ApacheCodeEditor.tsx +++ b/src/renderer/src/components/ApacheCodeEditor.tsx @@ -1,5 +1,5 @@ import React from 'react' -import Editor from 'react-simple-code-editor' +import SearchableCodeEditor from './SearchableCodeEditor' import Prism from 'prismjs' import 'prismjs/components/prism-apacheconf' import 'prismjs/themes/prism-tomorrow.css' @@ -20,7 +20,7 @@ const ApacheCodeEditor: React.FC = ({ value, onChange }) fontSize: '14px', minHeight: '300px' }}> - onChange(code)} highlight={code => Prism.highlight(code, Prism.languages.apacheconf, 'apacheconf')} @@ -32,7 +32,7 @@ const ApacheCodeEditor: React.FC = ({ value, onChange }) outline: 'none' }} textareaClassName="apache-editor-textarea" - preClassName="apache-editor-pre" + languageLabel="Apache" /> ) diff --git a/src/renderer/src/components/SearchableCodeEditor.tsx b/src/renderer/src/components/SearchableCodeEditor.tsx new file mode 100644 index 0000000..8476923 --- /dev/null +++ b/src/renderer/src/components/SearchableCodeEditor.tsx @@ -0,0 +1,209 @@ +import { + Box, + TextField, + IconButton, + Typography, + Paper, + Stack, + Tooltip +} from '@mui/material' +import { + Close as CloseIcon, + KeyboardArrowDown as NextIcon, + KeyboardArrowUp as PrevIcon, + Search as SearchIcon +} from '@mui/icons-material' +import { useState, useRef, useEffect, useCallback } from 'react' +import Editor from 'react-simple-code-editor' + +interface SearchableCodeEditorProps { + value: string; + onValueChange: (v: string) => void; + highlight: (code: string) => string | JSX.Element; + padding?: number; + style?: React.CSSProperties; + textareaClassName?: string; + languageLabel?: string; +} + +export default function SearchableCodeEditor({ + value, + onValueChange, + highlight, + padding = 20, + style, + textareaClassName = "editor-textarea", + languageLabel +}: SearchableCodeEditorProps) { + const [isSearchOpen, setIsSearchOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [searchIndex, setSearchIndex] = useState(-1); + const [searchResults, setSearchResults] = useState([]); + + const editorRef = useRef(null); + const searchInputRef = useRef(null); + + const performSearch = useCallback((query: string) => { + if (!query) { + setSearchResults([]); + setSearchIndex(-1); + return; + } + + const indices: number[] = []; + let pos = value.toLowerCase().indexOf(query.toLowerCase()); + while (pos !== -1) { + indices.push(pos); + pos = value.toLowerCase().indexOf(query.toLowerCase(), pos + 1); + } + setSearchResults(indices); + setSearchIndex(indices.length > 0 ? 0 : -1); + }, [value]); + + useEffect(() => { + if (isSearchOpen && searchQuery) { + performSearch(searchQuery); + } + }, [searchQuery, isSearchOpen, performSearch]); + + const scrollToMatch = (index: number) => { + if (index === -1 || searchResults.length === 0) return; + + const offset = searchResults[index]; + const textarea = editorRef.current?.querySelector('textarea'); + if (textarea) { + textarea.focus(); + textarea.setSelectionRange(offset, offset + searchQuery.length); + + // Basic line height estimate to scroll correctly + const lineCount = value.substring(0, offset).split('\n').length; + const lineHeight = 22; // Typical line height for 15px mono font + textarea.scrollTop = (lineCount - 5) * lineHeight; + } + }; + + useEffect(() => { + if (searchIndex !== -1) { + scrollToMatch(searchIndex); + } + }, [searchIndex]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'f') { + e.preventDefault(); + setIsSearchOpen(true); + setTimeout(() => searchInputRef.current?.focus(), 100); + } + if (e.key === 'Escape') { + setIsSearchOpen(false); + setSearchQuery(''); + } + }; + + const nextMatch = () => { + if (searchResults.length === 0) return; + setSearchIndex((prev) => (prev + 1) % searchResults.length); + }; + + const prevMatch = () => { + if (searchResults.length === 0) return; + setSearchIndex((prev) => (prev - 1 + searchResults.length) % searchResults.length); + }; + + return ( + + {isSearchOpen && ( + + + setSearchQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') nextMatch(); + }} + autoComplete="off" + sx={{ + flexGrow: 1, + '& .MuiInputBase-root': { + fontSize: '0.85rem', + color: '#fff' + }, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none' + } + }} + /> + + {searchResults.length > 0 && ( + + {searchIndex + 1} / {searchResults.length} + + )} + + + + + + + + + { setIsSearchOpen(false); setSearchQuery(''); }} sx={{ color: 'error.main', opacity: 0.8 }}> + + + + + )} + + + + + + ); +}