1- import React , { useState , useEffect } from 'react' ;
2- import { X , Save , RotateCcw , FileText , AlertTriangle , Check } from 'lucide-react' ;
1+ import React , { useState , useEffect , useMemo } from 'react' ;
2+ import { X , Save , RotateCcw , FileText , AlertTriangle , Check , Search , Code , LayoutGrid , Power } from 'lucide-react' ;
33import clsx from 'clsx' ;
44import { useModal } from '../context/ModalContext' ;
55
@@ -12,10 +12,15 @@ function PhpIniEditor({ version, isOpen, onClose }) {
1212 const [ error , setError ] = useState ( null ) ;
1313 const [ saved , setSaved ] = useState ( false ) ;
1414 const [ hasChanges , setHasChanges ] = useState ( false ) ;
15+
16+ const [ activeTab , setActiveTab ] = useState ( 'extensions' ) ;
17+ const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
1518
1619 useEffect ( ( ) => {
1720 if ( isOpen && version ) {
1821 loadPhpIni ( ) ;
22+ setActiveTab ( 'extensions' ) ;
23+ setSearchQuery ( '' ) ;
1924 }
2025 } , [ isOpen , version ] ) ;
2126
@@ -41,6 +46,106 @@ function PhpIniEditor({ version, isOpen, onClose }) {
4146 }
4247 } ;
4348
49+ // Parse extensions from php.ini content
50+ const extensions = useMemo ( ( ) => {
51+ if ( ! content ) return [ ] ;
52+
53+ // List of widely used extensions to always show, even if missing from php.ini
54+ const WIDELY_USED_EXTENSIONS = [
55+ 'bz2' , 'curl' , 'ffi' , 'ftp' , 'fileinfo' , 'gd' , 'gettext' , 'gmp' , 'intl' ,
56+ 'imap' , 'ldap' , 'mbstring' , 'exif' , 'mysqli' , 'oci8_12c' , 'odbc' , 'openssl' ,
57+ 'pdo_firebird' , 'pdo_mysql' , 'pdo_oci' , 'pdo_odbc' , 'pdo_pgsql' , 'pdo_sqlite' ,
58+ 'pgsql' , 'shmop' , 'snmp' , 'soap' , 'sockets' , 'sodium' , 'sqlite3' , 'tidy' , 'xsl' , 'zip' ,
59+ 'redis' , 'mongodb' , 'imagick' , 'memcached' , 'xdebug' , 'opcache' , 'bcmath' ,
60+ 'calendar' , 'xmlrpc'
61+ ] ;
62+
63+ const lines = content . split ( '\n' ) ;
64+ const extMap = new Map ( ) ;
65+
66+ lines . forEach ( ( line , index ) => {
67+ const trimmed = line . trim ( ) ;
68+ // Match extension=... or zend_extension=... with optional leading semicolon
69+ const match = trimmed . match ( / ^ ( ; ? ) \s * ( z e n d _ e x t e n s i o n | e x t e n s i o n ) \s * = \s * ( [ ^ \s ; ] + ) / i) ;
70+
71+ if ( match ) {
72+ let extName = match [ 3 ] . replace ( / ^ [ " ' ] | [ " ' ] $ / g, '' ) ;
73+ // Clean up names for display
74+ const displayName = extName
75+ . replace ( / ^ p h p _ / , '' )
76+ . replace ( / \. d l l $ / , '' )
77+ . replace ( / \. s o $ / , '' ) ;
78+
79+ const ext = {
80+ lineIndex : index ,
81+ isDisabled : match [ 1 ] === ';' ,
82+ type : match [ 2 ] . toLowerCase ( ) ,
83+ name : extName ,
84+ displayName,
85+ existsInFile : true ,
86+ } ;
87+
88+ // If we see the exact same display name, prefer the uncommented one if conflict exists
89+ if ( extMap . has ( displayName ) ) {
90+ const existing = extMap . get ( displayName ) ;
91+ if ( existing . isDisabled && ! ext . isDisabled ) {
92+ extMap . set ( displayName , ext ) ;
93+ }
94+ } else {
95+ extMap . set ( displayName , ext ) ;
96+ }
97+ }
98+ } ) ;
99+
100+ // Add widely used extensions that completely missing from the file
101+ WIDELY_USED_EXTENSIONS . forEach ( extName => {
102+ if ( ! extMap . has ( extName ) ) {
103+ extMap . set ( extName , {
104+ lineIndex : - 1 , // Indicates it doesn't exist in file yet
105+ isDisabled : true , // Missing means it's not enabled
106+ type : extName === 'opcache' || extName === 'xdebug' ? 'zend_extension' : 'extension' ,
107+ name : extName , // We don't append .dll/.so strictly here, just the name is usually enough for modern PHP
108+ displayName : extName ,
109+ existsInFile : false ,
110+ } ) ;
111+ }
112+ } ) ;
113+
114+ return Array . from ( extMap . values ( ) ) . sort ( ( a , b ) => a . displayName . localeCompare ( b . displayName ) ) ;
115+ } , [ content ] ) ;
116+
117+ const filteredExtensions = useMemo ( ( ) => {
118+ if ( ! searchQuery ) return extensions ;
119+ const query = searchQuery . toLowerCase ( ) ;
120+ return extensions . filter ( ext =>
121+ ext . displayName . toLowerCase ( ) . includes ( query ) ||
122+ ext . name . toLowerCase ( ) . includes ( query )
123+ ) ;
124+ } , [ extensions , searchQuery ] ) ;
125+
126+ const toggleExtension = ( ext ) => {
127+ const lines = content . split ( '\n' ) ;
128+
129+ if ( ext . existsInFile ) {
130+ // Toggle existing line
131+ const line = lines [ ext . lineIndex ] ;
132+ if ( line . match ( / ^ \s * ; / ) ) {
133+ // Remove the first semicolon
134+ lines [ ext . lineIndex ] = line . replace ( / ^ ( \s * ) ; \s * / , '$1' ) ;
135+ } else {
136+ // Add a semicolon at the start
137+ lines [ ext . lineIndex ] = line . replace ( / ^ ( \s * ) / , '$1;' ) ;
138+ }
139+ } else {
140+ // Doesn't exist, we must add it.
141+ // Easiest is to append to the end of the file
142+ lines . push ( `\n; DevBox Pro automatically added extension` ) ;
143+ lines . push ( `${ ext . type } =${ ext . name } ` ) ;
144+ }
145+
146+ setContent ( lines . join ( '\n' ) ) ;
147+ } ;
148+
44149 const handleSave = async ( ) => {
45150 setSaving ( true ) ;
46151 setError ( null ) ;
@@ -115,7 +220,7 @@ function PhpIniEditor({ version, isOpen, onClose }) {
115220
116221 return (
117222 < div className = "fixed inset-0 bg-black/50 flex items-center justify-center z-50" >
118- < div className = "bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-[900px] max-h-[80vh ] flex flex-col overflow-hidden" >
223+ < div className = "bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-[900px] max-h-[85vh ] flex flex-col overflow-hidden" >
119224 { /* Header */ }
120225 < div className = "flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700" >
121226 < div className = "flex items-center gap-3" >
@@ -125,7 +230,7 @@ function PhpIniEditor({ version, isOpen, onClose }) {
125230 PHP { version } Configuration
126231 </ h3 >
127232 < p className = "text-sm text-gray-500 dark:text-gray-400" >
128- Edit php.ini settings for PHP { version }
233+ Manage extensions and php.ini settings
129234 </ p >
130235 </ div >
131236 </ div >
@@ -137,8 +242,36 @@ function PhpIniEditor({ version, isOpen, onClose }) {
137242 </ button >
138243 </ div >
139244
245+ { /* Tabs */ }
246+ < div className = "flex items-center px-6 bg-gray-50/50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700" >
247+ < button
248+ onClick = { ( ) => setActiveTab ( 'extensions' ) }
249+ className = { clsx (
250+ 'px-4 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2' ,
251+ activeTab === 'extensions'
252+ ? 'border-primary-500 text-primary-600 dark:text-primary-400'
253+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600'
254+ ) }
255+ >
256+ < LayoutGrid className = "w-4 h-4" />
257+ Extensions
258+ </ button >
259+ < button
260+ onClick = { ( ) => setActiveTab ( 'editor' ) }
261+ className = { clsx (
262+ 'px-4 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2' ,
263+ activeTab === 'editor'
264+ ? 'border-primary-500 text-primary-600 dark:text-primary-400'
265+ : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600'
266+ ) }
267+ >
268+ < Code className = "w-4 h-4" />
269+ Raw Editor
270+ </ button >
271+ </ div >
272+
140273 { /* Content */ }
141- < div className = "flex-1 overflow-hidden flex flex-col" >
274+ < div className = "flex-1 overflow-hidden flex flex-col bg-gray-50/30 dark:bg-gray-900/20 " >
142275 { loading ? (
143276 < div className = "flex-1 flex items-center justify-center" >
144277 < div className = "animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" > </ div >
@@ -149,27 +282,101 @@ function PhpIniEditor({ version, isOpen, onClose }) {
149282 { error }
150283 </ div >
151284 ) : (
152- < textarea
153- value = { content }
154- onChange = { ( e ) => setContent ( e . target . value ) }
155- className = "flex-1 w-full p-4 font-mono text-sm bg-gray-900 text-gray-100 resize-none focus:outline-none"
156- spellCheck = { false }
157- style = { { minHeight : '400px' } }
158- />
285+ < >
286+ { activeTab === 'extensions' && (
287+ < div className = "flex-1 flex flex-col overflow-hidden" >
288+ < div className = "p-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800" >
289+ < div className = "relative" >
290+ < Search className = "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
291+ < input
292+ type = "text"
293+ placeholder = "Search extensions..."
294+ value = { searchQuery }
295+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
296+ className = "w-full pl-9 pr-4 py-2 bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-transparent"
297+ />
298+ </ div >
299+ </ div >
300+ < div className = "flex-1 overflow-y-auto p-4" >
301+ { filteredExtensions . length === 0 ? (
302+ < div className = "text-center py-8 text-gray-500 dark:text-gray-400" >
303+ No extensions found matching "{ searchQuery } "
304+ </ div >
305+ ) : (
306+ < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3" >
307+ { filteredExtensions . map ( ( ext ) => (
308+ < div
309+ key = { `${ ext . lineIndex } -${ ext . name } ` }
310+ className = { clsx (
311+ "flex items-center justify-between p-3 rounded-lg border transition-all duration-200" ,
312+ ext . isDisabled
313+ ? "bg-white dark:bg-gray-800/60 border-gray-200 dark:border-gray-700/50"
314+ : "bg-primary-50/50 dark:bg-primary-900/10 border-primary-200 dark:border-primary-800"
315+ ) }
316+ >
317+ < div className = "min-w-0 pr-3" >
318+ < h4 className = "font-medium text-sm text-gray-900 dark:text-gray-100 truncate flex items-center gap-2" >
319+ { ext . displayName }
320+ { ext . type === 'zend_extension' && (
321+ < span className = "text-[10px] px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 font-medium" >
322+ Zend
323+ </ span >
324+ ) }
325+ </ h4 >
326+ < p className = "text-xs text-gray-500 dark:text-gray-500 mt-0.5 truncate font-mono" >
327+ { ext . name }
328+ </ p >
329+ </ div >
330+ < button
331+ onClick = { ( ) => toggleExtension ( ext ) }
332+ className = { clsx (
333+ "relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2" ,
334+ ext . isDisabled ? "bg-gray-200 dark:bg-gray-700" : "bg-primary-500"
335+ ) }
336+ role = "switch"
337+ aria-checked = { ! ext . isDisabled }
338+ >
339+ < span
340+ aria-hidden = "true"
341+ className = { clsx (
342+ "pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" ,
343+ ext . isDisabled ? "translate-x-0" : "translate-x-4"
344+ ) }
345+ />
346+ </ button >
347+ </ div >
348+ ) ) }
349+ </ div >
350+ ) }
351+ </ div >
352+ </ div >
353+ ) }
354+
355+ { activeTab === 'editor' && (
356+ < textarea
357+ value = { content }
358+ onChange = { ( e ) => setContent ( e . target . value ) }
359+ className = "flex-1 w-full p-4 font-mono text-sm bg-[#1e1e1e] text-gray-300 resize-none focus:outline-none leading-relaxed"
360+ spellCheck = { false }
361+ placeholder = "Paste or edit php.ini content here..."
362+ style = { { minHeight : '400px' } }
363+ />
364+ ) }
365+ </ >
159366 ) }
160367 </ div >
161368
162369 { /* Footer */ }
163- < div className = "flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 " >
370+ < div className = "flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 " >
164371 < div className = "flex items-center gap-2" >
165372 { hasChanges && (
166- < span className = "text-sm text-amber-500 flex items-center gap-1" >
373+ < span className = "text-sm font-medium text-amber-600 dark:text-amber-400 flex items-center gap-1.5 bg-amber-50 dark:bg-amber-900/20 px-2 py-1 rounded-md " >
167374 < AlertTriangle className = "w-4 h-4" />
168375 Unsaved changes
169376 </ span >
170377 ) }
171378 { saved && (
172- < span className = "text-sm text-green-500 flex items-center gap-1" >
379+ < span className = "text-sm font-medium text-green-600 dark:text-green-400 flex items-center gap-1.5 bg-green-50 dark:bg-green-900/20 px-2 py-1 rounded-md " >
173380 < Check className = "w-4 h-4" />
174381 Saved successfully
175382 </ span >
@@ -179,7 +386,7 @@ function PhpIniEditor({ version, isOpen, onClose }) {
179386 < button
180387 onClick = { handleReset }
181388 disabled = { loading || saving }
182- className = "btn-secondary "
389+ className = "px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors "
183390 >
184391 < RotateCcw className = "w-4 h-4" />
185392 Reset to Default
@@ -188,8 +395,10 @@ function PhpIniEditor({ version, isOpen, onClose }) {
188395 onClick = { handleSave }
189396 disabled = { loading || saving || ! hasChanges }
190397 className = { clsx (
191- 'btn-primary' ,
192- ( ! hasChanges || saving ) && 'opacity-50 cursor-not-allowed'
398+ 'px-4 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 flex items-center gap-2 transition-all' ,
399+ ( ! hasChanges || saving )
400+ ? 'bg-primary-400 cursor-not-allowed'
401+ : 'bg-primary-600 hover:bg-primary-700 shadow-sm'
193402 ) }
194403 >
195404 < Save className = "w-4 h-4" />
0 commit comments