ntropi 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,428 +0,0 @@
1
- import { useState } from 'react'
2
- import { useConfigState } from '../context/ConfigState'
3
- import DatasetEditor from './DatasetEditor'
4
- import { Activity, Plus, Clock, AlertCircle, Edit2, Trash2, Database, Bot, Copy, ExternalLink } from 'lucide-react'
5
- // eslint-disable-next-line no-unused-vars
6
- import { motion, AnimatePresence } from 'framer-motion'
7
-
8
- export default function ConfigVisualizer() {
9
- const { endpoints, setEndpoints, parsedResult } = useConfigState()
10
- const [isModalOpen, setIsModalOpen] = useState(false)
11
- const [isDrawerOpen, setIsDrawerOpen] = useState(false)
12
- const [selectedEndpointIndex, setSelectedEndpointIndex] = useState(null)
13
- const [formData, setFormData] = useState({
14
- method: 'GET',
15
- path: '',
16
- delay: 0,
17
- failureRate: 0,
18
- })
19
-
20
- const handleDelayScroll = (event, index) => {
21
- event.preventDefault()
22
- const delta = event.deltaY > 0 ? -50 : 50
23
- updateDelay(index, delta)
24
- }
25
-
26
- const updateDelay = (index, delta) => {
27
- setEndpoints((previousEndpoints) =>
28
- previousEndpoints.map((endpoint, currentIndex) =>
29
- currentIndex === index
30
- ? {
31
- ...endpoint,
32
- delay: Math.max(0, Math.round((endpoint.delay ?? 0) + delta)),
33
- }
34
- : endpoint
35
- )
36
- )
37
- }
38
-
39
- const handleFailureRateScroll = (event, index) => {
40
- event.preventDefault()
41
- const delta = event.deltaY > 0 ? -0.05 : 0.05
42
- updateFailureRate(index, delta)
43
- }
44
-
45
- const updateFailureRate = (index, delta) => {
46
- setEndpoints((previousEndpoints) =>
47
- previousEndpoints.map((endpoint, currentIndex) =>
48
- currentIndex === index
49
- ? {
50
- ...endpoint,
51
- failureRate:
52
- Math.max(
53
- 0,
54
- Math.min(100, Math.round(((endpoint.failureRate ?? 0) + delta) * 100))
55
- ) / 100,
56
- }
57
- : endpoint
58
- )
59
- )
60
- }
61
-
62
- const handleOpenModal = () => {
63
- setIsModalOpen(true)
64
- }
65
-
66
- const handleCloseModal = () => {
67
- setIsModalOpen(false)
68
- setFormData({
69
- method: 'GET',
70
- path: '',
71
- delay: 0,
72
- failureRate: 0,
73
- })
74
- }
75
-
76
- const handleFormChange = (event) => {
77
- const { name, value } = event.target
78
- setFormData((previous) => ({
79
- ...previous,
80
- [name]:
81
- name === 'delay'
82
- ? Math.max(0, parseInt(value, 10) || 0)
83
- : name === 'failureRate'
84
- ? Math.max(0, Math.min(100, parseInt(value, 10) || 0))
85
- : value,
86
- }))
87
- }
88
-
89
- const handleAddEndpoint = (event) => {
90
- event.preventDefault()
91
-
92
- if (!parsedResult.isValid) {
93
- alert('Fix invalid JSON in the editor before adding endpoints')
94
- return
95
- }
96
-
97
- if (!formData.path.trim()) {
98
- alert('Please enter a path')
99
- return
100
- }
101
-
102
- let endpointPath = formData.path.trim()
103
- if (!endpointPath.startsWith('/api/')) {
104
- endpointPath = `/api/${endpointPath}`
105
- }
106
-
107
- const newEndpoint = {
108
- path: endpointPath,
109
- method: formData.method,
110
- data: ["Response from " + endpointPath.replace('/api/', ''), ],
111
- delay: Math.max(0, Math.round(formData.delay)),
112
- failureRate: Math.max(0, Math.min(100, Math.round(formData.failureRate))),
113
- }
114
-
115
- setEndpoints((previousEndpoints) => [...previousEndpoints, newEndpoint])
116
- handleCloseModal()
117
- }
118
-
119
- const handleDeleteEndpoint = (index) => {
120
- if (selectedEndpointIndex === index) {
121
- setIsDrawerOpen(false)
122
- setSelectedEndpointIndex(null)
123
- }
124
-
125
- if (selectedEndpointIndex !== null && selectedEndpointIndex > index) {
126
- setSelectedEndpointIndex((previous) => previous - 1)
127
- }
128
-
129
- setEndpoints((previousEndpoints) => previousEndpoints.filter((_, currentIndex) => currentIndex !== index))
130
- }
131
-
132
- const handleOpenDrawer = (index) => {
133
- setSelectedEndpointIndex(index)
134
- setIsDrawerOpen(true)
135
- }
136
-
137
- const handleCloseDrawer = () => {
138
- setIsDrawerOpen(false)
139
- setSelectedEndpointIndex(null)
140
- }
141
-
142
- const handleUpdateEndpointField = (index, field, value) => {
143
- setEndpoints((previousEndpoints) =>
144
- previousEndpoints.map((endpoint, currentIndex) =>
145
- currentIndex === index
146
- ? {
147
- ...endpoint,
148
- [field]: value,
149
- }
150
- : endpoint
151
- )
152
- )
153
- }
154
-
155
- return (
156
- <section className="flex flex-col gap-6">
157
- {/* Overview header stats or simple title */}
158
- <div className="flex items-center justify-between mt-2">
159
- <div className="flex items-center gap-3">
160
- <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-500/10 text-indigo-400 border border-indigo-500/20">
161
- <Activity size={20} />
162
- </div>
163
- <div>
164
- <h2 className="text-xl font-semibold tracking-tight text-white leading-tight">Live Endpoints</h2>
165
- <p className="text-sm text-neutral-400 font-medium">Manage mocked routes and error rates</p>
166
- </div>
167
- </div>
168
- <button
169
- type="button"
170
- onClick={handleOpenModal}
171
- disabled={!parsedResult.isValid}
172
- className="flex items-center gap-2 rounded-xl bg-white text-black px-4 py-2 text-sm font-semibold transition-transform hover:scale-105 active:scale-95 disabled:pointer-events-none disabled:opacity-50"
173
- >
174
- <Plus size={16} />
175
- <span>Add Route</span>
176
- </button>
177
- </div>
178
-
179
- {/* Endpoints grid */}
180
- {endpoints.length === 0 ? (
181
- <div className="flex flex-col items-center justify-center rounded-2xl border border-dashed border-white/10 bg-[#0A0A0A] p-12 text-center text-neutral-400 mt-4">
182
- <div className="flex h-12 w-12 items-center justify-center rounded-full bg-neutral-900 mb-4 border border-white/5">
183
- <Activity size={24} className="text-neutral-500" />
184
- </div>
185
- <h3 className="text-base font-medium text-white mb-1">No Endpoints Mocked</h3>
186
- <p className="text-sm px-8 max-w-sm mb-6 leading-relaxed">Create realistic JSON endpoints instantly. Configure simulated delays and random network failures to stress-test your application's reliability.</p>
187
-
188
- <button
189
- type="button"
190
- onClick={handleOpenModal}
191
- disabled={!parsedResult.isValid}
192
- className="flex items-center gap-2 rounded-lg bg-indigo-500/10 text-indigo-400 px-5 py-2.5 text-sm font-semibold transition-colors hover:bg-indigo-500/20 disabled:pointer-events-none disabled:opacity-50"
193
- >
194
- <Plus size={16} />
195
- <span>Create First Mock Route</span>
196
- </button>
197
- </div>
198
- ) : (
199
- <div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
200
- <AnimatePresence>
201
- {endpoints.map((endpoint, index) => {
202
- const methodColor = {
203
- GET: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
204
- POST: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
205
- PUT: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
206
- PATCH: 'bg-orange-500/10 text-orange-400 border-orange-500/20',
207
- DELETE: 'bg-red-500/10 text-red-400 border-red-500/20',
208
- }[endpoint.method] || 'bg-neutral-500/10 text-neutral-400 border-neutral-500/20'
209
-
210
- return (
211
- <motion.div
212
- key={index}
213
- initial={{ opacity: 0, y: 10 }}
214
- animate={{ opacity: 1, y: 0 }}
215
- exit={{ opacity: 0, scale: 0.95 }}
216
- transition={{ duration: 0.2 }}
217
- className="group relative flex flex-col rounded-xl border border-white/10 bg-[#080808] p-5 transition-all hover:border-white/20 hover:bg-white/2 shadow-xl shadow-black/50"
218
- >
219
- {/* Endpoint Header */}
220
- <div className="flex items-start justify-between">
221
- <div className="flex items-center gap-3 overflow-hidden">
222
- <span className={`rounded-md px-2.5 py-1 text-[11px] uppercase tracking-wider font-bold border ${methodColor}`}>
223
- {endpoint.method}
224
- </span>
225
- <span className="truncate font-mono text-[15px] font-medium text-neutral-200" title={endpoint.path}>
226
- {endpoint.path}
227
- </span>
228
- </div>
229
- <div className="flex items-center gap-2">
230
- <button
231
- type="button"
232
- onClick={() => {
233
- window.open(`http://localhost:8008${endpoint.path}`, '_blank', 'noopener,noreferrer');
234
- }}
235
- className="rounded-md border border-neutral-500/20 bg-neutral-500/5 p-1.5 text-neutral-400 transition-colors hover:bg-neutral-500/20 hover:text-neutral-300"
236
- title="Open API Link"
237
- >
238
- <ExternalLink size={15} />
239
- </button>
240
- <button
241
- type="button"
242
- onClick={() => handleOpenDrawer(index)}
243
- className="flex items-center gap-1.5 rounded-md border border-indigo-500/20 bg-indigo-500/5 px-2.5 py-1.5 text-xs font-semibold text-indigo-400 transition-colors hover:bg-indigo-500/20 hover:text-indigo-300"
244
- title="Edit Custom Dataset"
245
- >
246
- <Database size={13} />
247
- <span>Dataset</span>
248
- </button>
249
- <button
250
- type="button"
251
- onClick={() => handleDeleteEndpoint(index)}
252
- className="rounded-md border border-red-500/20 bg-red-500/5 p-1.5 text-red-500/70 transition-colors hover:bg-red-500/20 hover:text-red-400"
253
- title="Delete Route"
254
- >
255
- <Trash2 size={15} />
256
- </button>
257
- </div>
258
- </div>
259
-
260
- {/* Endpoint Config Controllers */}
261
- <div className="mt-5 grid grid-cols-2 gap-6 border-t border-white/5 pt-5 cursor-default">
262
- <div className="flex flex-col gap-2">
263
- <span className="flex items-center gap-1.5 text-[11px] uppercase tracking-wider font-semibold text-neutral-500">
264
- <Clock size={12} /> Delay
265
- </span>
266
- <div className="flex items-center gap-2 group/delay relative">
267
- <input
268
- type="number"
269
- min="0"
270
- step="50"
271
- value={endpoint.delay || 0}
272
- onWheel={(e) => handleDelayScroll(e, index)}
273
- onChange={(e) => {
274
- const val = Math.max(0, parseInt(e.target.value, 10) || 0);
275
- setEndpoints((prev) => prev.map((ep, i) => i === index ? { ...ep, delay: val } : ep));
276
- }}
277
- className="w-full rounded-md border border-white/10 bg-[#121212] px-3 py-1.5 text-sm tabular-nums text-neutral-200 outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/50 transition-all hover:bg-[#1a1a1a]"
278
- />
279
- <span className="absolute right-3 text-xs text-neutral-600 font-medium pointer-events-none">ms</span>
280
- </div>
281
- </div>
282
-
283
- <div className="flex flex-col gap-2.5">
284
- <div className="flex items-center justify-between">
285
- <span className="flex items-center gap-1.5 text-[11px] uppercase tracking-wider font-semibold text-neutral-500">
286
- <AlertCircle size={12} className={(endpoint.failureRate || 0) > 0 ? "text-amber-500" : ""} /> Failure Rate
287
- </span>
288
- <span className="text-xs font-semibold text-neutral-300 tabular-nums bg-[#121212] px-2 py-0.5 rounded border border-white/5">{Math.round((endpoint.failureRate || 0))}%</span>
289
- </div>
290
- <div className="flex items-center h-8.5">
291
- <input
292
- type="range"
293
- min="0"
294
- max="100"
295
- step="5"
296
- value={Math.round((endpoint.failureRate || 0))}
297
- onWheel={(e) => handleFailureRateScroll(e, index)}
298
- onChange={(e) => {
299
- const val = parseInt(e.target.value, 10);
300
- setEndpoints((prev) => prev.map((ep, i) => i === index ? { ...ep, failureRate: val } : ep));
301
- }}
302
- className="h-1.5 w-full appearance-none rounded-full bg-white/10 outline-none [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-amber-400 [&::-webkit-slider-thumb]:shadow-[0_0_10px_rgba(251,191,36,0.3)] hover:[&::-webkit-slider-thumb]:bg-amber-300 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-amber-400 [&::-moz-range-thumb]:shadow-[0_0_10px_rgba(251,191,36,0.3)] cursor-pointer transition-all"
303
- />
304
- </div>
305
- </div>
306
- </div>
307
- </motion.div>
308
- )
309
- })}
310
- </AnimatePresence>
311
- </div>
312
- )}
313
-
314
- {/* Side drawer for advanced dataset editing per endpoint. */}
315
- <DatasetEditor
316
- isOpen={isDrawerOpen}
317
- endpointIndex={selectedEndpointIndex}
318
- endpoint={selectedEndpointIndex !== null ? endpoints[selectedEndpointIndex] : null}
319
- onClose={handleCloseDrawer}
320
- onSaveField={handleUpdateEndpointField}
321
- />
322
-
323
- {/* Modal for creating a new endpoint entry. */}
324
- {isModalOpen && (
325
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-[#000000]/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
326
- <div className="w-full max-w-105 relative rounded-2xl p-px overflow-hidden group shadow-2xl">
327
- <div className="absolute -inset-full animate-[spin_4s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#191922_0%,#d1d5db_50%,#191922_100%)] opacity-30 group-hover:opacity-70 transition-opacity duration-500 rounded-2xl"></div>
328
- <div className="relative w-full h-full rounded-2xl bg-[#13151a] border border-white/5 overflow-hidden">
329
- <div className="flex items-center justify-between border-b border-white/5 px-6 py-5">
330
- <h2 className="text-[18px] font-bold tracking-tight text-white">Add New Endpoint</h2>
331
- <button
332
- onClick={handleCloseModal}
333
- className="flex h-7 w-7 items-center justify-center rounded-md bg-white/5 text-neutral-400 transition-colors hover:bg-white/10 hover:text-white"
334
- >
335
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
336
- </button>
337
- </div>
338
-
339
- <form onSubmit={handleAddEndpoint} className="flex flex-col gap-5 p-6 pb-7">
340
- <div className="flex flex-col gap-2">
341
- <label className="text-[14px] font-semibold text-white">
342
- Method
343
- </label>
344
- <div className="relative">
345
- <select
346
- name="method"
347
- value={formData.method}
348
- onChange={handleFormChange}
349
- className="w-full appearance-none rounded-xl border border-white/5 bg-[#0a0c10] px-4 py-3.5 text-[14px] font-medium text-white outline-none transition-colors hover:border-white/20 focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400"
350
- >
351
- <option>GET</option>
352
- <option>POST</option>
353
- <option>PUT</option>
354
- <option>DELETE</option>
355
- <option>PATCH</option>
356
- </select>
357
- <div className="pointer-events-none absolute inset-y-0 right-4 flex items-center text-neutral-500">
358
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
359
- </div>
360
- </div>
361
- </div>
362
-
363
- <div className="flex flex-col gap-2">
364
- <label className="text-[14px] font-semibold text-white">
365
- Path
366
- </label>
367
- <input
368
- type="text"
369
- name="path"
370
- placeholder="users, payments etc."
371
- value={formData.path}
372
- onChange={handleFormChange}
373
- className="w-full rounded-xl border border-white/5 bg-[#0a0c10] px-4 py-3.5 text-[14px] font-mono font-medium text-white placeholder:text-neutral-600 outline-none transition-colors hover:border-white/20 focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400"
374
- />
375
- </div>
376
-
377
- <div className="flex flex-col gap-2">
378
- <label className="text-[14px] font-semibold text-white">
379
- Delay (ms)
380
- </label>
381
- <input
382
- type="number"
383
- name="delay"
384
- min="0"
385
- value={formData.delay}
386
- onChange={handleFormChange}
387
- className="w-full rounded-xl border border-white/5 bg-[#0a0c10] px-4 py-3.5 text-[14px] font-mono font-medium tabular-nums text-white outline-none transition-colors hover:border-white/20 focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
388
- />
389
- </div>
390
-
391
- <div className="flex flex-col gap-2">
392
- <label className="text-[14px] font-semibold text-white">
393
- Failure Rate (%)
394
- </label>
395
- <input
396
- type="number"
397
- name="failureRate"
398
- min="0"
399
- max="100"
400
- value={formData.failureRate}
401
- onChange={handleFormChange}
402
- className="w-full rounded-xl border border-white/5 bg-[#0a0c10] px-4 py-3.5 text-[14px] font-mono font-medium tabular-nums text-white outline-none transition-colors hover:border-white/20 focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
403
- />
404
- </div>
405
-
406
- <div className="mt-4 flex gap-3">
407
- <button
408
- type="button"
409
- onClick={handleCloseModal}
410
- className="flex-1 rounded-xl border border-white/10 bg-[#1c1f26] px-4 py-3.5 text-[15px] font-bold text-white transition-all hover:bg-[#252830]"
411
- >
412
- Cancel
413
- </button>
414
- <button
415
- type="submit"
416
- className="relative flex-1 rounded-xl bg-linear-to-b from-neutral-200 to-neutral-400 px-4 py-3.5 text-[15px] font-bold text-black shadow-[0_0_20px_rgba(255,255,255,0.15)] transition-all hover:from-white hover:to-neutral-300 active:scale-[0.98] before:absolute before:inset-0 before:rounded-xl before:border before:border-white/40"
417
- >
418
- Add Endpoint
419
- </button>
420
- </div>
421
- </form>
422
- </div>
423
- </div>
424
- </div>
425
- )}
426
- </section>
427
- )
428
- }