|
1 | 1 | import { TraceGenerationRequest } from '../../backend/shared/types'; |
2 | | -import { Info, Plus, Send, Trash2 } from 'lucide-react'; |
3 | | -import React, { useState } from 'react'; |
| 2 | +import { Info, Plus, Play, Send, Square, Trash2 } from 'lucide-react'; |
| 3 | +import React, { useEffect, useRef, useState } from 'react'; |
4 | 4 | import { apiClient } from '../api/client'; |
5 | 5 | import { ResetButton } from './ResetButton'; |
6 | 6 |
|
@@ -38,6 +38,9 @@ export function TraceGenerationForm({ onError, onSuccess }: TraceGenerationFormP |
38 | 38 | ); |
39 | 39 | const [nextAttrId, setNextAttrId] = useState(DEFAULT_CUSTOM_ATTRIBUTES.length + 1); |
40 | 40 | const [loading, setLoading] = useState(false); |
| 41 | + const [isRepeating, setIsRepeating] = useState(false); |
| 42 | + const [intervalMs, setIntervalMs] = useState(1000); |
| 43 | + const intervalRef = useRef<NodeJS.Timeout | null>(null); |
41 | 44 |
|
42 | 45 | const updateValue = <K extends keyof typeof formData>(field: K, value: (typeof formData)[K]) => { |
43 | 46 | setFormData((prev) => ({ ...prev, [field]: value })); |
@@ -78,27 +81,66 @@ export function TraceGenerationForm({ onError, onSuccess }: TraceGenerationFormP |
78 | 81 | } |
79 | 82 | }; |
80 | 83 |
|
81 | | - const handleSubmit = async (e: React.FormEvent) => { |
82 | | - e.preventDefault(); |
83 | | - setLoading(true); |
| 84 | + const stopRepeating = () => { |
| 85 | + setIsRepeating(false); |
| 86 | + if (intervalRef.current) { |
| 87 | + clearInterval(intervalRef.current); |
| 88 | + intervalRef.current = null; |
| 89 | + } |
| 90 | + }; |
84 | 91 |
|
85 | | - try { |
86 | | - const customAttrs: Record<string, string> = {}; |
87 | | - customAttributes.forEach((attr) => { |
88 | | - if (attr.key.trim() && attr.value.trim()) { |
89 | | - customAttrs[attr.key.trim()] = attr.value.trim(); |
90 | | - } |
91 | | - }); |
| 92 | + const sendTrace = async () => { |
| 93 | + const customAttrs: Record<string, string> = {}; |
| 94 | + customAttributes.forEach((attr) => { |
| 95 | + if (attr.key.trim() && attr.value.trim()) { |
| 96 | + customAttrs[attr.key.trim()] = attr.value.trim(); |
| 97 | + } |
| 98 | + }); |
92 | 99 |
|
93 | | - const request: TraceGenerationRequest = { |
94 | | - ...formData, |
95 | | - customAttributes: customAttrs, |
96 | | - }; |
| 100 | + const request: TraceGenerationRequest = { |
| 101 | + ...formData, |
| 102 | + customAttributes: customAttrs, |
| 103 | + }; |
97 | 104 |
|
| 105 | + try { |
98 | 106 | await apiClient.generateTrace(request); |
99 | 107 | onSuccess?.('Trace generated and sent successfully!'); |
100 | 108 | } catch (err: any) { |
101 | 109 | onError(err.message || 'Failed to generate trace'); |
| 110 | + // Stop repeating on error |
| 111 | + stopRepeating(); |
| 112 | + } |
| 113 | + }; |
| 114 | + |
| 115 | + const startRepeating = () => { |
| 116 | + if (intervalMs < 100) { |
| 117 | + onError('Interval must be at least 100ms'); |
| 118 | + return; |
| 119 | + } |
| 120 | + setIsRepeating(true); |
| 121 | + // Send immediately |
| 122 | + sendTrace(); |
| 123 | + // Then set up interval |
| 124 | + intervalRef.current = setInterval(() => { |
| 125 | + sendTrace(); |
| 126 | + }, intervalMs); |
| 127 | + }; |
| 128 | + |
| 129 | + // Cleanup interval on unmount |
| 130 | + useEffect(() => { |
| 131 | + return () => { |
| 132 | + if (intervalRef.current) { |
| 133 | + clearInterval(intervalRef.current); |
| 134 | + } |
| 135 | + }; |
| 136 | + }, []); |
| 137 | + |
| 138 | + const handleSubmit = async (e: React.FormEvent) => { |
| 139 | + e.preventDefault(); |
| 140 | + setLoading(true); |
| 141 | + |
| 142 | + try { |
| 143 | + await sendTrace(); |
102 | 144 | } finally { |
103 | 145 | setLoading(false); |
104 | 146 | } |
@@ -362,10 +404,84 @@ export function TraceGenerationForm({ onError, onSuccess }: TraceGenerationFormP |
362 | 404 | </div> |
363 | 405 | </div> |
364 | 406 |
|
365 | | - <button type="submit" disabled={loading} className="material-button w-full md:w-auto flex items-center gap-2"> |
| 407 | + <button |
| 408 | + type="submit" |
| 409 | + disabled={loading || isRepeating} |
| 410 | + className="material-button w-full md:w-auto flex items-center gap-2" |
| 411 | + > |
366 | 412 | <Send className="w-5 h-5" /> |
367 | | - Generate and Send Trace |
| 413 | + {isRepeating ? 'Stop Repeating First' : 'Generate and Send Trace'} |
368 | 414 | </button> |
| 415 | + |
| 416 | + {/* Repeated Sending Settings */} |
| 417 | + <div> |
| 418 | + <h3 className="text-xl font-bold text-primary mb-4">Repeated Sending</h3> |
| 419 | + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| 420 | + <div className="space-y-2"> |
| 421 | + <label className="flex items-center gap-2 text-sm font-medium text-primary"> |
| 422 | + Interval (ms) |
| 423 | + <div className="group relative"> |
| 424 | + <span className="info-icon"> |
| 425 | + <Info className="w-4 h-4" /> |
| 426 | + </span> |
| 427 | + <span className="tooltip w-64">Time interval between trace sends in milliseconds (minimum 100ms)</span> |
| 428 | + </div> |
| 429 | + </label> |
| 430 | + <div className="range-slider-container"> |
| 431 | + <input |
| 432 | + type="range" |
| 433 | + min="100" |
| 434 | + max="60000" |
| 435 | + step="100" |
| 436 | + value={Math.min(Math.max(intervalMs, 100), 60000)} |
| 437 | + onChange={(e) => setIntervalMs(parseInt(e.target.value))} |
| 438 | + disabled={isRepeating} |
| 439 | + /> |
| 440 | + <input |
| 441 | + type="number" |
| 442 | + value={intervalMs} |
| 443 | + onChange={(e) => { |
| 444 | + const value = parseInt(e.target.value, 10); |
| 445 | + if (!isNaN(value) && value >= 100) { |
| 446 | + setIntervalMs(value); |
| 447 | + } |
| 448 | + }} |
| 449 | + className="material-input text-center" |
| 450 | + min="100" |
| 451 | + disabled={isRepeating} |
| 452 | + /> |
| 453 | + </div> |
| 454 | + </div> |
| 455 | + <div className="flex items-end gap-3"> |
| 456 | + {!isRepeating ? ( |
| 457 | + <button |
| 458 | + type="button" |
| 459 | + onClick={startRepeating} |
| 460 | + disabled={loading || intervalMs < 100} |
| 461 | + className="material-button flex items-center gap-2" |
| 462 | + > |
| 463 | + <Play className="w-5 h-5" /> |
| 464 | + Start Repeating |
| 465 | + </button> |
| 466 | + ) : ( |
| 467 | + <button |
| 468 | + type="button" |
| 469 | + onClick={stopRepeating} |
| 470 | + className="material-button bg-danger hover:bg-danger-dark flex items-center gap-2" |
| 471 | + > |
| 472 | + <Square className="w-5 h-5" /> |
| 473 | + Stop Repeating |
| 474 | + </button> |
| 475 | + )} |
| 476 | + {isRepeating && ( |
| 477 | + <div className="flex items-center gap-2 text-sm text-primary"> |
| 478 | + <div className="w-2 h-2 bg-danger rounded-full animate-pulse"></div> |
| 479 | + <span>Sending every {intervalMs}ms</span> |
| 480 | + </div> |
| 481 | + )} |
| 482 | + </div> |
| 483 | + </div> |
| 484 | + </div> |
369 | 485 | </form> |
370 | 486 | ); |
371 | 487 | } |
0 commit comments