@nextclaw/ui 0.5.21 → 0.5.22
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/CHANGELOG.md +10 -0
- package/dist/assets/ChannelsList-dmUfRDVE.js +1 -0
- package/dist/assets/{ChatPage-BUm3UPap.js → ChatPage-CmRkhiCX.js} +1 -1
- package/dist/assets/{CronConfig-9dYfTRJl.js → CronConfig-B15BA--M.js} +1 -1
- package/dist/assets/{DocBrowser-BIV0vpA0.js → DocBrowser-CBwMA2wK.js} +1 -1
- package/dist/assets/{MarketplacePage-2Zi0JSVi.js → MarketplacePage-BZRz4tCA.js} +1 -1
- package/dist/assets/{ModelConfig-h21P5rV0.js → ModelConfig-CByjZNvf.js} +1 -1
- package/dist/assets/{ProvidersList-DEaK1a3y.js → ProvidersList-BbsQEnoZ.js} +1 -1
- package/dist/assets/{RuntimeConfig-DXMzf-gF.js → RuntimeConfig-CrQsFzP7.js} +1 -1
- package/dist/assets/SecretsConfig-DPZqWmry.js +3 -0
- package/dist/assets/{SessionsConfig-SdXvn_9E.js → SessionsConfig-Bk7RCsWw.js} +2 -2
- package/dist/assets/{action-link-C9xMkxl2.js → action-link-CKSHFT5k.js} +1 -1
- package/dist/assets/{card-Cnqfntk5.js → card-6hv7Kf0F.js} +1 -1
- package/dist/assets/{dialog-DJs630RE.js → dialog-Bmy_bApp.js} +1 -1
- package/dist/assets/index-BsDasSXm.css +1 -0
- package/dist/assets/index-y6creQ7S.js +2 -0
- package/dist/assets/{label-CXGuE6Oa.js → label-Btp6gGxV.js} +1 -1
- package/dist/assets/{page-layout-BVZlyPFt.js → page-layout-DWVKbt_g.js} +1 -1
- package/dist/assets/{switch-BLF45eI3.js → switch-AeXayTRS.js} +1 -1
- package/dist/assets/{tabs-custom-DQ0GpEV5.js → tabs-custom-DEZwNpXo.js} +1 -1
- package/dist/assets/useConfig-BiQH98MD.js +1 -0
- package/dist/assets/{useConfirmDialog-CK7KAyDf.js → useConfirmDialog-BY0hni-H.js} +1 -1
- package/dist/assets/{vendor-RXIbhDBC.js → vendor-H2M3a_4Z.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/App.tsx +2 -0
- package/src/api/config.ts +16 -0
- package/src/api/types.ts +52 -0
- package/src/components/config/ChannelForm.tsx +9 -0
- package/src/components/config/SecretsConfig.tsx +469 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/hooks/useConfig.ts +17 -0
- package/src/lib/i18n.ts +42 -0
- package/dist/assets/ChannelsList-TFFw4Cem.js +0 -1
- package/dist/assets/index-CrUDzcei.js +0 -2
- package/dist/assets/index-Zy7fAOe1.css +0 -1
- package/dist/assets/useConfig-vFQvF4kn.js +0 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useConfig, useUpdateSecrets } from '@/hooks/useConfig';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Input } from '@/components/ui/input';
|
|
6
|
+
import { Label } from '@/components/ui/label';
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
8
|
+
import { Switch } from '@/components/ui/switch';
|
|
9
|
+
import { PageHeader, PageLayout } from '@/components/layout/page-layout';
|
|
10
|
+
import type { SecretProviderView, SecretRefView, SecretSourceView } from '@/api/types';
|
|
11
|
+
import { t } from '@/lib/i18n';
|
|
12
|
+
import { Plus, Save, Trash2 } from 'lucide-react';
|
|
13
|
+
import { toast } from 'sonner';
|
|
14
|
+
|
|
15
|
+
type ProviderRow = {
|
|
16
|
+
alias: string;
|
|
17
|
+
source: SecretSourceView;
|
|
18
|
+
prefix: string;
|
|
19
|
+
path: string;
|
|
20
|
+
command: string;
|
|
21
|
+
argsText: string;
|
|
22
|
+
cwd: string;
|
|
23
|
+
timeoutMs: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type RefRow = {
|
|
27
|
+
path: string;
|
|
28
|
+
source: SecretSourceView;
|
|
29
|
+
provider: string;
|
|
30
|
+
id: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const SOURCE_OPTIONS: SecretSourceView[] = ['env', 'file', 'exec'];
|
|
34
|
+
|
|
35
|
+
function createProviderRow(alias = ''): ProviderRow {
|
|
36
|
+
return {
|
|
37
|
+
alias,
|
|
38
|
+
source: 'env',
|
|
39
|
+
prefix: '',
|
|
40
|
+
path: '',
|
|
41
|
+
command: '',
|
|
42
|
+
argsText: '',
|
|
43
|
+
cwd: '',
|
|
44
|
+
timeoutMs: 5000
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createRefRow(): RefRow {
|
|
49
|
+
return {
|
|
50
|
+
path: '',
|
|
51
|
+
source: 'env',
|
|
52
|
+
provider: '',
|
|
53
|
+
id: ''
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function providerToRow(alias: string, provider: SecretProviderView): ProviderRow {
|
|
58
|
+
if (provider.source === 'env') {
|
|
59
|
+
return {
|
|
60
|
+
...createProviderRow(alias),
|
|
61
|
+
source: 'env',
|
|
62
|
+
prefix: provider.prefix ?? ''
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (provider.source === 'file') {
|
|
67
|
+
return {
|
|
68
|
+
...createProviderRow(alias),
|
|
69
|
+
source: 'file',
|
|
70
|
+
path: provider.path
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...createProviderRow(alias),
|
|
76
|
+
source: 'exec',
|
|
77
|
+
command: provider.command,
|
|
78
|
+
argsText: (provider.args ?? []).join('\n'),
|
|
79
|
+
cwd: provider.cwd ?? '',
|
|
80
|
+
timeoutMs: provider.timeoutMs ?? 5000
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rowToProvider(row: ProviderRow): SecretProviderView {
|
|
85
|
+
if (row.source === 'env') {
|
|
86
|
+
return {
|
|
87
|
+
source: 'env',
|
|
88
|
+
...(row.prefix.trim() ? { prefix: row.prefix.trim() } : {})
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (row.source === 'file') {
|
|
93
|
+
return {
|
|
94
|
+
source: 'file',
|
|
95
|
+
path: row.path.trim(),
|
|
96
|
+
format: 'json'
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
source: 'exec',
|
|
102
|
+
command: row.command.trim(),
|
|
103
|
+
args: row.argsText
|
|
104
|
+
.split('\n')
|
|
105
|
+
.map((item) => item.trim())
|
|
106
|
+
.filter(Boolean),
|
|
107
|
+
...(row.cwd.trim() ? { cwd: row.cwd.trim() } : {}),
|
|
108
|
+
timeoutMs: Math.max(1, Math.trunc(row.timeoutMs || 5000))
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function SecretsConfig() {
|
|
113
|
+
const { data: config, isLoading } = useConfig();
|
|
114
|
+
const updateSecrets = useUpdateSecrets();
|
|
115
|
+
|
|
116
|
+
const [enabled, setEnabled] = useState(true);
|
|
117
|
+
const [defaultEnv, setDefaultEnv] = useState('');
|
|
118
|
+
const [defaultFile, setDefaultFile] = useState('');
|
|
119
|
+
const [defaultExec, setDefaultExec] = useState('');
|
|
120
|
+
const [providers, setProviders] = useState<ProviderRow[]>([]);
|
|
121
|
+
const [refs, setRefs] = useState<RefRow[]>([]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
const secrets = config?.secrets;
|
|
125
|
+
if (!secrets) {
|
|
126
|
+
setEnabled(true);
|
|
127
|
+
setDefaultEnv('');
|
|
128
|
+
setDefaultFile('');
|
|
129
|
+
setDefaultExec('');
|
|
130
|
+
setProviders([]);
|
|
131
|
+
setRefs([]);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setEnabled(Boolean(secrets.enabled));
|
|
136
|
+
setDefaultEnv(secrets.defaults.env ?? '');
|
|
137
|
+
setDefaultFile(secrets.defaults.file ?? '');
|
|
138
|
+
setDefaultExec(secrets.defaults.exec ?? '');
|
|
139
|
+
|
|
140
|
+
const nextProviders = Object.entries(secrets.providers).map(([alias, provider]) =>
|
|
141
|
+
providerToRow(alias, provider)
|
|
142
|
+
);
|
|
143
|
+
const nextRefs = Object.entries(secrets.refs).map(([path, ref]) => ({
|
|
144
|
+
path,
|
|
145
|
+
source: ref.source,
|
|
146
|
+
provider: ref.provider ?? '',
|
|
147
|
+
id: ref.id
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
setProviders(nextProviders);
|
|
151
|
+
setRefs(nextRefs);
|
|
152
|
+
}, [config?.secrets]);
|
|
153
|
+
|
|
154
|
+
const providerAliases = useMemo(() => {
|
|
155
|
+
const aliases = providers
|
|
156
|
+
.map((item) => item.alias.trim())
|
|
157
|
+
.filter(Boolean);
|
|
158
|
+
return Array.from(new Set(aliases));
|
|
159
|
+
}, [providers]);
|
|
160
|
+
|
|
161
|
+
const updateProvider = (index: number, patch: Partial<ProviderRow>) => {
|
|
162
|
+
setProviders((prev) => prev.map((entry, cursor) => (cursor === index ? { ...entry, ...patch } : entry)));
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const updateRef = (index: number, patch: Partial<RefRow>) => {
|
|
166
|
+
setRefs((prev) => prev.map((entry, cursor) => (cursor === index ? { ...entry, ...patch } : entry)));
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleSave = () => {
|
|
170
|
+
try {
|
|
171
|
+
const providerMap: Record<string, SecretProviderView> = {};
|
|
172
|
+
for (const [index, row] of providers.entries()) {
|
|
173
|
+
const alias = row.alias.trim();
|
|
174
|
+
if (!alias) {
|
|
175
|
+
throw new Error(`${t('providerAlias')} #${index + 1} ${t('isRequired')}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (providerMap[alias]) {
|
|
179
|
+
throw new Error(`${t('providerAlias')}: ${alias} (${t('duplicate')})`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (row.source === 'file' && !row.path.trim()) {
|
|
183
|
+
throw new Error(`${t('secretFilePath')} #${index + 1} ${t('isRequired')}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (row.source === 'exec' && !row.command.trim()) {
|
|
187
|
+
throw new Error(`${t('secretExecCommand')} #${index + 1} ${t('isRequired')}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
providerMap[alias] = rowToProvider(row);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const refMap: Record<string, SecretRefView> = {};
|
|
194
|
+
for (const [index, row] of refs.entries()) {
|
|
195
|
+
const path = row.path.trim();
|
|
196
|
+
const id = row.id.trim();
|
|
197
|
+
if (!path) {
|
|
198
|
+
throw new Error(`${t('secretConfigPath')} #${index + 1} ${t('isRequired')}`);
|
|
199
|
+
}
|
|
200
|
+
if (!id) {
|
|
201
|
+
throw new Error(`${t('secretId')} #${index + 1} ${t('isRequired')}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const provider = row.provider.trim();
|
|
205
|
+
if (provider && !providerMap[provider]) {
|
|
206
|
+
throw new Error(`${t('secretProviderAlias')}: ${provider} ${t('notFound')}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
refMap[path] = {
|
|
210
|
+
source: row.source,
|
|
211
|
+
...(provider ? { provider } : {}),
|
|
212
|
+
id
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
updateSecrets.mutate({
|
|
217
|
+
data: {
|
|
218
|
+
enabled,
|
|
219
|
+
defaults: {
|
|
220
|
+
env: defaultEnv.trim() || null,
|
|
221
|
+
file: defaultFile.trim() || null,
|
|
222
|
+
exec: defaultExec.trim() || null
|
|
223
|
+
},
|
|
224
|
+
providers: providerMap,
|
|
225
|
+
refs: refMap
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
toast.error(message);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (isLoading) {
|
|
235
|
+
return <div className="p-8 text-gray-400">{t('loading')}</div>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<PageLayout className="space-y-6">
|
|
240
|
+
<PageHeader title={t('secretsPageTitle')} description={t('secretsPageDescription')} />
|
|
241
|
+
|
|
242
|
+
<Card>
|
|
243
|
+
<CardHeader>
|
|
244
|
+
<CardTitle>{t('secrets')}</CardTitle>
|
|
245
|
+
<CardDescription>{t('secretsEnabledHelp')}</CardDescription>
|
|
246
|
+
</CardHeader>
|
|
247
|
+
<CardContent className="space-y-4">
|
|
248
|
+
<div className="flex items-center justify-between rounded-xl border border-gray-200 p-3">
|
|
249
|
+
<div>
|
|
250
|
+
<p className="text-sm font-medium text-gray-800">{t('enabled')}</p>
|
|
251
|
+
<p className="text-xs text-gray-500">{t('secretsEnabledHelp')}</p>
|
|
252
|
+
</div>
|
|
253
|
+
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
257
|
+
<div className="space-y-2">
|
|
258
|
+
<Label>{t('defaultEnvProvider')}</Label>
|
|
259
|
+
<Select value={defaultEnv || '__none__'} onValueChange={(value) => setDefaultEnv(value === '__none__' ? '' : value)}>
|
|
260
|
+
<SelectTrigger>
|
|
261
|
+
<SelectValue placeholder={t('noneOption')} />
|
|
262
|
+
</SelectTrigger>
|
|
263
|
+
<SelectContent>
|
|
264
|
+
<SelectItem value="__none__">{t('noneOption')}</SelectItem>
|
|
265
|
+
{providerAliases.map((alias) => (
|
|
266
|
+
<SelectItem key={alias} value={alias}>
|
|
267
|
+
{alias}
|
|
268
|
+
</SelectItem>
|
|
269
|
+
))}
|
|
270
|
+
</SelectContent>
|
|
271
|
+
</Select>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className="space-y-2">
|
|
275
|
+
<Label>{t('defaultFileProvider')}</Label>
|
|
276
|
+
<Select value={defaultFile || '__none__'} onValueChange={(value) => setDefaultFile(value === '__none__' ? '' : value)}>
|
|
277
|
+
<SelectTrigger>
|
|
278
|
+
<SelectValue placeholder={t('noneOption')} />
|
|
279
|
+
</SelectTrigger>
|
|
280
|
+
<SelectContent>
|
|
281
|
+
<SelectItem value="__none__">{t('noneOption')}</SelectItem>
|
|
282
|
+
{providerAliases.map((alias) => (
|
|
283
|
+
<SelectItem key={alias} value={alias}>
|
|
284
|
+
{alias}
|
|
285
|
+
</SelectItem>
|
|
286
|
+
))}
|
|
287
|
+
</SelectContent>
|
|
288
|
+
</Select>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div className="space-y-2">
|
|
292
|
+
<Label>{t('defaultExecProvider')}</Label>
|
|
293
|
+
<Select value={defaultExec || '__none__'} onValueChange={(value) => setDefaultExec(value === '__none__' ? '' : value)}>
|
|
294
|
+
<SelectTrigger>
|
|
295
|
+
<SelectValue placeholder={t('noneOption')} />
|
|
296
|
+
</SelectTrigger>
|
|
297
|
+
<SelectContent>
|
|
298
|
+
<SelectItem value="__none__">{t('noneOption')}</SelectItem>
|
|
299
|
+
{providerAliases.map((alias) => (
|
|
300
|
+
<SelectItem key={alias} value={alias}>
|
|
301
|
+
{alias}
|
|
302
|
+
</SelectItem>
|
|
303
|
+
))}
|
|
304
|
+
</SelectContent>
|
|
305
|
+
</Select>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</CardContent>
|
|
309
|
+
</Card>
|
|
310
|
+
|
|
311
|
+
<Card>
|
|
312
|
+
<CardHeader>
|
|
313
|
+
<CardTitle>{t('secretProvidersTitle')}</CardTitle>
|
|
314
|
+
<CardDescription>{t('secretProvidersDescription')}</CardDescription>
|
|
315
|
+
</CardHeader>
|
|
316
|
+
<CardContent className="space-y-3">
|
|
317
|
+
{providers.map((provider, index) => (
|
|
318
|
+
<div key={`provider-${index}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
|
|
319
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
320
|
+
<Input
|
|
321
|
+
value={provider.alias}
|
|
322
|
+
onChange={(event) => updateProvider(index, { alias: event.target.value })}
|
|
323
|
+
placeholder={t('providerAlias')}
|
|
324
|
+
/>
|
|
325
|
+
<Select value={provider.source} onValueChange={(value) => updateProvider(index, { source: value as SecretSourceView })}>
|
|
326
|
+
<SelectTrigger>
|
|
327
|
+
<SelectValue />
|
|
328
|
+
</SelectTrigger>
|
|
329
|
+
<SelectContent>
|
|
330
|
+
{SOURCE_OPTIONS.map((source) => (
|
|
331
|
+
<SelectItem key={source} value={source}>
|
|
332
|
+
{source}
|
|
333
|
+
</SelectItem>
|
|
334
|
+
))}
|
|
335
|
+
</SelectContent>
|
|
336
|
+
</Select>
|
|
337
|
+
<Button type="button" variant="outline" onClick={() => setProviders((prev) => prev.filter((_, i) => i !== index))}>
|
|
338
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
339
|
+
{t('removeProvider')}
|
|
340
|
+
</Button>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{provider.source === 'env' && (
|
|
344
|
+
<Input
|
|
345
|
+
value={provider.prefix}
|
|
346
|
+
onChange={(event) => updateProvider(index, { prefix: event.target.value })}
|
|
347
|
+
placeholder={t('envPrefix')}
|
|
348
|
+
/>
|
|
349
|
+
)}
|
|
350
|
+
|
|
351
|
+
{provider.source === 'file' && (
|
|
352
|
+
<Input
|
|
353
|
+
value={provider.path}
|
|
354
|
+
onChange={(event) => updateProvider(index, { path: event.target.value })}
|
|
355
|
+
placeholder={t('secretFilePath')}
|
|
356
|
+
/>
|
|
357
|
+
)}
|
|
358
|
+
|
|
359
|
+
{provider.source === 'exec' && (
|
|
360
|
+
<div className="space-y-2">
|
|
361
|
+
<Input
|
|
362
|
+
value={provider.command}
|
|
363
|
+
onChange={(event) => updateProvider(index, { command: event.target.value })}
|
|
364
|
+
placeholder={t('secretExecCommand')}
|
|
365
|
+
/>
|
|
366
|
+
<textarea
|
|
367
|
+
className="min-h-[84px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
|
|
368
|
+
value={provider.argsText}
|
|
369
|
+
onChange={(event) => updateProvider(index, { argsText: event.target.value })}
|
|
370
|
+
placeholder={t('secretExecArgs')}
|
|
371
|
+
/>
|
|
372
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
373
|
+
<Input
|
|
374
|
+
value={provider.cwd}
|
|
375
|
+
onChange={(event) => updateProvider(index, { cwd: event.target.value })}
|
|
376
|
+
placeholder={t('secretExecCwd')}
|
|
377
|
+
/>
|
|
378
|
+
<Input
|
|
379
|
+
type="number"
|
|
380
|
+
min={1}
|
|
381
|
+
value={provider.timeoutMs}
|
|
382
|
+
onChange={(event) => updateProvider(index, { timeoutMs: Number.parseInt(event.target.value, 10) || 5000 })}
|
|
383
|
+
placeholder={t('secretExecTimeoutMs')}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
))}
|
|
390
|
+
|
|
391
|
+
<Button type="button" variant="outline" onClick={() => setProviders((prev) => [...prev, createProviderRow()])}>
|
|
392
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
393
|
+
{t('addSecretProvider')}
|
|
394
|
+
</Button>
|
|
395
|
+
</CardContent>
|
|
396
|
+
</Card>
|
|
397
|
+
|
|
398
|
+
<Card>
|
|
399
|
+
<CardHeader>
|
|
400
|
+
<CardTitle>{t('secretRefsTitle')}</CardTitle>
|
|
401
|
+
<CardDescription>{t('secretRefsDescription')}</CardDescription>
|
|
402
|
+
</CardHeader>
|
|
403
|
+
<CardContent className="space-y-3">
|
|
404
|
+
{refs.map((ref, index) => (
|
|
405
|
+
<div key={`ref-${index}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
|
|
406
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
407
|
+
<Input
|
|
408
|
+
value={ref.path}
|
|
409
|
+
onChange={(event) => updateRef(index, { path: event.target.value })}
|
|
410
|
+
placeholder={t('secretConfigPath')}
|
|
411
|
+
/>
|
|
412
|
+
<Input
|
|
413
|
+
value={ref.id}
|
|
414
|
+
onChange={(event) => updateRef(index, { id: event.target.value })}
|
|
415
|
+
placeholder={t('secretId')}
|
|
416
|
+
/>
|
|
417
|
+
<Select value={ref.source} onValueChange={(value) => updateRef(index, { source: value as SecretSourceView })}>
|
|
418
|
+
<SelectTrigger>
|
|
419
|
+
<SelectValue />
|
|
420
|
+
</SelectTrigger>
|
|
421
|
+
<SelectContent>
|
|
422
|
+
{SOURCE_OPTIONS.map((source) => (
|
|
423
|
+
<SelectItem key={source} value={source}>
|
|
424
|
+
{source}
|
|
425
|
+
</SelectItem>
|
|
426
|
+
))}
|
|
427
|
+
</SelectContent>
|
|
428
|
+
</Select>
|
|
429
|
+
<div className="grid grid-cols-[1fr_auto] gap-2">
|
|
430
|
+
<Select
|
|
431
|
+
value={ref.provider || '__none__'}
|
|
432
|
+
onValueChange={(value) => updateRef(index, { provider: value === '__none__' ? '' : value })}
|
|
433
|
+
>
|
|
434
|
+
<SelectTrigger>
|
|
435
|
+
<SelectValue placeholder={t('secretProviderAlias')} />
|
|
436
|
+
</SelectTrigger>
|
|
437
|
+
<SelectContent>
|
|
438
|
+
<SelectItem value="__none__">{t('noneOption')}</SelectItem>
|
|
439
|
+
{providerAliases.map((alias) => (
|
|
440
|
+
<SelectItem key={alias} value={alias}>
|
|
441
|
+
{alias}
|
|
442
|
+
</SelectItem>
|
|
443
|
+
))}
|
|
444
|
+
</SelectContent>
|
|
445
|
+
</Select>
|
|
446
|
+
<Button type="button" variant="outline" onClick={() => setRefs((prev) => prev.filter((_, i) => i !== index))}>
|
|
447
|
+
<Trash2 className="h-4 w-4" />
|
|
448
|
+
</Button>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
))}
|
|
453
|
+
|
|
454
|
+
<Button type="button" variant="outline" onClick={() => setRefs((prev) => [...prev, createRefRow()])}>
|
|
455
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
456
|
+
{t('addSecretRef')}
|
|
457
|
+
</Button>
|
|
458
|
+
</CardContent>
|
|
459
|
+
</Card>
|
|
460
|
+
|
|
461
|
+
<div className="flex justify-end">
|
|
462
|
+
<Button type="button" onClick={handleSave} disabled={updateSecrets.isPending}>
|
|
463
|
+
<Save className="h-4 w-4 mr-2" />
|
|
464
|
+
{updateSecrets.isPending ? t('saving') : t('save')}
|
|
465
|
+
</Button>
|
|
466
|
+
</div>
|
|
467
|
+
</PageLayout>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
2
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
|
|
3
3
|
import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
4
|
-
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette } from 'lucide-react';
|
|
4
|
+
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound } from 'lucide-react';
|
|
5
5
|
import { NavLink } from 'react-router-dom';
|
|
6
6
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
7
7
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
@@ -66,6 +66,11 @@ export function Sidebar() {
|
|
|
66
66
|
label: t('cron'),
|
|
67
67
|
icon: AlarmClock,
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
target: '/secrets',
|
|
71
|
+
label: t('secrets'),
|
|
72
|
+
icon: KeyRound,
|
|
73
|
+
},
|
|
69
74
|
{
|
|
70
75
|
target: '/marketplace/plugins',
|
|
71
76
|
label: t('marketplaceFilterPlugins'),
|
package/src/hooks/useConfig.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
updateProvider,
|
|
8
8
|
updateChannel,
|
|
9
9
|
updateRuntime,
|
|
10
|
+
updateSecrets,
|
|
10
11
|
executeConfigAction,
|
|
11
12
|
fetchSessions,
|
|
12
13
|
fetchSessionHistory,
|
|
@@ -109,6 +110,22 @@ export function useUpdateRuntime() {
|
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
export function useUpdateSecrets() {
|
|
114
|
+
const queryClient = useQueryClient();
|
|
115
|
+
|
|
116
|
+
return useMutation({
|
|
117
|
+
mutationFn: ({ data }: { data: unknown }) =>
|
|
118
|
+
updateSecrets(data as Parameters<typeof updateSecrets>[0]),
|
|
119
|
+
onSuccess: () => {
|
|
120
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
121
|
+
toast.success(t('configSavedApplied'));
|
|
122
|
+
},
|
|
123
|
+
onError: (error: Error) => {
|
|
124
|
+
toast.error(t('configSaveFailed') + ': ' + error.message);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
112
129
|
export function useExecuteConfigAction() {
|
|
113
130
|
return useMutation({
|
|
114
131
|
mutationFn: ({ actionId, data }: { actionId: string; data: unknown }) =>
|
package/src/lib/i18n.ts
CHANGED
|
@@ -128,6 +128,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
128
128
|
providers: { zh: '提供商', en: 'Providers' },
|
|
129
129
|
channels: { zh: '渠道', en: 'Channels' },
|
|
130
130
|
cron: { zh: '定时任务', en: 'Cron Jobs' },
|
|
131
|
+
secrets: { zh: '密钥管理', en: 'Secrets' },
|
|
131
132
|
runtime: { zh: '路由与运行时', en: 'Routing & Runtime' },
|
|
132
133
|
marketplace: { zh: '市场', en: 'Marketplace' },
|
|
133
134
|
|
|
@@ -149,10 +150,14 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
149
150
|
all: { zh: '全部', en: 'All' },
|
|
150
151
|
prev: { zh: '上一页', en: 'Prev' },
|
|
151
152
|
next: { zh: '下一页', en: 'Next' },
|
|
153
|
+
noneOption: { zh: '无', en: 'None' },
|
|
152
154
|
language: { zh: '语言', en: 'Language' },
|
|
153
155
|
theme: { zh: '主题', en: 'Theme' },
|
|
154
156
|
themeWarm: { zh: '暖色', en: 'Warm' },
|
|
155
157
|
themeCool: { zh: '冷色', en: 'Cool' },
|
|
158
|
+
isRequired: { zh: '必填', en: 'is required' },
|
|
159
|
+
duplicate: { zh: '重复', en: 'duplicate' },
|
|
160
|
+
notFound: { zh: '未找到', en: 'not found' },
|
|
156
161
|
|
|
157
162
|
// Model
|
|
158
163
|
modelPageTitle: { zh: '模型配置', en: 'Model Configuration' },
|
|
@@ -315,6 +320,43 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
315
320
|
addBinding: { zh: '添加绑定', en: 'Add Binding' },
|
|
316
321
|
saveRuntimeSettings: { zh: '保存运行时设置', en: 'Save Runtime Settings' },
|
|
317
322
|
|
|
323
|
+
// Secrets
|
|
324
|
+
secretsPageTitle: { zh: '密钥管理', en: 'Secrets Management' },
|
|
325
|
+
secretsPageDescription: {
|
|
326
|
+
zh: '集中管理 secrets.providers、secrets.defaults 与 secrets.refs。',
|
|
327
|
+
en: 'Manage secrets.providers, secrets.defaults, and secrets.refs in one place.'
|
|
328
|
+
},
|
|
329
|
+
secretsEnabledHelp: {
|
|
330
|
+
zh: '关闭后不会解析 `{{secret:*}}` 引用。',
|
|
331
|
+
en: 'When disabled, `{{secret:*}}` refs are not resolved.'
|
|
332
|
+
},
|
|
333
|
+
defaultEnvProvider: { zh: '默认 Env 提供器', en: 'Default Env Provider' },
|
|
334
|
+
defaultFileProvider: { zh: '默认 File 提供器', en: 'Default File Provider' },
|
|
335
|
+
defaultExecProvider: { zh: '默认 Exec 提供器', en: 'Default Exec Provider' },
|
|
336
|
+
secretProvidersTitle: { zh: 'Secret Providers', en: 'Secret Providers' },
|
|
337
|
+
secretProvidersDescription: {
|
|
338
|
+
zh: '定义可复用的 secrets provider(env/file/exec)。',
|
|
339
|
+
en: 'Define reusable secret providers (env/file/exec).'
|
|
340
|
+
},
|
|
341
|
+
providerAlias: { zh: '提供器别名', en: 'Provider Alias' },
|
|
342
|
+
removeProvider: { zh: '移除提供器', en: 'Remove Provider' },
|
|
343
|
+
envPrefix: { zh: '环境变量前缀', en: 'Environment Prefix' },
|
|
344
|
+
secretFilePath: { zh: 'Secrets 文件路径', en: 'Secrets File Path' },
|
|
345
|
+
secretExecCommand: { zh: '执行命令', en: 'Exec Command' },
|
|
346
|
+
secretExecArgs: { zh: '命令参数(每行一个)', en: 'Exec Args (one per line)' },
|
|
347
|
+
secretExecCwd: { zh: '执行目录(可选)', en: 'Exec Working Directory (optional)' },
|
|
348
|
+
secretExecTimeoutMs: { zh: '超时(毫秒)', en: 'Timeout (ms)' },
|
|
349
|
+
addSecretProvider: { zh: '添加 Provider', en: 'Add Provider' },
|
|
350
|
+
secretRefsTitle: { zh: 'Secret Refs', en: 'Secret Refs' },
|
|
351
|
+
secretRefsDescription: {
|
|
352
|
+
zh: '把配置路径映射到 secret 引用(source/provider/id)。',
|
|
353
|
+
en: 'Map config paths to secret refs (source/provider/id).'
|
|
354
|
+
},
|
|
355
|
+
secretConfigPath: { zh: '配置路径', en: 'Config Path' },
|
|
356
|
+
secretId: { zh: 'Secret ID', en: 'Secret ID' },
|
|
357
|
+
secretProviderAlias: { zh: 'Provider 别名', en: 'Provider Alias' },
|
|
358
|
+
addSecretRef: { zh: '添加 Ref', en: 'Add Ref' },
|
|
359
|
+
|
|
318
360
|
// Sessions
|
|
319
361
|
sessionsPageTitle: { zh: '会话管理', en: 'Sessions' },
|
|
320
362
|
sessionsPageDescription: {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as v,j as a,ac as ae,M as S,ad as M,ae as N,aa as te,_ as ne,ab as le,af as se,ag as oe,n as H,ah as re,ai as ce,aj as ie}from"./vendor-RXIbhDBC.js";import{u as _,a as q,i as me,j as pe,I as D,g as de}from"./useConfig-vFQvF4kn.js";import{t as e,c as G,u as J,S as be,b as ue,d as ge,e as ye,g as xe}from"./index-CrUDzcei.js";import{D as he,a as fe,b as we,c as je,d as ve,e as ke}from"./dialog-DJs630RE.js";import{B as F,P as Ce,a as Pe}from"./page-layout-BVZlyPFt.js";import{L as Ne}from"./label-CXGuE6Oa.js";import{S as Se}from"./switch-BLF45eI3.js";import{h as T}from"./config-hints-CApS3K_7.js";import{T as Ie}from"./tabs-custom-DQ0GpEV5.js";import{C as De,a as Fe,L as Te,d as Me,S as Ae,b as Le,c as Ue,A as Ee}from"./action-link-C9xMkxl2.js";function Be({value:n,onChange:d,className:i,placeholder:m=""}){const[c,y]=v.useState(""),b=u=>{u.key==="Enter"&&c.trim()?(u.preventDefault(),d([...n,c.trim()]),y("")):u.key==="Backspace"&&!c&&n.length>0&&d(n.slice(0,-1))},f=u=>{d(n.filter((o,g)=>g!==u))};return a.jsxs("div",{className:G("flex flex-wrap gap-2 p-2 border rounded-md min-h-[42px]",i),children:[n.map((u,o)=>a.jsxs("span",{className:"inline-flex items-center gap-1 px-2 py-1 bg-primary text-primary-foreground rounded text-sm",children:[u,a.jsx("button",{type:"button",onClick:()=>f(o),className:"hover:text-red-300 transition-colors",children:a.jsx(ae,{className:"h-3 w-3"})})]},o)),a.jsx("input",{type:"text",value:c,onChange:u=>y(u.target.value),onKeyDown:b,className:"flex-1 outline-none min-w-[100px] bg-transparent text-sm",placeholder:m||e("enterTag")})]})}const U=[{value:"pairing",label:"pairing"},{value:"allowlist",label:"allowlist"},{value:"open",label:"open"},{value:"disabled",label:"disabled"}],E=[{value:"open",label:"open"},{value:"allowlist",label:"allowlist"},{value:"disabled",label:"disabled"}],Oe=[{value:"off",label:"off"},{value:"partial",label:"partial"},{value:"block",label:"block"},{value:"progress",label:"progress"}],$e=n=>n.includes("token")||n.includes("secret")||n.includes("password")?a.jsx(te,{className:"h-3.5 w-3.5 text-gray-500"}):n.includes("url")||n.includes("host")?a.jsx(ne,{className:"h-3.5 w-3.5 text-gray-500"}):n.includes("email")||n.includes("mail")?a.jsx(M,{className:"h-3.5 w-3.5 text-gray-500"}):n.includes("id")||n.includes("from")?a.jsx(le,{className:"h-3.5 w-3.5 text-gray-500"}):n==="enabled"||n==="consentGranted"?a.jsx(se,{className:"h-3.5 w-3.5 text-gray-500"}):a.jsx(oe,{className:"h-3.5 w-3.5 text-gray-500"});function B(){return{telegram:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"token",type:"password",label:e("botToken")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"proxy",type:"text",label:e("proxy")},{name:"accountId",type:"text",label:e("accountId")},{name:"dmPolicy",type:"select",label:e("dmPolicy"),options:U},{name:"groupPolicy",type:"select",label:e("groupPolicy"),options:E},{name:"groupAllowFrom",type:"tags",label:e("groupAllowFrom")},{name:"requireMention",type:"boolean",label:e("requireMention")},{name:"mentionPatterns",type:"tags",label:e("mentionPatterns")},{name:"groups",type:"json",label:e("groupRulesJson")}],discord:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"token",type:"password",label:e("botToken")},{name:"allowBots",type:"boolean",label:e("allowBotMessages")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"gatewayUrl",type:"text",label:e("gatewayUrl")},{name:"intents",type:"number",label:e("intents")},{name:"proxy",type:"text",label:e("proxy")},{name:"mediaMaxMb",type:"number",label:e("attachmentMaxSizeMb")},{name:"streaming",type:"select",label:e("streamingMode"),options:Oe},{name:"draftChunk",type:"json",label:e("draftChunkingJson")},{name:"textChunkLimit",type:"number",label:e("textChunkLimit")},{name:"accountId",type:"text",label:e("accountId")},{name:"dmPolicy",type:"select",label:e("dmPolicy"),options:U},{name:"groupPolicy",type:"select",label:e("groupPolicy"),options:E},{name:"groupAllowFrom",type:"tags",label:e("groupAllowFrom")},{name:"requireMention",type:"boolean",label:e("requireMention")},{name:"mentionPatterns",type:"tags",label:e("mentionPatterns")},{name:"groups",type:"json",label:e("groupRulesJson")}],whatsapp:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"bridgeUrl",type:"text",label:e("bridgeUrl")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],feishu:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"appId",type:"text",label:e("appId")},{name:"appSecret",type:"password",label:e("appSecret")},{name:"encryptKey",type:"password",label:e("encryptKey")},{name:"verificationToken",type:"password",label:e("verificationToken")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],dingtalk:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"clientId",type:"text",label:e("clientId")},{name:"clientSecret",type:"password",label:e("clientSecret")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],wecom:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"corpId",type:"text",label:e("corpId")},{name:"agentId",type:"text",label:e("agentId")},{name:"secret",type:"password",label:e("secret")},{name:"token",type:"password",label:e("token")},{name:"callbackPort",type:"number",label:e("callbackPort")},{name:"callbackPath",type:"text",label:e("callbackPath")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],slack:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"mode",type:"text",label:e("mode")},{name:"webhookPath",type:"text",label:e("webhookPath")},{name:"allowBots",type:"boolean",label:e("allowBotMessages")},{name:"botToken",type:"password",label:e("botToken")},{name:"appToken",type:"password",label:e("appToken")}],email:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"consentGranted",type:"boolean",label:e("consentGranted")},{name:"imapHost",type:"text",label:e("imapHost")},{name:"imapPort",type:"number",label:e("imapPort")},{name:"imapUsername",type:"text",label:e("imapUsername")},{name:"imapPassword",type:"password",label:e("imapPassword")},{name:"fromAddress",type:"email",label:e("fromAddress")}],mochat:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"baseUrl",type:"text",label:e("baseUrl")},{name:"clawToken",type:"password",label:e("clawToken")},{name:"agentUserId",type:"text",label:e("agentUserId")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],qq:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"appId",type:"text",label:e("appId")},{name:"secret",type:"password",label:e("appSecret")},{name:"markdownSupport",type:"boolean",label:e("markdownSupport")},{name:"allowFrom",type:"tags",label:e("allowFrom")}]}}const O={telegram:S,slack:S,email:M,default:S},$={telegram:"from-primary-300 to-primary-600",slack:"from-primary-200 to-primary-500",email:"from-primary-100 to-primary-400",default:"from-gray-300 to-gray-500"};function I(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}function K(n,d){const i={...n};for(const[m,c]of Object.entries(d)){const y=i[m];if(I(y)&&I(c)){i[m]=K(y,c);continue}i[m]=c}return i}function Re(n,d){const i=n.split("."),m={};let c=m;for(let y=0;y<i.length-1;y+=1){const b=i[y];c[b]={},c=c[b]}return c[i[i.length-1]]=d,m}function He(){var A,L;const{channelModal:n,closeChannelModal:d}=J(),{data:i}=_(),{data:m}=q(),c=me(),y=pe(),[b,f]=v.useState({}),[u,o]=v.useState({}),[g,x]=v.useState(null),l=n.channel,w=l?i==null?void 0:i.channels[l]:null,k=l?B()[l]??[]:[],C=m==null?void 0:m.uiHints,P=l?`channels.${l}`:null,z=((A=m==null?void 0:m.actions)==null?void 0:A.filter(t=>t.scope===P))??[],V=l&&(((L=T(`channels.${l}`,C))==null?void 0:L.label)??l);v.useEffect(()=>{if(w){f({...w});const t={};(l?B()[l]??[]:[]).filter(r=>r.type==="json").forEach(r=>{const h=w[r.name];t[r.name]=JSON.stringify(h??{},null,2)}),o(t)}else f({}),o({})},[w,l]);const j=(t,s)=>{f(r=>({...r,[t]:s}))},Y=t=>{if(t.preventDefault(),!l)return;const s={...b};for(const r of k){if(r.type!=="json")continue;const h=u[r.name]??"";try{s[r.name]=h.trim()?JSON.parse(h):{}}catch{N.error(`${e("invalidJson")}: ${r.name}`);return}}c.mutate({channel:l,data:s},{onSuccess:()=>d()})},W=t=>{if(!t||!l)return;const s=t.channels;if(!I(s))return;const r=s[l];I(r)&&f(h=>K(h,r))},X=async t=>{if(!(!l||!P)){x(t.id);try{let s={...b};t.saveBeforeRun&&(s={...s,...t.savePatch??{}},f(s),await c.mutateAsync({channel:l,data:s}));const r=await y.mutateAsync({actionId:t.id,data:{scope:P,draftConfig:Re(P,s)}});W(r.patch),r.ok?N.success(r.message||e("success")):N.error(r.message||e("error"))}catch(s){const r=s instanceof Error?s.message:String(s);N.error(`${e("error")}: ${r}`)}finally{x(null)}}},Q=O[l||""]||O.default,Z=$[l||""]||$.default;return a.jsx(he,{open:n.open,onOpenChange:d,children:a.jsxs(fe,{className:"sm:max-w-[550px] max-h-[85vh] overflow-hidden flex flex-col",children:[a.jsx(we,{children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:`h-10 w-10 rounded-xl bg-gradient-to-br ${Z} flex items-center justify-center`,children:a.jsx(Q,{className:"h-5 w-5 text-white"})}),a.jsxs("div",{children:[a.jsx(je,{className:"capitalize",children:V}),a.jsx(ve,{children:e("configureMessageChannelParameters")})]})]})}),a.jsxs("form",{onSubmit:Y,className:"flex flex-col flex-1 overflow-hidden",children:[a.jsx("div",{className:"flex-1 overflow-y-auto custom-scrollbar py-2 pr-2 space-y-5",children:k.map(t=>{const s=l?T(`channels.${l}.${t.name}`,C):void 0,r=(s==null?void 0:s.label)??t.label,h=s==null?void 0:s.placeholder;return a.jsxs("div",{className:"space-y-2.5",children:[a.jsxs(Ne,{htmlFor:t.name,className:"text-sm font-medium text-gray-900 flex items-center gap-2",children:[$e(t.name),r]}),t.type==="boolean"&&a.jsxs("div",{className:"flex items-center justify-between p-3 rounded-xl bg-gray-50",children:[a.jsx("span",{className:"text-sm text-gray-500",children:b[t.name]?e("enabled"):e("disabled")}),a.jsx(Se,{id:t.name,checked:b[t.name]||!1,onCheckedChange:p=>j(t.name,p),className:"data-[state=checked]:bg-emerald-500"})]}),(t.type==="text"||t.type==="email")&&a.jsx(D,{id:t.name,type:t.type,value:b[t.name]||"",onChange:p=>j(t.name,p.target.value),placeholder:h,className:"rounded-xl"}),t.type==="password"&&a.jsx(D,{id:t.name,type:"password",value:b[t.name]||"",onChange:p=>j(t.name,p.target.value),placeholder:h??e("leaveBlankToKeepUnchanged"),className:"rounded-xl"}),t.type==="number"&&a.jsx(D,{id:t.name,type:"number",value:b[t.name]||0,onChange:p=>j(t.name,parseInt(p.target.value)||0),placeholder:h,className:"rounded-xl"}),t.type==="tags"&&a.jsx(Be,{value:b[t.name]||[],onChange:p=>j(t.name,p)}),t.type==="select"&&a.jsxs(be,{value:b[t.name]||"",onValueChange:p=>j(t.name,p),children:[a.jsx(ue,{className:"rounded-xl",children:a.jsx(ge,{})}),a.jsx(ye,{children:(t.options??[]).map(p=>a.jsx(xe,{value:p.value,children:p.label},p.value))})]}),t.type==="json"&&a.jsx("textarea",{id:t.name,value:u[t.name]??"{}",onChange:p=>o(ee=>({...ee,[t.name]:p.target.value})),className:"min-h-[120px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"})]},t.name)})}),a.jsxs(ke,{className:"pt-4 flex-shrink-0",children:[a.jsx(F,{type:"button",variant:"outline",onClick:d,children:e("cancel")}),a.jsx(F,{type:"submit",disabled:c.isPending||!!g,children:c.isPending?e("saving"):e("save")}),z.filter(t=>t.trigger==="manual").map(t=>a.jsx(F,{type:"button",onClick:()=>X(t),disabled:c.isPending||!!g,variant:"secondary",children:g===t.id?e("connecting"):t.title},t.id))]})]})]})})}const R={telegram:S,slack:ce,email:M,webhook:re,default:H},_e={telegram:"channelDescTelegram",slack:"channelDescSlack",email:"channelDescEmail",webhook:"channelDescWebhook",discord:"channelDescDiscord",feishu:"channelDescFeishu"};function Ze(){const{data:n}=_(),{data:d}=de(),{data:i}=q(),{openChannelModal:m}=J(),[c,y]=v.useState("active"),b=i==null?void 0:i.uiHints;if(!n||!d)return a.jsx("div",{className:"p-8 text-gray-400",children:e("channelsLoading")});const f=[{id:"active",label:e("channelsTabEnabled"),count:d.channels.filter(o=>{var g;return(g=n.channels[o.name])==null?void 0:g.enabled}).length},{id:"all",label:e("channelsTabAll"),count:d.channels.length}],u=d.channels.filter(o=>{var x;const g=((x=n.channels[o.name])==null?void 0:x.enabled)||!1;return c==="all"||g});return a.jsxs(Ce,{children:[a.jsx(Pe,{title:e("channelsPageTitle")}),a.jsx(Ie,{tabs:f,activeTab:c,onChange:y}),a.jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4",children:u.map(o=>{const g=n.channels[o.name],x=(g==null?void 0:g.enabled)||!1,l=R[o.name]||R.default,w=T(`channels.${o.name}`,b),k=(w==null?void 0:w.help)||e(_e[o.name]||"channelDescriptionDefault");return a.jsxs(De,{onClick:()=>m(o.name),children:[a.jsxs(Fe,{children:[a.jsx(Te,{name:o.name,src:Me(o.name),className:G("h-11 w-11 rounded-xl border transition-all",x?"bg-white border-primary/30":"bg-white border-gray-200/60 group-hover:border-gray-300"),imgClassName:"h-5 w-5",fallback:a.jsx(l,{className:"h-5 w-5"})}),a.jsx(Ae,{status:x?"active":"inactive",label:x?e("statusActive"):e("statusInactive")})]}),a.jsx(Le,{title:o.displayName||o.name,description:k}),a.jsxs(Ue,{children:[a.jsx(Ee,{label:x?e("actionConfigure"):e("actionEnable")}),o.tutorialUrl&&a.jsx("a",{href:o.tutorialUrl,target:"_blank",rel:"noreferrer",onClick:C=>C.stopPropagation(),className:"flex items-center justify-center h-6 w-6 rounded-md text-gray-300 hover:text-gray-500 hover:bg-gray-100/60 transition-colors",title:e("channelsGuideTitle"),children:a.jsx(ie,{className:"h-3.5 w-3.5"})})]})]},o.name)})}),u.length===0&&a.jsxs("div",{className:"flex flex-col items-center justify-center py-16 text-center",children:[a.jsx("div",{className:"h-14 w-14 flex items-center justify-center rounded-xl bg-gray-100/80 mb-4",children:a.jsx(H,{className:"h-6 w-6 text-gray-300"})}),a.jsx("h3",{className:"text-[14px] font-semibold text-gray-900 mb-1.5",children:e("channelsEmptyTitle")}),a.jsx("p",{className:"text-[13px] text-gray-400 max-w-sm",children:e("channelsEmptyDescription")})]}),a.jsx(He,{})]})}export{Ze as ChannelsList};
|