@tradejs/app 3.1.3 → 3.1.4
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.
- package/package.json +6 -6
- package/src/app/api/user/runtime-deployments/[deploymentId]/strategies/[strategyName]/control/route.ts +2 -2
- package/src/app/api/user/runtime-deployments/route.ts +52 -57
- package/src/app/api/user/settings/route.ts +14 -30
- package/src/app/api/user/trading-accounts/route.ts +1 -42
- package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +23 -63
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +24 -38
- package/src/app/components/Strategies/RuntimeStrategyConfigDrawer.tsx +43 -483
- package/src/app/api/user/runtime-strategy-configs/route.ts +0 -51
- package/src/app/api/user/runtime-strategy-releases/route.ts +0 -60
- package/src/app/lib/runtimeStrategyConfigForm.ts +0 -86
- package/src/app/lib/runtimeStrategyConfigService.ts +0 -279
- package/src/app/lib/runtimeStrategyReleaseService.ts +0 -123
|
@@ -1,501 +1,61 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
type ReactNode,
|
|
5
|
-
useCallback,
|
|
6
|
-
useEffect,
|
|
7
|
-
useMemo,
|
|
8
|
-
useState,
|
|
9
|
-
} from 'react';
|
|
10
|
-
import {
|
|
11
|
-
Box,
|
|
12
4
|
Button,
|
|
13
|
-
Checkbox,
|
|
14
5
|
CloseButton,
|
|
15
6
|
Drawer,
|
|
16
|
-
Field,
|
|
17
|
-
Flex,
|
|
18
|
-
Input,
|
|
19
|
-
NativeSelect,
|
|
20
7
|
Portal,
|
|
21
8
|
Text,
|
|
22
9
|
Textarea,
|
|
23
10
|
} from '@chakra-ui/react';
|
|
24
|
-
import type {
|
|
25
|
-
import {
|
|
26
|
-
DEFAULT_RUNTIME_STRATEGY_MANAGED_PARAMETERS,
|
|
27
|
-
mergeRuntimeStrategyManagedParameters,
|
|
28
|
-
splitRuntimeStrategyConfig,
|
|
29
|
-
} from '#app/lib/runtimeStrategyConfigForm';
|
|
30
|
-
import { toaster } from '#ui';
|
|
31
|
-
|
|
32
|
-
type AccountOption = {
|
|
33
|
-
id: string;
|
|
34
|
-
label: string;
|
|
35
|
-
provider: string;
|
|
36
|
-
enabled: boolean;
|
|
37
|
-
isDefault?: boolean;
|
|
38
|
-
universes: MarketUniverse[];
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
type OptionsResponse = {
|
|
42
|
-
strategyNames: string[];
|
|
43
|
-
accounts: AccountOption[];
|
|
44
|
-
intervals: string[];
|
|
45
|
-
error?: string;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const SelectField = ({
|
|
49
|
-
label,
|
|
50
|
-
value,
|
|
51
|
-
onChange,
|
|
52
|
-
children,
|
|
53
|
-
disabled,
|
|
54
|
-
}: {
|
|
55
|
-
label: string;
|
|
56
|
-
value: string;
|
|
57
|
-
onChange: (value: string) => void;
|
|
58
|
-
children: ReactNode;
|
|
59
|
-
disabled?: boolean;
|
|
60
|
-
}) => (
|
|
61
|
-
<Field.Root>
|
|
62
|
-
<Field.Label>{label}</Field.Label>
|
|
63
|
-
<NativeSelect.Root disabled={disabled}>
|
|
64
|
-
<NativeSelect.Field
|
|
65
|
-
value={value}
|
|
66
|
-
onChange={(event) => onChange(event.currentTarget.value)}
|
|
67
|
-
>
|
|
68
|
-
{children}
|
|
69
|
-
</NativeSelect.Field>
|
|
70
|
-
<NativeSelect.Indicator />
|
|
71
|
-
</NativeSelect.Root>
|
|
72
|
-
</Field.Root>
|
|
73
|
-
);
|
|
11
|
+
import type { RuntimeStrategyView } from '@tradejs/types';
|
|
74
12
|
|
|
75
13
|
export const RuntimeStrategyConfigDrawer = ({
|
|
76
14
|
open,
|
|
77
15
|
strategy,
|
|
78
16
|
onOpenChange,
|
|
79
|
-
onSaved,
|
|
80
17
|
}: {
|
|
81
18
|
open: boolean;
|
|
82
|
-
strategy
|
|
19
|
+
strategy: RuntimeStrategyView;
|
|
83
20
|
onOpenChange: (open: boolean) => void;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} finally {
|
|
126
|
-
setLoading(false);
|
|
127
|
-
}
|
|
128
|
-
}, [editing, strategyName]);
|
|
129
|
-
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (!open) return;
|
|
132
|
-
if (strategy) {
|
|
133
|
-
const { managed, parameters: strategyParameters } =
|
|
134
|
-
splitRuntimeStrategyConfig(strategy.config);
|
|
135
|
-
setStrategyName(strategy.strategyName);
|
|
136
|
-
setConfigId(strategy.configId);
|
|
137
|
-
setInterval(String(strategy.interval ?? '15'));
|
|
138
|
-
setUniverse(strategy.universe ?? 'crypto');
|
|
139
|
-
setAccountId(String(strategy.config?.ACCOUNT_ID ?? ''));
|
|
140
|
-
setEnabled(strategy.enabled);
|
|
141
|
-
setMaxLossValue(String(managed.maxLossValue));
|
|
142
|
-
setAiEnabled(managed.aiEnabled);
|
|
143
|
-
setAiMode(managed.aiMode);
|
|
144
|
-
setMinAiQuality(String(managed.minAiQuality));
|
|
145
|
-
setMlEnabled(managed.mlEnabled);
|
|
146
|
-
setMlThreshold(String(managed.mlThreshold));
|
|
147
|
-
setParameters(JSON.stringify(strategyParameters, null, 2));
|
|
148
|
-
} else {
|
|
149
|
-
const managed = DEFAULT_RUNTIME_STRATEGY_MANAGED_PARAMETERS;
|
|
150
|
-
setConfigId('config');
|
|
151
|
-
setInterval('15');
|
|
152
|
-
setUniverse('crypto');
|
|
153
|
-
setAccountId('');
|
|
154
|
-
setEnabled(true);
|
|
155
|
-
setMaxLossValue(String(managed.maxLossValue));
|
|
156
|
-
setAiEnabled(managed.aiEnabled);
|
|
157
|
-
setAiMode(managed.aiMode);
|
|
158
|
-
setMinAiQuality(String(managed.minAiQuality));
|
|
159
|
-
setMlEnabled(managed.mlEnabled);
|
|
160
|
-
setMlThreshold(String(managed.mlThreshold));
|
|
161
|
-
setParameters('{}');
|
|
162
|
-
}
|
|
163
|
-
if (!strategy?.releaseVersion) void loadOptions();
|
|
164
|
-
}, [loadOptions, open, strategy]);
|
|
165
|
-
|
|
166
|
-
const compatibleAccounts = useMemo(
|
|
167
|
-
() =>
|
|
168
|
-
options.accounts.filter(
|
|
169
|
-
(account) =>
|
|
170
|
-
account.enabled &&
|
|
171
|
-
account.provider === 'bybit' &&
|
|
172
|
-
account.universes.includes(universe),
|
|
173
|
-
),
|
|
174
|
-
[options.accounts, universe],
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
useEffect(() => {
|
|
178
|
-
if (
|
|
179
|
-
accountId &&
|
|
180
|
-
!compatibleAccounts.some((account) => account.id === accountId)
|
|
181
|
-
) {
|
|
182
|
-
setAccountId('');
|
|
183
|
-
}
|
|
184
|
-
}, [accountId, compatibleAccounts]);
|
|
185
|
-
|
|
186
|
-
const save = async () => {
|
|
187
|
-
setSaving(true);
|
|
188
|
-
try {
|
|
189
|
-
const parsed = JSON.parse(parameters) as unknown;
|
|
190
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
191
|
-
throw new Error('Strategy parameters must be a JSON object');
|
|
192
|
-
}
|
|
193
|
-
const parsedMaxLossValue = Number(maxLossValue);
|
|
194
|
-
if (!Number.isFinite(parsedMaxLossValue) || parsedMaxLossValue < 0) {
|
|
195
|
-
throw new Error('Max loss value must be a non-negative number');
|
|
196
|
-
}
|
|
197
|
-
const parsedMinAiQuality = Number(minAiQuality);
|
|
198
|
-
if (
|
|
199
|
-
!Number.isInteger(parsedMinAiQuality) ||
|
|
200
|
-
parsedMinAiQuality < 0 ||
|
|
201
|
-
parsedMinAiQuality > 5
|
|
202
|
-
) {
|
|
203
|
-
throw new Error('Minimum AI quality must be an integer from 0 to 5');
|
|
204
|
-
}
|
|
205
|
-
const parsedMlThreshold = Number(mlThreshold);
|
|
206
|
-
if (
|
|
207
|
-
!Number.isFinite(parsedMlThreshold) ||
|
|
208
|
-
parsedMlThreshold < 0 ||
|
|
209
|
-
parsedMlThreshold > 1
|
|
210
|
-
) {
|
|
211
|
-
throw new Error('ML threshold must be a number from 0 to 1');
|
|
212
|
-
}
|
|
213
|
-
const managedParameters = mergeRuntimeStrategyManagedParameters(
|
|
214
|
-
parsed as Record<string, unknown>,
|
|
215
|
-
{
|
|
216
|
-
maxLossValue: parsedMaxLossValue,
|
|
217
|
-
aiEnabled,
|
|
218
|
-
aiMode,
|
|
219
|
-
minAiQuality: parsedMinAiQuality,
|
|
220
|
-
mlEnabled,
|
|
221
|
-
mlThreshold: parsedMlThreshold,
|
|
222
|
-
},
|
|
223
|
-
);
|
|
224
|
-
const response = await fetch('/api/user/runtime-strategy-configs', {
|
|
225
|
-
method: editing ? 'PATCH' : 'POST',
|
|
226
|
-
headers: { 'content-type': 'application/json' },
|
|
227
|
-
body: JSON.stringify({
|
|
228
|
-
strategyName,
|
|
229
|
-
configId,
|
|
230
|
-
interval,
|
|
231
|
-
universe,
|
|
232
|
-
accountId: accountId || null,
|
|
233
|
-
enabled,
|
|
234
|
-
parameters: managedParameters,
|
|
235
|
-
}),
|
|
236
|
-
});
|
|
237
|
-
const payload = (await response.json()) as { error?: string };
|
|
238
|
-
if (!response.ok) throw new Error(payload.error || 'Save failed');
|
|
239
|
-
toaster.success({
|
|
240
|
-
title: editing ? 'Strategy config updated' : 'Strategy config created',
|
|
241
|
-
});
|
|
242
|
-
onOpenChange(false);
|
|
243
|
-
await onSaved();
|
|
244
|
-
} catch (error) {
|
|
245
|
-
toaster.error({
|
|
246
|
-
title: 'Could not save strategy config',
|
|
247
|
-
description: error instanceof Error ? error.message : String(error),
|
|
248
|
-
});
|
|
249
|
-
} finally {
|
|
250
|
-
setSaving(false);
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
if (strategy?.releaseVersion) {
|
|
255
|
-
return (
|
|
256
|
-
<Drawer.Root
|
|
257
|
-
size="lg"
|
|
258
|
-
open={open}
|
|
259
|
-
onOpenChange={(event) => onOpenChange(event.open)}
|
|
260
|
-
>
|
|
261
|
-
<Portal>
|
|
262
|
-
<Drawer.Backdrop />
|
|
263
|
-
<Drawer.Positioner>
|
|
264
|
-
<Drawer.Content bg="gray.950">
|
|
265
|
-
<Drawer.Header>
|
|
266
|
-
<Drawer.Title>
|
|
267
|
-
{strategy.strategyName} release v{strategy.releaseVersion}
|
|
268
|
-
</Drawer.Title>
|
|
269
|
-
<Drawer.CloseTrigger asChild>
|
|
270
|
-
<CloseButton size="sm" />
|
|
271
|
-
</Drawer.CloseTrigger>
|
|
272
|
-
</Drawer.Header>
|
|
273
|
-
<Drawer.Body display="flex" flexDirection="column" gap={4}>
|
|
274
|
-
<Text color="gray.400">
|
|
275
|
-
Published releases are immutable. Create and publish a new
|
|
276
|
-
release to change this configuration.
|
|
277
|
-
</Text>
|
|
278
|
-
<Textarea
|
|
279
|
-
value={JSON.stringify(strategy.config ?? {}, null, 2)}
|
|
280
|
-
readOnly
|
|
281
|
-
minH="70vh"
|
|
282
|
-
fontFamily="mono"
|
|
283
|
-
fontSize="sm"
|
|
284
|
-
/>
|
|
285
|
-
</Drawer.Body>
|
|
286
|
-
<Drawer.Footer>
|
|
287
|
-
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
288
|
-
Close
|
|
289
|
-
</Button>
|
|
290
|
-
</Drawer.Footer>
|
|
291
|
-
</Drawer.Content>
|
|
292
|
-
</Drawer.Positioner>
|
|
293
|
-
</Portal>
|
|
294
|
-
</Drawer.Root>
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return (
|
|
299
|
-
<Drawer.Root
|
|
300
|
-
size="lg"
|
|
301
|
-
open={open}
|
|
302
|
-
onOpenChange={(event) => onOpenChange(event.open)}
|
|
303
|
-
>
|
|
304
|
-
<Portal>
|
|
305
|
-
<Drawer.Backdrop />
|
|
306
|
-
<Drawer.Positioner>
|
|
307
|
-
<Drawer.Content bg="gray.950">
|
|
308
|
-
<Drawer.Header>
|
|
309
|
-
<Drawer.Title>
|
|
310
|
-
{editing
|
|
311
|
-
? 'Edit strategy configuration'
|
|
312
|
-
: 'Create strategy configuration'}
|
|
313
|
-
</Drawer.Title>
|
|
314
|
-
<Drawer.CloseTrigger asChild>
|
|
315
|
-
<CloseButton size="sm" />
|
|
316
|
-
</Drawer.CloseTrigger>
|
|
317
|
-
</Drawer.Header>
|
|
318
|
-
<Drawer.Body display="flex" flexDirection="column" gap={5}>
|
|
319
|
-
{editing ? (
|
|
320
|
-
<Field.Root>
|
|
321
|
-
<Field.Label>Strategy</Field.Label>
|
|
322
|
-
<Input value={strategyName} disabled />
|
|
323
|
-
</Field.Root>
|
|
324
|
-
) : (
|
|
325
|
-
<SelectField
|
|
326
|
-
label="Strategy"
|
|
327
|
-
value={strategyName}
|
|
328
|
-
onChange={setStrategyName}
|
|
329
|
-
disabled={loading}
|
|
330
|
-
>
|
|
331
|
-
{options.strategyNames.map((name) => (
|
|
332
|
-
<option key={name} value={name}>
|
|
333
|
-
{name}
|
|
334
|
-
</option>
|
|
335
|
-
))}
|
|
336
|
-
</SelectField>
|
|
337
|
-
)}
|
|
338
|
-
<Field.Root>
|
|
339
|
-
<Field.Label>Configuration id</Field.Label>
|
|
340
|
-
<Input
|
|
341
|
-
value={configId}
|
|
342
|
-
disabled={editing}
|
|
343
|
-
onChange={(event) => setConfigId(event.target.value)}
|
|
344
|
-
/>
|
|
345
|
-
</Field.Root>
|
|
346
|
-
<Flex gap={4} align="start">
|
|
347
|
-
<Box flex="1">
|
|
348
|
-
<SelectField
|
|
349
|
-
label="Timeframe"
|
|
350
|
-
value={interval}
|
|
351
|
-
onChange={setInterval}
|
|
352
|
-
>
|
|
353
|
-
{options.intervals.map((value) => (
|
|
354
|
-
<option key={value} value={value}>
|
|
355
|
-
{value}m
|
|
356
|
-
</option>
|
|
357
|
-
))}
|
|
358
|
-
</SelectField>
|
|
359
|
-
</Box>
|
|
360
|
-
<Box flex="1">
|
|
361
|
-
<SelectField
|
|
362
|
-
label="Universe"
|
|
363
|
-
value={universe}
|
|
364
|
-
onChange={(value) => setUniverse(value as MarketUniverse)}
|
|
365
|
-
>
|
|
366
|
-
<option value="crypto">Crypto</option>
|
|
367
|
-
<option value="tradfi">TradFi</option>
|
|
368
|
-
</SelectField>
|
|
369
|
-
</Box>
|
|
370
|
-
</Flex>
|
|
371
|
-
<SelectField
|
|
372
|
-
label="Trading account"
|
|
373
|
-
value={accountId}
|
|
374
|
-
onChange={setAccountId}
|
|
375
|
-
>
|
|
376
|
-
<option value="">Default account for {universe}</option>
|
|
377
|
-
{compatibleAccounts.map((account) => (
|
|
378
|
-
<option key={account.id} value={account.id}>
|
|
379
|
-
{account.label}
|
|
380
|
-
{account.isDefault ? ' (default)' : ''}
|
|
381
|
-
</option>
|
|
382
|
-
))}
|
|
383
|
-
</SelectField>
|
|
384
|
-
<Checkbox.Root
|
|
385
|
-
checked={enabled}
|
|
386
|
-
onCheckedChange={(event) => setEnabled(event.checked === true)}
|
|
387
|
-
>
|
|
388
|
-
<Checkbox.HiddenInput />
|
|
389
|
-
<Checkbox.Control>
|
|
390
|
-
<Checkbox.Indicator />
|
|
391
|
-
</Checkbox.Control>
|
|
392
|
-
<Checkbox.Label>Enabled</Checkbox.Label>
|
|
393
|
-
</Checkbox.Root>
|
|
394
|
-
<Field.Root>
|
|
395
|
-
<Field.Label>Max loss value</Field.Label>
|
|
396
|
-
<Input
|
|
397
|
-
type="number"
|
|
398
|
-
min={0}
|
|
399
|
-
step="0.1"
|
|
400
|
-
value={maxLossValue}
|
|
401
|
-
onChange={(event) => setMaxLossValue(event.target.value)}
|
|
402
|
-
/>
|
|
403
|
-
</Field.Root>
|
|
404
|
-
<Text color="gray.300" fontWeight="semibold">
|
|
405
|
-
AI
|
|
406
|
-
</Text>
|
|
407
|
-
<Flex gap={4} align="start">
|
|
408
|
-
<Box flex="1">
|
|
409
|
-
<SelectField
|
|
410
|
-
label="AI status"
|
|
411
|
-
value={aiEnabled ? 'enabled' : 'disabled'}
|
|
412
|
-
onChange={(value) => setAiEnabled(value === 'enabled')}
|
|
413
|
-
>
|
|
414
|
-
<option value="enabled">Enabled</option>
|
|
415
|
-
<option value="disabled">Disabled</option>
|
|
416
|
-
</SelectField>
|
|
417
|
-
</Box>
|
|
418
|
-
<Box flex="1">
|
|
419
|
-
<SelectField
|
|
420
|
-
label="AI mode"
|
|
421
|
-
value={aiMode}
|
|
422
|
-
onChange={(value) =>
|
|
423
|
-
setAiMode(value === 'llm' ? 'llm' : 'gate')
|
|
424
|
-
}
|
|
425
|
-
>
|
|
426
|
-
<option value="gate">Gate</option>
|
|
427
|
-
<option value="llm">LLM</option>
|
|
428
|
-
</SelectField>
|
|
429
|
-
</Box>
|
|
430
|
-
<Box flex="1">
|
|
431
|
-
<Field.Root>
|
|
432
|
-
<Field.Label>Minimum quality</Field.Label>
|
|
433
|
-
<Input
|
|
434
|
-
type="number"
|
|
435
|
-
min={0}
|
|
436
|
-
max={5}
|
|
437
|
-
step={1}
|
|
438
|
-
value={minAiQuality}
|
|
439
|
-
onChange={(event) => setMinAiQuality(event.target.value)}
|
|
440
|
-
/>
|
|
441
|
-
</Field.Root>
|
|
442
|
-
</Box>
|
|
443
|
-
</Flex>
|
|
444
|
-
<Text color="gray.300" fontWeight="semibold">
|
|
445
|
-
ML
|
|
446
|
-
</Text>
|
|
447
|
-
<Flex gap={4} align="start">
|
|
448
|
-
<Box flex="1">
|
|
449
|
-
<SelectField
|
|
450
|
-
label="ML status"
|
|
451
|
-
value={mlEnabled ? 'enabled' : 'disabled'}
|
|
452
|
-
onChange={(value) => setMlEnabled(value === 'enabled')}
|
|
453
|
-
>
|
|
454
|
-
<option value="enabled">Enabled</option>
|
|
455
|
-
<option value="disabled">Disabled</option>
|
|
456
|
-
</SelectField>
|
|
457
|
-
</Box>
|
|
458
|
-
<Box flex="1">
|
|
459
|
-
<Field.Root>
|
|
460
|
-
<Field.Label>ML threshold</Field.Label>
|
|
461
|
-
<Input
|
|
462
|
-
type="number"
|
|
463
|
-
min={0}
|
|
464
|
-
max={1}
|
|
465
|
-
step="0.01"
|
|
466
|
-
value={mlThreshold}
|
|
467
|
-
onChange={(event) => setMlThreshold(event.target.value)}
|
|
468
|
-
/>
|
|
469
|
-
</Field.Root>
|
|
470
|
-
</Box>
|
|
471
|
-
</Flex>
|
|
472
|
-
<Field.Root flex="1">
|
|
473
|
-
<Field.Label>Strategy parameters (JSON)</Field.Label>
|
|
474
|
-
<Textarea
|
|
475
|
-
value={parameters}
|
|
476
|
-
onChange={(event) => setParameters(event.target.value)}
|
|
477
|
-
minH="300px"
|
|
478
|
-
fontFamily="mono"
|
|
479
|
-
fontSize="sm"
|
|
480
|
-
/>
|
|
481
|
-
</Field.Root>
|
|
482
|
-
</Drawer.Body>
|
|
483
|
-
<Drawer.Footer>
|
|
484
|
-
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
485
|
-
Cancel
|
|
486
|
-
</Button>
|
|
487
|
-
<Button
|
|
488
|
-
colorPalette="teal"
|
|
489
|
-
loading={saving}
|
|
490
|
-
disabled={loading || !strategyName || !configId.trim()}
|
|
491
|
-
onClick={() => void save()}
|
|
492
|
-
>
|
|
493
|
-
{editing ? 'Save changes' : 'Create'}
|
|
494
|
-
</Button>
|
|
495
|
-
</Drawer.Footer>
|
|
496
|
-
</Drawer.Content>
|
|
497
|
-
</Drawer.Positioner>
|
|
498
|
-
</Portal>
|
|
499
|
-
</Drawer.Root>
|
|
500
|
-
);
|
|
501
|
-
};
|
|
21
|
+
}) => (
|
|
22
|
+
<Drawer.Root
|
|
23
|
+
size="lg"
|
|
24
|
+
open={open}
|
|
25
|
+
onOpenChange={(event) => onOpenChange(event.open)}
|
|
26
|
+
>
|
|
27
|
+
<Portal>
|
|
28
|
+
<Drawer.Backdrop />
|
|
29
|
+
<Drawer.Positioner>
|
|
30
|
+
<Drawer.Content bg="gray.950">
|
|
31
|
+
<Drawer.Header>
|
|
32
|
+
<Drawer.Title>
|
|
33
|
+
{strategy.strategyName} release v{strategy.releaseVersion}
|
|
34
|
+
</Drawer.Title>
|
|
35
|
+
<Drawer.CloseTrigger asChild>
|
|
36
|
+
<CloseButton size="sm" />
|
|
37
|
+
</Drawer.CloseTrigger>
|
|
38
|
+
</Drawer.Header>
|
|
39
|
+
<Drawer.Body display="flex" flexDirection="column" gap={4}>
|
|
40
|
+
<Text color="gray.400">
|
|
41
|
+
Published releases are immutable. Roll out a new release to change
|
|
42
|
+
this configuration.
|
|
43
|
+
</Text>
|
|
44
|
+
<Textarea
|
|
45
|
+
value={JSON.stringify(strategy.config, null, 2)}
|
|
46
|
+
readOnly
|
|
47
|
+
minH="70vh"
|
|
48
|
+
fontFamily="mono"
|
|
49
|
+
fontSize="sm"
|
|
50
|
+
/>
|
|
51
|
+
</Drawer.Body>
|
|
52
|
+
<Drawer.Footer>
|
|
53
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
54
|
+
Close
|
|
55
|
+
</Button>
|
|
56
|
+
</Drawer.Footer>
|
|
57
|
+
</Drawer.Content>
|
|
58
|
+
</Drawer.Positioner>
|
|
59
|
+
</Portal>
|
|
60
|
+
</Drawer.Root>
|
|
61
|
+
);
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
3
|
-
getRuntimeStrategyConfigOptions,
|
|
4
|
-
RuntimeStrategyConfigServiceError,
|
|
5
|
-
saveRuntimeStrategyConfigForUser,
|
|
6
|
-
} from '#app/lib/runtimeStrategyConfigService';
|
|
7
|
-
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
8
|
-
|
|
9
|
-
export const dynamic = 'force-dynamic';
|
|
10
|
-
|
|
11
|
-
const projectRoot =
|
|
12
|
-
String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
13
|
-
|
|
14
|
-
export const GET = async () => {
|
|
15
|
-
const userName = await getCurrentUserName();
|
|
16
|
-
if (!userName)
|
|
17
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
18
|
-
return NextResponse.json(
|
|
19
|
-
await getRuntimeStrategyConfigOptions({ userName, projectRoot }),
|
|
20
|
-
);
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const save = async (request: NextRequest, editing: boolean) => {
|
|
24
|
-
const userName = await getCurrentUserName();
|
|
25
|
-
if (!userName)
|
|
26
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
27
|
-
try {
|
|
28
|
-
return NextResponse.json(
|
|
29
|
-
await saveRuntimeStrategyConfigForUser({
|
|
30
|
-
userName,
|
|
31
|
-
projectRoot,
|
|
32
|
-
input: (await request.json()) as Record<string, unknown>,
|
|
33
|
-
editing,
|
|
34
|
-
}),
|
|
35
|
-
);
|
|
36
|
-
} catch (error) {
|
|
37
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
-
const status =
|
|
39
|
-
error instanceof RuntimeStrategyConfigServiceError &&
|
|
40
|
-
error.code === 'conflict'
|
|
41
|
-
? 409
|
|
42
|
-
: error instanceof RuntimeStrategyConfigServiceError &&
|
|
43
|
-
error.code === 'not_found'
|
|
44
|
-
? 404
|
|
45
|
-
: 400;
|
|
46
|
-
return NextResponse.json({ error: message }, { status });
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const POST = (request: NextRequest) => save(request, false);
|
|
51
|
-
export const PATCH = (request: NextRequest) => save(request, true);
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
3
|
-
getRuntimeStrategyReleaseOptions,
|
|
4
|
-
publishRuntimeStrategyReleaseForUser,
|
|
5
|
-
saveRuntimeStrategyReleaseDraftForUser,
|
|
6
|
-
} from '#app/lib/runtimeStrategyReleaseService';
|
|
7
|
-
import { getCurrentUserName } from '#app/lib/currentUser';
|
|
8
|
-
|
|
9
|
-
export const dynamic = 'force-dynamic';
|
|
10
|
-
|
|
11
|
-
const projectRoot =
|
|
12
|
-
String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
13
|
-
|
|
14
|
-
export const GET = async () => {
|
|
15
|
-
const userName = await getCurrentUserName();
|
|
16
|
-
if (!userName)
|
|
17
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
18
|
-
return NextResponse.json(
|
|
19
|
-
await getRuntimeStrategyReleaseOptions({ userName, projectRoot }),
|
|
20
|
-
);
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const PATCH = async (request: NextRequest) => {
|
|
24
|
-
const userName = await getCurrentUserName();
|
|
25
|
-
if (!userName)
|
|
26
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
27
|
-
try {
|
|
28
|
-
const draft = await saveRuntimeStrategyReleaseDraftForUser({
|
|
29
|
-
userName,
|
|
30
|
-
projectRoot,
|
|
31
|
-
input: (await request.json()) as Record<string, unknown>,
|
|
32
|
-
});
|
|
33
|
-
return NextResponse.json({ draft });
|
|
34
|
-
} catch (error) {
|
|
35
|
-
return NextResponse.json(
|
|
36
|
-
{ error: error instanceof Error ? error.message : String(error) },
|
|
37
|
-
{ status: 400 },
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const POST = async (request: NextRequest) => {
|
|
43
|
-
const userName = await getCurrentUserName();
|
|
44
|
-
if (!userName)
|
|
45
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
46
|
-
try {
|
|
47
|
-
const body = (await request.json()) as { strategyName?: unknown };
|
|
48
|
-
const release = await publishRuntimeStrategyReleaseForUser({
|
|
49
|
-
userName,
|
|
50
|
-
projectRoot,
|
|
51
|
-
strategyName: String(body.strategyName ?? '').trim(),
|
|
52
|
-
});
|
|
53
|
-
return NextResponse.json({ release });
|
|
54
|
-
} catch (error) {
|
|
55
|
-
return NextResponse.json(
|
|
56
|
-
{ error: error instanceof Error ? error.message : String(error) },
|
|
57
|
-
{ status: 400 },
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
};
|