|
| 1 | +import * as Sentry from '@sentry/react'; |
1 | 2 | import { useEffect, useState } from 'react'; |
2 | 3 | import { usePerformanceMonitoring } from '~/lib/performance'; |
3 | | -import * as Sentry from '@sentry/react'; |
4 | 4 |
|
5 | 5 | interface DevToolsProps { |
6 | 6 | enabled?: boolean; |
7 | 7 | } |
8 | 8 |
|
9 | | -type DevToolTab = 'performance' | 'errors' | 'network' | 'info'; |
| 9 | +type DevToolView = 'main' | 'settings'; |
10 | 10 |
|
11 | 11 | export function DevTools({ |
12 | 12 | enabled = process.env.NODE_ENV === 'development', |
13 | 13 | }: DevToolsProps) { |
14 | 14 | const [isVisible, setIsVisible] = useState(false); |
15 | | - const [activeTab, setActiveTab] = useState<DevToolTab>('performance'); |
| 15 | + const [currentView, setCurrentView] = useState<DevToolView>('main'); |
16 | 16 | const [metrics, setMetrics] = useState<any[]>([]); |
17 | | - const [errors, setErrors] = useState<any[]>([]); |
18 | 17 | const analytics = usePerformanceMonitoring(); |
19 | 18 |
|
20 | 19 | useEffect(() => { |
21 | | - if (!enabled || !analytics) return; |
| 20 | + if (!enabled) return; |
| 21 | + |
| 22 | + // Add some initial test data for demonstration |
| 23 | + setMetrics([ |
| 24 | + { name: 'FCP', value: 1200, timestamp: Date.now() - 1000 }, |
| 25 | + { name: 'LCP', value: 2100, timestamp: Date.now() - 2000 }, |
| 26 | + { name: 'CLS', value: 0.05, timestamp: Date.now() - 3000 }, |
| 27 | + ]); |
22 | 28 |
|
23 | 29 | // Listen for custom performance events |
24 | 30 | const handlePerformanceEvent = (event: CustomEvent) => { |
25 | 31 | setMetrics(prev => [event.detail, ...prev].slice(0, 10)); |
26 | 32 | }; |
27 | 33 |
|
28 | | - // Listen for error events |
29 | | - const handleErrorEvent = (event: CustomEvent) => { |
30 | | - setErrors(prev => [event.detail, ...prev].slice(0, 10)); |
31 | | - }; |
32 | | - |
33 | | - // Listen for global errors |
34 | | - const handleGlobalError = (event: ErrorEvent) => { |
35 | | - setErrors(prev => |
36 | | - [ |
37 | | - { |
38 | | - type: 'JavaScript Error', |
39 | | - message: event.message, |
40 | | - filename: event.filename, |
41 | | - lineno: event.lineno, |
42 | | - colno: event.colno, |
43 | | - timestamp: new Date().toISOString(), |
44 | | - }, |
45 | | - ...prev, |
46 | | - ].slice(0, 10) |
47 | | - ); |
48 | | - }; |
49 | | - |
50 | 34 | window.addEventListener( |
51 | 35 | 'performance-metric', |
52 | 36 | handlePerformanceEvent as EventListener |
53 | 37 | ); |
54 | | - window.addEventListener( |
55 | | - 'dev-tools-error', |
56 | | - handleErrorEvent as EventListener |
57 | | - ); |
58 | | - window.addEventListener('error', handleGlobalError); |
59 | 38 |
|
60 | | - // Initialize Sentry breadcrumb (silent) |
61 | | - Sentry.addBreadcrumb({ |
62 | | - message: 'Dev Tools initialized', |
63 | | - level: 'info', |
64 | | - data: { component: 'DevTools' }, |
65 | | - }); |
| 39 | + // Initialize Sentry breadcrumb (silent) - only if Sentry is available |
| 40 | + try { |
| 41 | + Sentry.addBreadcrumb({ |
| 42 | + message: 'Dev Tools initialized', |
| 43 | + level: 'info', |
| 44 | + data: { component: 'DevTools' }, |
| 45 | + }); |
| 46 | + } catch (error) { |
| 47 | + // Sentry not available, continue without it |
| 48 | + } |
66 | 49 |
|
67 | 50 | return () => { |
68 | 51 | window.removeEventListener( |
69 | 52 | 'performance-metric', |
70 | 53 | handlePerformanceEvent as EventListener |
71 | 54 | ); |
72 | | - window.removeEventListener( |
73 | | - 'dev-tools-error', |
74 | | - handleErrorEvent as EventListener |
75 | | - ); |
76 | | - window.removeEventListener('error', handleGlobalError); |
77 | 55 | }; |
78 | | - }, [enabled, analytics]); |
| 56 | + }, [enabled]); |
79 | 57 |
|
80 | 58 | if (!enabled) return null; |
81 | 59 |
|
@@ -108,113 +86,123 @@ export function DevTools({ |
108 | 86 | return value > budget ? 'text-red-600' : 'text-green-600'; |
109 | 87 | }; |
110 | 88 |
|
111 | | - const tabs = [ |
112 | | - { id: 'performance' as const, label: 'Perf', icon: '📊' }, |
113 | | - { id: 'errors' as const, label: 'Errors', icon: '🚨' }, |
114 | | - { id: 'network' as const, label: 'Network', icon: '🌐' }, |
115 | | - { id: 'info' as const, label: 'Info', icon: 'ℹ️' }, |
116 | | - ]; |
117 | | - |
118 | | - const renderTabContent = () => { |
119 | | - switch (activeTab) { |
120 | | - case 'performance': |
121 | | - return ( |
122 | | - <div className='space-y-3'> |
123 | | - {metrics.length > 0 ? ( |
124 | | - <div className='space-y-1 max-h-32 overflow-y-auto'> |
125 | | - {metrics.map((metric, index) => ( |
126 | | - <div |
127 | | - key={index} |
128 | | - className='text-xs border-l-2 border-gray-200 pl-2' |
129 | | - > |
130 | | - <div className='flex justify-between items-center'> |
131 | | - <span className='font-mono text-gray-600'> |
132 | | - {metric.name} |
133 | | - </span> |
134 | | - <span |
135 | | - className={`font-semibold ${getStatusColor(metric.name, metric.value)}`} |
136 | | - > |
137 | | - {formatValue(metric.value, metric.name)} |
138 | | - </span> |
139 | | - </div> |
140 | | - </div> |
141 | | - ))} |
142 | | - </div> |
143 | | - ) : ( |
144 | | - <div className='text-gray-500 text-center py-2 text-xs'> |
145 | | - No metrics yet |
146 | | - </div> |
147 | | - )} |
148 | | - </div> |
149 | | - ); |
150 | | - |
151 | | - case 'errors': |
152 | | - return ( |
153 | | - <div className='space-y-3'> |
154 | | - {errors.length > 0 ? ( |
155 | | - <div className='space-y-1 max-h-32 overflow-y-auto'> |
156 | | - {errors.map((error, index) => ( |
157 | | - <div |
158 | | - key={index} |
159 | | - className='text-xs border-l-2 border-red-200 pl-2' |
160 | | - > |
161 | | - <div className='font-mono text-red-600'> |
162 | | - {error.type || 'Error'} |
163 | | - </div> |
164 | | - <div className='text-gray-500 truncate'> |
165 | | - {error.message} |
166 | | - </div> |
167 | | - {error.filename && ( |
168 | | - <div className='text-gray-400 text-xs'> |
169 | | - {error.filename}:{error.lineno} |
170 | | - </div> |
171 | | - )} |
172 | | - </div> |
173 | | - ))} |
174 | | - </div> |
175 | | - ) : ( |
176 | | - <div className='text-gray-500 text-center py-2 text-xs'> |
177 | | - No errors detected |
178 | | - </div> |
179 | | - )} |
180 | | - </div> |
181 | | - ); |
182 | | - |
183 | | - case 'network': |
184 | | - return ( |
185 | | - <div className='space-y-3'> |
186 | | - <div className='text-gray-500 text-center py-2 text-xs'> |
187 | | - Network monitoring coming soon |
188 | | - </div> |
| 89 | + const renderMainView = () => ( |
| 90 | + <div className='space-y-3'> |
| 91 | + {/* Web Vitals in a single row */} |
| 92 | + <div className='flex items-center space-x-4'> |
| 93 | + {metrics.slice(0, 4).map((metric, index) => ( |
| 94 | + <div key={index} className='flex items-center space-x-1'> |
| 95 | + <span className='text-xs text-gray-500 font-mono'> |
| 96 | + {metric.name}: |
| 97 | + </span> |
| 98 | + <span |
| 99 | + className={`text-xs font-semibold ${getStatusColor(metric.name, metric.value)}`} |
| 100 | + > |
| 101 | + {formatValue(metric.value, metric.name)} |
| 102 | + </span> |
189 | 103 | </div> |
190 | | - ); |
| 104 | + ))} |
| 105 | + </div> |
191 | 106 |
|
192 | | - case 'info': |
193 | | - return ( |
194 | | - <div className='space-y-3'> |
195 | | - <div className='space-y-1 text-xs'> |
196 | | - <div className='flex justify-between'> |
197 | | - <span className='text-gray-500'>Environment:</span> |
198 | | - <span className='font-mono'>{import.meta.env.MODE}</span> |
199 | | - </div> |
200 | | - <div className='flex justify-between'> |
201 | | - <span className='text-gray-500'>Sentry:</span> |
202 | | - <span className='font-mono'> |
203 | | - {import.meta.env.VITE_SENTRY_DSN ? 'Enabled' : 'Disabled'} |
204 | | - </span> |
205 | | - </div> |
206 | | - <div className='flex justify-between'> |
207 | | - <span className='text-gray-500'>React Router:</span> |
208 | | - <span className='font-mono'>v7</span> |
209 | | - </div> |
210 | | - </div> |
211 | | - </div> |
212 | | - ); |
| 107 | + {/* Settings button on its own row */} |
| 108 | + <button |
| 109 | + onClick={() => setCurrentView('settings')} |
| 110 | + className='w-full flex items-center justify-center space-x-2 px-3 py-2 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors' |
| 111 | + > |
| 112 | + <svg |
| 113 | + className='w-4 h-4' |
| 114 | + fill='none' |
| 115 | + stroke='currentColor' |
| 116 | + viewBox='0 0 24 24' |
| 117 | + > |
| 118 | + <path |
| 119 | + strokeLinecap='round' |
| 120 | + strokeLinejoin='round' |
| 121 | + strokeWidth={2} |
| 122 | + d='M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' |
| 123 | + /> |
| 124 | + <path |
| 125 | + strokeLinecap='round' |
| 126 | + strokeLinejoin='round' |
| 127 | + strokeWidth={2} |
| 128 | + d='M15 12a3 3 0 11-6 0 3 3 0 016 0z' |
| 129 | + /> |
| 130 | + </svg> |
| 131 | + <span>Settings</span> |
| 132 | + </button> |
| 133 | + </div> |
| 134 | + ); |
213 | 135 |
|
214 | | - default: |
215 | | - return null; |
216 | | - } |
217 | | - }; |
| 136 | + const renderSettingsView = () => ( |
| 137 | + <div className='space-y-3'> |
| 138 | + <div className='flex items-center space-x-2'> |
| 139 | + <button |
| 140 | + onClick={() => setCurrentView('main')} |
| 141 | + className='text-gray-500 hover:text-gray-700' |
| 142 | + > |
| 143 | + <svg |
| 144 | + className='w-4 h-4' |
| 145 | + fill='none' |
| 146 | + stroke='currentColor' |
| 147 | + viewBox='0 0 24 24' |
| 148 | + > |
| 149 | + <path |
| 150 | + strokeLinecap='round' |
| 151 | + strokeLinejoin='round' |
| 152 | + strokeWidth={2} |
| 153 | + d='M15 19l-7-7 7-7' |
| 154 | + /> |
| 155 | + </svg> |
| 156 | + </button> |
| 157 | + <h3 className='font-semibold text-gray-800 text-sm'>Settings</h3> |
| 158 | + </div> |
| 159 | + |
| 160 | + <div className='space-y-2 text-xs'> |
| 161 | + <div className='flex justify-between items-center'> |
| 162 | + <span className='text-gray-600'>Current Route:</span> |
| 163 | + <span className='font-mono text-gray-800'> |
| 164 | + {window.location.pathname} |
| 165 | + </span> |
| 166 | + </div> |
| 167 | + <div className='flex justify-between items-center'> |
| 168 | + <span className='text-gray-600'>React Router:</span> |
| 169 | + <span className='font-mono text-gray-800'>v7</span> |
| 170 | + </div> |
| 171 | + <div className='flex justify-between items-center'> |
| 172 | + <span className='text-gray-600'>Environment:</span> |
| 173 | + <span className='font-mono text-gray-800'> |
| 174 | + {import.meta.env.MODE} |
| 175 | + </span> |
| 176 | + </div> |
| 177 | + </div> |
| 178 | + |
| 179 | + <div className='pt-2 border-t border-gray-200 space-y-2'> |
| 180 | + <button |
| 181 | + onClick={() => { |
| 182 | + // Restart dev server functionality |
| 183 | + window.location.reload(); |
| 184 | + }} |
| 185 | + className='w-full px-3 py-2 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors' |
| 186 | + > |
| 187 | + Restart Dev Server |
| 188 | + </button> |
| 189 | + <button |
| 190 | + onClick={() => { |
| 191 | + // Clear cache functionality |
| 192 | + if ('caches' in window) { |
| 193 | + caches.keys().then(names => { |
| 194 | + names.forEach(name => caches.delete(name)); |
| 195 | + }); |
| 196 | + } |
| 197 | + window.location.reload(); |
| 198 | + }} |
| 199 | + className='w-full px-3 py-2 text-xs bg-orange-100 text-orange-700 rounded hover:bg-orange-200 transition-colors' |
| 200 | + > |
| 201 | + Clear Cache & Reload |
| 202 | + </button> |
| 203 | + </div> |
| 204 | + </div> |
| 205 | + ); |
218 | 206 |
|
219 | 207 | return ( |
220 | 208 | <> |
@@ -253,26 +241,10 @@ export function DevTools({ |
253 | 241 | </button> |
254 | 242 | </div> |
255 | 243 |
|
256 | | - {/* Tabs */} |
257 | | - <div className='flex border-b border-gray-200'> |
258 | | - {tabs.map(tab => ( |
259 | | - <button |
260 | | - key={tab.id} |
261 | | - onClick={() => setActiveTab(tab.id)} |
262 | | - className={`flex-1 px-2 py-1 text-xs font-medium transition-colors ${ |
263 | | - activeTab === tab.id |
264 | | - ? 'bg-blue-50 text-blue-600 border-b-2 border-blue-600' |
265 | | - : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' |
266 | | - }`} |
267 | | - > |
268 | | - <span className='mr-1'>{tab.icon}</span> |
269 | | - {tab.label} |
270 | | - </button> |
271 | | - ))} |
272 | | - </div> |
273 | | - |
274 | 244 | {/* Content */} |
275 | | - <div className='p-3'>{renderTabContent()}</div> |
| 245 | + <div className='p-3'> |
| 246 | + {currentView === 'main' ? renderMainView() : renderSettingsView()} |
| 247 | + </div> |
276 | 248 | </div> |
277 | 249 | )} |
278 | 250 | </> |
|
0 commit comments