EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1'use client';
2
3import { useState, useEffect } from 'react';
4import { useStore } from '@/contexts/StoreContext';
5import { Icon } from '@/contexts/IconContext';
6
7interface TestResult {
8 name: string;
9 url: string;
10 status: number | null;
11 success: boolean;
12 error?: string;
13 responseType?: string;
14 recordCount?: number;
15 duration?: number;
16}
17
18interface TestCategory {
19 name: string;
20 tests: Array<{
21 name: string;
22 path: string;
23 type: 'internal' | 'external-buck' | 'external-openapi' | 'external-fd1';
24 }>;
25}
26
27export default function ApiTestPage() {
28 const { session } = useStore();
29 const [testing, setTesting] = useState(false);
30 const [results, setResults] = useState<TestResult[]>([]);
31 const [currentTest, setCurrentTest] = useState<string>('');
32 const [filter, setFilter] = useState<'all' | 'success' | 'failed'>('all');
33
34 const testCategories: TestCategory[] = [
35 {
36 name: 'ELINK Sales',
37 tests: [
38 { name: 'Sales List', path: '/api/v1/elink/sales?limit=10', type: 'internal' },
39 { name: 'Sales Flat', path: '/api/v1/elink/sales/flat?limit=10', type: 'internal' },
40 { name: 'Sales Details (ID: 50000000)', path: '/api/v1/elink/sales/50000000', type: 'internal' },
41 { name: 'Sales Report', path: '/api/v1/elink/sales-report', type: 'internal' },
42 ],
43 },
44 {
45 name: 'ELINK Products',
46 tests: [
47 { name: 'Products List', path: '/api/v1/elink/products?limit=10', type: 'internal' },
48 { name: 'Product Details (ID: 1)', path: '/api/v1/elink/products/1', type: 'internal' },
49 { name: 'Printer Cartridge', path: '/api/v1/elink/printer-cartridge', type: 'internal' },
50 { name: 'Printers & Cartridges', path: '/api/v1/printers-cartridges', type: 'internal' },
51 ],
52 },
53 {
54 name: 'ELINK Customers',
55 tests: [
56 { name: 'Customers List', path: '/api/v1/elink/customers?limit=10', type: 'internal' },
57 { name: 'Customer Details (ID: 1)', path: '/api/v1/elink/customers/1', type: 'internal' },
58 ],
59 },
60 {
61 name: 'ELINK Staff & Org',
62 tests: [
63 { name: 'Employees', path: '/api/v1/elink/employees', type: 'internal' },
64 { name: 'Staff', path: '/api/v1/elink/staff', type: 'internal' },
65 { name: 'Staff Used', path: '/api/v1/elink/staff/used', type: 'internal' },
66 { name: 'Staff Rights', path: '/api/v1/elink/staff/rights', type: 'internal' },
67 { name: 'Staff Used Detail', path: '/api/v1/elink/staff/used/detail', type: 'internal' },
68 { name: 'Departments', path: '/api/v1/elink/departments', type: 'internal' },
69 { name: 'Locations', path: '/api/v1/elink/locations', type: 'internal' },
70 { name: 'Suppliers', path: '/api/v1/elink/suppliers', type: 'internal' },
71 { name: 'Topology', path: '/api/v1/elink/topology', type: 'internal' },
72 ],
73 },
74 {
75 name: 'ELINK Advisor',
76 tests: [
77 { name: 'Advisor', path: '/api/v1/elink/advisor', type: 'internal' },
78 { name: 'Duplicate Customers', path: '/api/v1/elink/advisor/duplicate-customers', type: 'internal' },
79 { name: 'Duplicate PLU', path: '/api/v1/elink/advisor/duplicate-plu', type: 'internal' },
80 { name: 'Products Priced Low', path: '/api/v1/elink/advisor/products-priced-low', type: 'internal' },
81 ],
82 },
83 {
84 name: 'FD1 Endpoints',
85 tests: [
86 { name: 'FD1 Products', path: '/api/v1/fd1/products?limit=10', type: 'internal' },
87 { name: 'FD1 Product (ID: 1)', path: '/api/v1/fd1/products/1', type: 'internal' },
88 { name: 'FD1 Sales', path: '/api/v1/fd1/sales?limit=10', type: 'internal' },
89 { name: 'FD1 Sale (ID: 50000000)', path: '/api/v1/fd1/sales/50000000', type: 'internal' },
90 { name: 'FD1 Customers', path: '/api/v1/fd1/customers?limit=10', type: 'internal' },
91 { name: 'FD1 Customer (ID: 1)', path: '/api/v1/fd1/customers/1', type: 'internal' },
92 ],
93 },
94 {
95 name: 'OpenAPI Products',
96 tests: [
97 { name: 'Products', path: '/api/v1/openapi/products?limit=10', type: 'internal' },
98 { name: 'Product (ID: 1)', path: '/api/v1/openapi/products/1', type: 'internal' },
99 { name: 'Products Search', path: '/api/v1/openapi/products-search?limit=10', type: 'internal' },
100 { name: 'Barcodes', path: '/api/v1/openapi/barcodes', type: 'internal' },
101 ],
102 },
103 {
104 name: 'OpenAPI Sales & Customers',
105 tests: [
106 { name: 'Sales', path: '/api/v1/openapi/sales?limit=10', type: 'internal' },
107 { name: 'Customers', path: '/api/v1/openapi/customers?limit=10', type: 'internal' },
108 { name: 'Suppliers', path: '/api/v1/openapi/suppliers?limit=10', type: 'internal' },
109 { name: 'Supplier (ID: 1)', path: '/api/v1/openapi/suppliers/1', type: 'internal' },
110 ],
111 },
112 {
113 name: 'OpenAPI Inventory',
114 tests: [
115 { name: 'Stocktakes', path: '/api/v1/openapi/stocktakes', type: 'internal' },
116 { name: 'Stock Movements', path: '/api/v1/openapi/stock2/movements', type: 'internal' },
117 { name: 'Stock Count', path: '/api/v1/openapi/stock2/count', type: 'internal' },
118 { name: 'Stock Count Summary', path: '/api/v1/openapi/stock2/count-summary', type: 'internal' },
119 { name: 'Reorder Levels', path: '/api/v1/openapi/reorder-levels', type: 'internal' },
120 { name: 'Supplier Part Codes', path: '/api/v1/openapi/supplier-part-codes', type: 'internal' },
121 { name: 'Stock Levels', path: '/api/v1/inventory/stock-levels', type: 'internal' },
122 { name: 'Inventory Movements', path: '/api/v1/inventory/movements', type: 'internal' },
123 { name: 'Stock Alerts', path: '/api/v1/products/stock-alerts', type: 'internal' },
124 ],
125 },
126 {
127 name: 'OpenAPI Purchase Orders',
128 tests: [
129 { name: 'Purchase Orders', path: '/api/v1/openapi/purchase-orders', type: 'internal' },
130 { name: 'PO Parse', path: '/api/v1/openapi/purchase-order/parse', type: 'internal' },
131 { name: 'PO Report', path: '/api/v1/reports/purchase-orders', type: 'internal' },
132 ],
133 },
134 {
135 name: 'OpenAPI Config',
136 tests: [
137 { name: 'Departments', path: '/api/v1/openapi/departments', type: 'internal' },
138 { name: 'Department (ID: 1)', path: '/api/v1/openapi/departments/1', type: 'internal' },
139 { name: 'Locations', path: '/api/v1/openapi/locations', type: 'internal' },
140 { name: 'Webgroups', path: '/api/v1/openapi/webgroups', type: 'internal' },
141 { name: 'Pricebook Specials', path: '/api/v1/openapi/pricebook-specials', type: 'internal' },
142 { name: 'Shelf Visits', path: '/api/v1/openapi/shelf-visits', type: 'internal' },
143 { name: 'Supplier Marketing', path: '/api/v1/openapi/supplier-marketing-messages', type: 'internal' },
144 ],
145 },
146 {
147 name: 'Stats',
148 tests: [
149 { name: 'Stats Summary', path: '/api/v1/stats', type: 'internal' },
150 { name: 'Stats Today', path: '/api/v1/stats/today', type: 'internal' },
151 { name: 'Hourly Sales', path: '/api/v1/stats/hourly-sales?startDate=2026-01-19&endDate=2026-01-20', type: 'internal' },
152 { name: 'Daily Sales', path: '/api/v1/stats/daily-sales?startDate=2026-01-13&endDate=2026-01-20', type: 'internal' },
153 { name: 'Top Products', path: '/api/v1/stats/top-products?startDate=2026-01-19&endDate=2026-01-20', type: 'internal' },
154 { name: 'Store Performance', path: '/api/v1/stats/store-performance?startDate=2026-01-19&endDate=2026-01-20', type: 'internal' },
155 { name: 'Today Payment', path: '/api/v1/stats/today-payment', type: 'internal' },
156 { name: 'Teller Performance', path: '/api/v1/stats/teller-performance?startDate=2026-01-19&endDate=2026-01-20', type: 'internal' },
157 ],
158 },
159 {
160 name: 'Sales',
161 tests: [
162 { name: 'Sales List (v1)', path: '/api/v1/sales?limit=10', type: 'internal' },
163 { name: 'Sales Recent', path: '/api/v1/sales/recent', type: 'internal' },
164 { name: 'Sales Stats', path: '/api/v1/sales/stats', type: 'internal' },
165 { name: 'Sales Totals', path: '/api/v1/sales/totals', type: 'internal' },
166 { name: 'Sales List (legacy)', path: '/api/v1/sales/list', type: 'internal' },
167 { name: 'Sales Search', path: '/api/v1/sales/search', type: 'internal' },
168 { name: 'Sales Picking', path: '/api/v1/sales/picking', type: 'internal' },
169 { name: 'Invoices', path: '/api/v1/invoices', type: 'internal' },
170 ],
171 },
172 {
173 name: 'Reports',
174 tests: [
175 { name: 'Sales Totals', path: '/api/v1/reports/sales-totals', type: 'internal' },
176 { name: 'Sales Detail', path: '/api/v1/reports/sales-detail', type: 'internal' },
177 { name: 'Sales Cube', path: '/api/v1/reports/sales-cube', type: 'internal' },
178 { name: 'Sales Grouped', path: '/api/v1/reports/sales-grouped', type: 'internal' },
179 { name: 'Trading Summary', path: '/api/v1/reports/trading-summary', type: 'internal' },
180 { name: 'Product Performance', path: '/api/v1/reports/product-performance', type: 'internal' },
181 { name: 'Duplicate Descriptions', path: '/api/v1/reports/advisor/duplicate-descriptions', type: 'internal' },
182 ],
183 },
184 {
185 name: 'Analytics',
186 tests: [
187 { name: 'Pareto', path: '/api/v1/analytics/pareto', type: 'internal' },
188 { name: 'Product Performance', path: '/api/v1/analytics/product-performance', type: 'internal' },
189 ],
190 },
191 {
192 name: 'Accounts',
193 tests: [
194 { name: 'Accounts', path: '/api/v1/accounts', type: 'internal' },
195 { name: 'Customer Accounts', path: '/api/v1/accounts/customers', type: 'internal' },
196 { name: 'Financial Accounts', path: '/api/v1/accounts/financial', type: 'internal' },
197 { name: 'Account Sales', path: '/api/v1/accounts/sales', type: 'internal' },
198 ],
199 },
200 {
201 name: 'Config & Settings',
202 tests: [
203 { name: 'Base Settings', path: '/api/v1/config/base-settings', type: 'internal' },
204 { name: 'Departments', path: '/api/v1/departments', type: 'internal' },
205 { name: 'Department Hierarchy', path: '/api/v1/departments/hierarchy', type: 'internal' },
206 { name: 'Locations', path: '/api/v1/locations', type: 'internal' },
207 { name: 'Staff', path: '/api/v1/staff', type: 'internal' },
208 { name: 'Payment Types', path: '/api/v1/payment-types', type: 'internal' },
209 { name: 'Discounts', path: '/api/v1/discounts', type: 'internal' },
210 { name: 'Price Changes', path: '/api/v1/price-changes', type: 'internal' },
211 ],
212 },
213 {
214 name: 'BUCK',
215 tests: [
216 { name: 'Stocktakes', path: '/api/v1/buck/stocktakes', type: 'internal' },
217 { name: 'Stocktake Status', path: '/api/v1/buck/stocktake/status', type: 'internal' },
218 { name: 'Serial Items', path: '/api/v1/buck/serial-items', type: 'internal' },
219 { name: 'Loyalty Campaigns', path: '/api/v1/buck/loyalty/campaigns', type: 'internal' },
220 ],
221 },
222 {
223 name: 'Messaging',
224 tests: [
225 { name: 'Config', path: '/api/v1/messaging/config', type: 'internal' },
226 { name: 'SMS Send Log', path: '/api/v1/messaging/sms/sendlog', type: 'internal' },
227 { name: 'Email Send Log', path: '/api/v1/messaging/email/sendlog', type: 'internal' },
228 ],
229 },
230 {
231 name: 'Other',
232 tests: [
233 { name: 'Stocktake', path: '/api/v1/stocktake', type: 'internal' },
234 { name: 'Contact Logs', path: '/api/v1/contact-logs', type: 'internal' },
235 { name: 'Loyalty Campaigns', path: '/api/v1/loyalty/campaigns', type: 'internal' },
236 { name: 'Printing Pending', path: '/api/v1/printing/pending', type: 'internal' },
237 { name: 'Dashboard', path: '/api/dashboard', type: 'internal' },
238 { name: 'POS Config', path: '/api/pos/config', type: 'internal' },
239 { name: 'CWA Status', path: '/api/cwa/status', type: 'internal' },
240 { name: 'CWA Locations', path: '/api/cwa/locations', type: 'internal' },
241 { name: 'CWA Stats Today', path: '/api/cwa/stats/today', type: 'internal' },
242 { name: 'CWA Sales Totals', path: '/api/cwa/sales/totals', type: 'internal' },
243 ],
244 },
245 ];
246
247 const runTests = async () => {
248 setTesting(true);
249 setResults([]);
250 const newResults: TestResult[] = [];
251
252 for (const category of testCategories) {
253 for (const test of category.tests) {
254 setCurrentTest(`${category.name} - ${test.name}`);
255
256 try {
257 const startTime = Date.now();
258 const response = await fetch(test.path);
259 const duration = Date.now() - startTime;
260
261 let responseData;
262 let responseType = 'unknown';
263 let recordCount;
264
265 const contentType = response.headers.get('content-type');
266 if (contentType?.includes('application/json')) {
267 responseData = await response.json();
268 responseType = 'json';
269
270 // Try to detect record count
271 if (responseData.data && Array.isArray(responseData.data)) {
272 recordCount = responseData.data.length;
273 } else if (responseData.DATS && Array.isArray(responseData.DATS)) {
274 recordCount = responseData.DATS.length;
275 } else if (Array.isArray(responseData)) {
276 recordCount = responseData.length;
277 }
278 } else {
279 responseData = await response.text();
280 responseType = 'text';
281 }
282
283 const result: TestResult = {
284 name: `${category.name} - ${test.name}`,
285 url: test.path,
286 status: response.status,
287 success: response.status === 200,
288 responseType,
289 recordCount,
290 duration,
291 };
292
293 if (!result.success) {
294 result.error = typeof responseData === 'string'
295 ? responseData.substring(0, 200)
296 : JSON.stringify(responseData).substring(0, 200);
297 }
298
299 newResults.push(result);
300 } catch (error) {
301 newResults.push({
302 name: `${category.name} - ${test.name}`,
303 url: test.path,
304 status: null,
305 success: false,
306 error: error instanceof Error ? error.message : 'Unknown error',
307 });
308 }
309
310 setResults([...newResults]);
311 }
312 }
313
314 setTesting(false);
315 setCurrentTest('');
316 };
317
318 const successCount = results.filter(r => r.success).length;
319 const failedCount = results.filter(r => !r.success).length;
320 const totalTests = testCategories.reduce((acc, cat) => acc + cat.tests.length, 0);
321
322 const filteredResults = results.filter(r => {
323 if (filter === 'all') return true;
324 if (filter === 'success') return r.success;
325 if (filter === 'failed') return !r.success;
326 return true;
327 });
328
329 const exportToCSV = () => {
330 const headers = ['Test Name', 'Endpoint', 'Status', 'Success', 'Response Type', 'Record Count', 'Duration (ms)', 'Error'];
331 const rows = results.map(r => [
332 r.name,
333 r.url,
334 r.status || 'N/A',
335 r.success ? 'Yes' : 'No',
336 r.responseType || 'N/A',
337 r.recordCount || 'N/A',
338 r.duration || 'N/A',
339 r.error || ''
340 ]);
341
342 const csvContent = [
343 headers.join(','),
344 ...rows.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(','))
345 ].join('\n');
346
347 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
348 const link = document.createElement('a');
349 const url = URL.createObjectURL(blob);
350 const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
351 const storeName = session?.store?.id || 'unknown';
352
353 link.setAttribute('href', url);
354 link.setAttribute('download', `api-test-results-${storeName}-${timestamp}.csv`);
355 link.style.visibility = 'hidden';
356 document.body.appendChild(link);
357 link.click();
358 document.body.removeChild(link);
359 };
360
361 const exportToJSON = () => {
362 const exportData = {
363 metadata: {
364 exportDate: new Date().toISOString(),
365 store: session?.store?.name || 'N/A',
366 storeId: session?.store?.id || 'N/A',
367 user: session?.user?.name || 'N/A',
368 role: session?.user?.role || 'N/A',
369 totalTests: results.length,
370 successCount,
371 failedCount
372 },
373 results: results
374 };
375
376 const jsonContent = JSON.stringify(exportData, null, 2);
377 const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' });
378 const link = document.createElement('a');
379 const url = URL.createObjectURL(blob);
380 const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
381 const storeName = session?.store?.id || 'unknown';
382
383 link.setAttribute('href', url);
384 link.setAttribute('download', `api-test-results-${storeName}-${timestamp}.json`);
385 link.style.visibility = 'hidden';
386 document.body.appendChild(link);
387 link.click();
388 document.body.removeChild(link);
389 };
390
391 return (
392 <div className="max-w-7xl mx-auto p-6">
393 <div className="mb-6">
394 <h1 className="text-3xl font-bold text-text mb-2">API Endpoint Tester</h1>
395 <p className="text-muted">
396 Test all API endpoints with your current session ({session?.store?.id || 'Unknown'})
397 </p>
398 </div>
399
400 {/* Session Info */}
401 <div className="bg-surface rounded-lg border border-border p-4 mb-6">
402 <h2 className="text-lg font-semibold text-text mb-3 flex items-center gap-2">
403 <Icon name="account_circle" size={20} />
404 Session Information
405 </h2>
406 <div className="grid grid-cols-2 gap-4 text-sm">
407 <div>
408 <span className="text-muted">Store:</span>{' '}
409 <span className="text-text font-medium">{session?.store?.name || 'N/A'}</span>
410 </div>
411 <div>
412 <span className="text-muted">Store ID:</span>{' '}
413 <span className="text-text font-medium">{session?.store?.id || 'N/A'}</span>
414 </div>
415 <div>
416 <span className="text-muted">User:</span>{' '}
417 <span className="text-text font-medium">{session?.user?.name || 'N/A'}</span>
418 </div>
419 <div>
420 <span className="text-muted">Role:</span>{' '}
421 <span className="text-text font-mono text-xs">
422 {session?.user?.role || 'N/A'}
423 </span>
424 </div>
425 </div>
426 </div>
427
428 {/* Test Controls */}
429 <div className="bg-surface rounded-lg border border-border p-4 mb-6">
430 <div className="flex items-center justify-between mb-4">
431 <h2 className="text-lg font-semibold text-text flex items-center gap-2">
432 <Icon name="science" size={20} />
433 Test Controls
434 </h2>
435 <button
436 onClick={runTests}
437 disabled={testing}
438 className="px-4 py-2 bg-brand text-surface rounded hover:bg-brand/80 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
439 >
440 {testing ? (
441 <>
442 <Icon name="hourglass_empty" size={16} className="animate-spin" />
443 Testing...
444 </>
445 ) : (
446 <>
447 <Icon name="play_arrow" size={16} />
448 Run All Tests
449 </>
450 )}
451 </button>
452 </div>
453
454 {currentTest && (
455 <div className="text-sm text-muted flex items-center gap-2">
456 <Icon name="hourglass_empty" size={14} className="animate-spin" />
457 Currently testing: {currentTest}
458 </div>
459 )}
460
461 {results.length > 0 && (
462 <div className="mt-4 pt-4 border-t border-border">
463 <div className="grid grid-cols-3 gap-4 text-sm mb-4">
464 <div className="bg-success/10 rounded p-3 border border-success/20">
465 <div className="text-success text-2xl font-bold">{successCount}</div>
466 <div className="text-muted">Successful</div>
467 </div>
468 <div className="bg-danger/10 rounded p-3 border border-danger/20">
469 <div className="text-danger text-2xl font-bold">{failedCount}</div>
470 <div className="text-muted">Failed</div>
471 </div>
472 <div className="bg-info/10 rounded p-3 border border-info/20">
473 <div className="text-info text-2xl font-bold">{totalTests}</div>
474 <div className="text-muted">Total Tests</div>
475 </div>
476 </div>
477
478 <div className="flex items-center justify-between gap-2">
479 <div className="flex gap-2">
480 <button
481 onClick={() => setFilter('all')}
482 className={`px-3 py-1 rounded text-sm ${
483 filter === 'all'
484 ? 'bg-brand text-surface'
485 : 'bg-surface-2 text-muted hover:bg-surface-3'
486 }`}
487 >
488 All ({results.length})
489 </button>
490 <button
491 onClick={() => setFilter('success')}
492 className={`px-3 py-1 rounded text-sm ${
493 filter === 'success'
494 ? 'bg-success text-surface'
495 : 'bg-surface-2 text-muted hover:bg-surface-3'
496 }`}
497 >
498 Success ({successCount})
499 </button>
500 <button
501 onClick={() => setFilter('failed')}
502 className={`px-3 py-1 rounded text-sm ${
503 filter === 'failed'
504 ? 'bg-danger text-surface'
505 : 'bg-surface-2 text-muted hover:bg-surface-3'
506 }`}
507 >
508 Failed ({failedCount})
509 </button>
510 </div>
511
512 <div className="flex gap-2">
513 <button
514 onClick={exportToCSV}
515 className="px-3 py-1 rounded text-sm bg-surface-2 text-text hover:bg-surface-3 flex items-center gap-1"
516 title="Export as CSV"
517 >
518 <Icon name="download" size={14} />
519 CSV
520 </button>
521 <button
522 onClick={exportToJSON}
523 className="px-3 py-1 rounded text-sm bg-surface-2 text-text hover:bg-surface-3 flex items-center gap-1"
524 title="Export as JSON"
525 >
526 <Icon name="download" size={14} />
527 JSON
528 </button>
529 </div>
530 </div>
531 </div>
532 )}
533 </div>
534
535 {/* Results */}
536 {filteredResults.length > 0 && (
537 <div className="bg-surface rounded-lg border border-border overflow-hidden">
538 <div className="overflow-x-auto">
539 <table className="w-full">
540 <thead className="bg-surface-2 border-b border-border">
541 <tr>
542 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase">Status</th>
543 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase">Test Name</th>
544 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase">Endpoint</th>
545 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase">Response</th>
546 <th className="px-4 py-3 text-left text-xs font-semibold text-muted uppercase">Duration</th>
547 </tr>
548 </thead>
549 <tbody className="divide-y divide-border">
550 {filteredResults.map((result, index) => (
551 <tr key={index} className="hover:bg-surface-2/50">
552 <td className="px-4 py-3 whitespace-nowrap">
553 {result.success ? (
554 <span className="inline-flex items-center gap-1 px-2 py-1 bg-success/20 text-success rounded text-xs font-semibold">
555 <Icon name="check_circle" size={14} />
556 {result.status}
557 </span>
558 ) : (
559 <span className="inline-flex items-center gap-1 px-2 py-1 bg-danger/20 text-danger rounded text-xs font-semibold">
560 <Icon name="error" size={14} />
561 {result.status || 'ERR'}
562 </span>
563 )}
564 </td>
565 <td className="px-4 py-3 text-sm text-text">{result.name}</td>
566 <td className="px-4 py-3 text-xs font-mono text-muted">{result.url}</td>
567 <td className="px-4 py-3 text-sm">
568 {result.success ? (
569 <div className="text-success">
570 {result.responseType === 'json' && result.recordCount !== undefined && (
571 <span>{result.recordCount} records</span>
572 )}
573 {result.responseType === 'json' && result.recordCount === undefined && (
574 <span>JSON response</span>
575 )}
576 {result.responseType === 'text' && <span>Text response</span>}
577 </div>
578 ) : (
579 <div className="text-danger text-xs max-w-xs truncate" title={result.error}>
580 {result.error}
581 </div>
582 )}
583 </td>
584 <td className="px-4 py-3 text-sm text-muted">
585 {result.duration ? `${result.duration}ms` : '-'}
586 </td>
587 </tr>
588 ))}
589 </tbody>
590 </table>
591 </div>
592 </div>
593 )}
594 </div>
595 );
596}