@nextclaw/ui 0.5.9 → 0.5.11
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 +12 -0
- package/dist/assets/index-BGzsyzDd.css +1 -0
- package/dist/assets/index-BHB8zYn7.js +342 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/components/common/MaskedInput.tsx +1 -1
- package/src/components/common/StatusBadge.tsx +3 -5
- package/src/components/common/TagInput.tsx +3 -2
- package/src/components/config/ChannelForm.tsx +106 -104
- package/src/components/config/ChannelsList.tsx +22 -23
- package/src/components/config/CronConfig.tsx +20 -30
- package/src/components/config/ModelConfig.tsx +11 -12
- package/src/components/config/ProviderForm.tsx +3 -3
- package/src/components/config/ProvidersList.tsx +13 -13
- package/src/components/config/RuntimeConfig.tsx +40 -43
- package/src/components/config/SessionsConfig.tsx +12 -24
- package/src/components/layout/Sidebar.tsx +70 -40
- package/src/components/layout/page-layout.tsx +71 -0
- package/src/components/marketplace/MarketplacePage.tsx +34 -35
- package/src/components/providers/I18nProvider.tsx +64 -0
- package/src/components/ui/confirm-dialog.tsx +3 -2
- package/src/components/ui/tabs-custom.tsx +2 -1
- package/src/hooks/useConfirmDialog.tsx +5 -4
- package/src/hooks/useMarketplace.ts +4 -3
- package/src/lib/i18n.ts +267 -5
- package/src/main.tsx +6 -3
- package/dist/assets/index-BtwwwWcv.css +0 -1
- package/dist/assets/index-STUSj6p9.js +0 -337
package/dist/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>NextClaw - 系统配置</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BHB8zYn7.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BGzsyzDd.css">
|
|
11
11
|
</head>
|
|
12
12
|
|
|
13
13
|
<body>
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@ export function MaskedInput({ maskedValue, isSet, className, ...props }: MaskedI
|
|
|
18
18
|
<Input
|
|
19
19
|
type={showKey ? 'text' : 'password'}
|
|
20
20
|
className={cn('pr-20', className)}
|
|
21
|
-
placeholder={isSet ? `${t('apiKeySet')} (
|
|
21
|
+
placeholder={isSet ? `${t('apiKeySet')} (${t('unchanged')})` : ''}
|
|
22
22
|
{...props}
|
|
23
23
|
/>
|
|
24
24
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1">
|
|
@@ -11,22 +11,19 @@ interface StatusBadgeProps {
|
|
|
11
11
|
|
|
12
12
|
const statusConfig: Record<
|
|
13
13
|
Status,
|
|
14
|
-
{
|
|
14
|
+
{ dotClass: string; textClass: string; bgClass: string }
|
|
15
15
|
> = {
|
|
16
16
|
connected: {
|
|
17
|
-
label: t('connected'),
|
|
18
17
|
dotClass: 'bg-emerald-500',
|
|
19
18
|
textClass: 'text-emerald-600',
|
|
20
19
|
bgClass: 'bg-emerald-50',
|
|
21
20
|
},
|
|
22
21
|
disconnected: {
|
|
23
|
-
label: t('disconnected'),
|
|
24
22
|
dotClass: 'bg-gray-300',
|
|
25
23
|
textClass: 'text-gray-400',
|
|
26
24
|
bgClass: 'bg-gray-100/80',
|
|
27
25
|
},
|
|
28
26
|
connecting: {
|
|
29
|
-
label: t('connecting'),
|
|
30
27
|
dotClass: 'bg-amber-400',
|
|
31
28
|
textClass: 'text-amber-600',
|
|
32
29
|
bgClass: 'bg-amber-50',
|
|
@@ -35,6 +32,7 @@ const statusConfig: Record<
|
|
|
35
32
|
|
|
36
33
|
export function StatusBadge({ status, className }: StatusBadgeProps) {
|
|
37
34
|
const config = statusConfig[status];
|
|
35
|
+
const label = status === 'connected' ? t('connected') : status === 'disconnected' ? t('disconnected') : t('connecting');
|
|
38
36
|
|
|
39
37
|
return (
|
|
40
38
|
<div className={cn(
|
|
@@ -44,7 +42,7 @@ export function StatusBadge({ status, className }: StatusBadgeProps) {
|
|
|
44
42
|
)}>
|
|
45
43
|
<span className={cn('h-1.5 w-1.5 rounded-full', config.dotClass)} />
|
|
46
44
|
<span className={cn('text-[11px] font-medium flex items-center gap-1', config.textClass)}>
|
|
47
|
-
{
|
|
45
|
+
{label}
|
|
48
46
|
{status === 'connecting' && <Loader2 className="h-2.5 w-2.5 animate-spin" />}
|
|
49
47
|
</span>
|
|
50
48
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { X } from 'lucide-react';
|
|
3
3
|
import { cn } from '@/lib/utils';
|
|
4
|
+
import { t } from '@/lib/i18n';
|
|
4
5
|
|
|
5
6
|
interface TagInputProps {
|
|
6
7
|
value: string[];
|
|
@@ -9,7 +10,7 @@ interface TagInputProps {
|
|
|
9
10
|
placeholder?: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function TagInput({ value, onChange, className, placeholder = '
|
|
13
|
+
export function TagInput({ value, onChange, className, placeholder = '' }: TagInputProps) {
|
|
13
14
|
const [input, setInput] = useState('');
|
|
14
15
|
|
|
15
16
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
@@ -49,7 +50,7 @@ export function TagInput({ value, onChange, className, placeholder = 'Type and p
|
|
|
49
50
|
onChange={(e) => setInput(e.target.value)}
|
|
50
51
|
onKeyDown={handleKeyDown}
|
|
51
52
|
className="flex-1 outline-none min-w-[100px] bg-transparent text-sm"
|
|
52
|
-
placeholder={placeholder}
|
|
53
|
+
placeholder={placeholder || t('enterTag')}
|
|
53
54
|
/>
|
|
54
55
|
</div>
|
|
55
56
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
2
|
import { useConfig, useConfigSchema, useUpdateChannel, useExecuteConfigAction } from '@/hooks/useConfig';
|
|
3
3
|
import { useUiStore } from '@/stores/ui.store';
|
|
4
4
|
import {
|
|
@@ -65,102 +65,103 @@ const getFieldIcon = (fieldName: string) => {
|
|
|
65
65
|
return <Settings className="h-3.5 w-3.5 text-gray-500" />;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
68
|
+
function buildChannelFields(): Record<string, ChannelField[]> {
|
|
69
|
+
return {
|
|
70
|
+
telegram: [
|
|
71
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
72
|
+
{ name: 'token', type: 'password', label: t('botToken') },
|
|
73
|
+
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') },
|
|
74
|
+
{ name: 'proxy', type: 'text', label: t('proxy') },
|
|
75
|
+
{ name: 'accountId', type: 'text', label: t('accountId') },
|
|
76
|
+
{ name: 'dmPolicy', type: 'select', label: t('dmPolicy'), options: DM_POLICY_OPTIONS },
|
|
77
|
+
{ name: 'groupPolicy', type: 'select', label: t('groupPolicy'), options: GROUP_POLICY_OPTIONS },
|
|
78
|
+
{ name: 'groupAllowFrom', type: 'tags', label: t('groupAllowFrom') },
|
|
79
|
+
{ name: 'requireMention', type: 'boolean', label: t('requireMention') },
|
|
80
|
+
{ name: 'mentionPatterns', type: 'tags', label: t('mentionPatterns') },
|
|
81
|
+
{ name: 'groups', type: 'json', label: t('groupRulesJson') }
|
|
82
|
+
],
|
|
83
|
+
discord: [
|
|
84
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
85
|
+
{ name: 'token', type: 'password', label: t('botToken') },
|
|
86
|
+
{ name: 'allowBots', type: 'boolean', label: t('allowBotMessages') },
|
|
87
|
+
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') },
|
|
88
|
+
{ name: 'gatewayUrl', type: 'text', label: t('gatewayUrl') },
|
|
89
|
+
{ name: 'intents', type: 'number', label: t('intents') },
|
|
90
|
+
{ name: 'proxy', type: 'text', label: t('proxy') },
|
|
91
|
+
{ name: 'mediaMaxMb', type: 'number', label: t('attachmentMaxSizeMb') },
|
|
92
|
+
{ name: 'streaming', type: 'select', label: t('streamingMode'), options: STREAMING_MODE_OPTIONS },
|
|
93
|
+
{ name: 'draftChunk', type: 'json', label: t('draftChunkingJson') },
|
|
94
|
+
{ name: 'textChunkLimit', type: 'number', label: t('textChunkLimit') },
|
|
95
|
+
{ name: 'accountId', type: 'text', label: t('accountId') },
|
|
96
|
+
{ name: 'dmPolicy', type: 'select', label: t('dmPolicy'), options: DM_POLICY_OPTIONS },
|
|
97
|
+
{ name: 'groupPolicy', type: 'select', label: t('groupPolicy'), options: GROUP_POLICY_OPTIONS },
|
|
98
|
+
{ name: 'groupAllowFrom', type: 'tags', label: t('groupAllowFrom') },
|
|
99
|
+
{ name: 'requireMention', type: 'boolean', label: t('requireMention') },
|
|
100
|
+
{ name: 'mentionPatterns', type: 'tags', label: t('mentionPatterns') },
|
|
101
|
+
{ name: 'groups', type: 'json', label: t('groupRulesJson') }
|
|
102
|
+
],
|
|
103
|
+
whatsapp: [
|
|
104
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
105
|
+
{ name: 'bridgeUrl', type: 'text', label: t('bridgeUrl') },
|
|
106
|
+
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
107
|
+
],
|
|
108
|
+
feishu: [
|
|
109
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
110
|
+
{ name: 'appId', type: 'text', label: t('appId') },
|
|
111
|
+
{ name: 'appSecret', type: 'password', label: t('appSecret') },
|
|
112
|
+
{ name: 'encryptKey', type: 'password', label: t('encryptKey') },
|
|
113
|
+
{ name: 'verificationToken', type: 'password', label: t('verificationToken') },
|
|
114
|
+
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
115
|
+
],
|
|
116
|
+
dingtalk: [
|
|
117
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
118
|
+
{ name: 'clientId', type: 'text', label: t('clientId') },
|
|
119
|
+
{ name: 'clientSecret', type: 'password', label: t('clientSecret') },
|
|
120
|
+
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
121
|
+
],
|
|
122
|
+
wecom: [
|
|
123
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
124
|
+
{ name: 'corpId', type: 'text', label: t('corpId') },
|
|
125
|
+
{ name: 'agentId', type: 'text', label: t('agentId') },
|
|
126
|
+
{ name: 'secret', type: 'password', label: t('secret') },
|
|
127
|
+
{ name: 'token', type: 'password', label: t('token') },
|
|
128
|
+
{ name: 'callbackPort', type: 'number', label: t('callbackPort') },
|
|
129
|
+
{ name: 'callbackPath', type: 'text', label: t('callbackPath') },
|
|
130
|
+
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
131
|
+
],
|
|
132
|
+
slack: [
|
|
133
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
134
|
+
{ name: 'mode', type: 'text', label: t('mode') },
|
|
135
|
+
{ name: 'webhookPath', type: 'text', label: t('webhookPath') },
|
|
136
|
+
{ name: 'allowBots', type: 'boolean', label: t('allowBotMessages') },
|
|
137
|
+
{ name: 'botToken', type: 'password', label: t('botToken') },
|
|
138
|
+
{ name: 'appToken', type: 'password', label: t('appToken') }
|
|
139
|
+
],
|
|
140
|
+
email: [
|
|
141
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
142
|
+
{ name: 'consentGranted', type: 'boolean', label: t('consentGranted') },
|
|
143
|
+
{ name: 'imapHost', type: 'text', label: t('imapHost') },
|
|
144
|
+
{ name: 'imapPort', type: 'number', label: t('imapPort') },
|
|
145
|
+
{ name: 'imapUsername', type: 'text', label: t('imapUsername') },
|
|
146
|
+
{ name: 'imapPassword', type: 'password', label: t('imapPassword') },
|
|
147
|
+
{ name: 'fromAddress', type: 'email', label: t('fromAddress') }
|
|
148
|
+
],
|
|
149
|
+
mochat: [
|
|
150
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
151
|
+
{ name: 'baseUrl', type: 'text', label: t('baseUrl') },
|
|
152
|
+
{ name: 'clawToken', type: 'password', label: t('clawToken') },
|
|
153
|
+
{ name: 'agentUserId', type: 'text', label: t('agentUserId') },
|
|
154
|
+
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
155
|
+
],
|
|
156
|
+
qq: [
|
|
157
|
+
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
158
|
+
{ name: 'appId', type: 'text', label: t('appId') },
|
|
159
|
+
{ name: 'secret', type: 'password', label: t('appSecret') },
|
|
160
|
+
{ name: 'markdownSupport', type: 'boolean', label: t('markdownSupport') },
|
|
161
|
+
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') }
|
|
162
|
+
]
|
|
163
|
+
};
|
|
164
|
+
}
|
|
164
165
|
|
|
165
166
|
const channelIcons: Record<string, typeof MessageCircle> = {
|
|
166
167
|
telegram: MessageCircle,
|
|
@@ -219,7 +220,7 @@ export function ChannelForm() {
|
|
|
219
220
|
|
|
220
221
|
const channelName = channelModal.channel;
|
|
221
222
|
const channelConfig = channelName ? config?.channels[channelName] : null;
|
|
222
|
-
const fields =
|
|
223
|
+
const fields = channelName ? buildChannelFields()[channelName] ?? [] : [];
|
|
223
224
|
const uiHints = schema?.uiHints;
|
|
224
225
|
const scope = channelName ? `channels.${channelName}` : null;
|
|
225
226
|
const actions = schema?.actions?.filter((action) => action.scope === scope) ?? [];
|
|
@@ -231,7 +232,8 @@ export function ChannelForm() {
|
|
|
231
232
|
if (channelConfig) {
|
|
232
233
|
setFormData({ ...channelConfig });
|
|
233
234
|
const nextDrafts: Record<string, string> = {};
|
|
234
|
-
|
|
235
|
+
const currentFields = channelName ? buildChannelFields()[channelName] ?? [] : [];
|
|
236
|
+
currentFields
|
|
235
237
|
.filter((field) => field.type === 'json')
|
|
236
238
|
.forEach((field) => {
|
|
237
239
|
const value = channelConfig[field.name];
|
|
@@ -242,7 +244,7 @@ export function ChannelForm() {
|
|
|
242
244
|
setFormData({});
|
|
243
245
|
setJsonDrafts({});
|
|
244
246
|
}
|
|
245
|
-
}, [channelConfig, channelName
|
|
247
|
+
}, [channelConfig, channelName]);
|
|
246
248
|
|
|
247
249
|
const updateField = (name: string, value: unknown) => {
|
|
248
250
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
|
@@ -262,7 +264,7 @@ export function ChannelForm() {
|
|
|
262
264
|
try {
|
|
263
265
|
payload[field.name] = raw.trim() ? JSON.parse(raw) : {};
|
|
264
266
|
} catch {
|
|
265
|
-
toast.error(
|
|
267
|
+
toast.error(`${t('invalidJson')}: ${field.name}`);
|
|
266
268
|
return;
|
|
267
269
|
}
|
|
268
270
|
}
|
|
@@ -342,7 +344,7 @@ export function ChannelForm() {
|
|
|
342
344
|
</div>
|
|
343
345
|
<div>
|
|
344
346
|
<DialogTitle className="capitalize">{channelLabel}</DialogTitle>
|
|
345
|
-
<DialogDescription>
|
|
347
|
+
<DialogDescription>{t('configureMessageChannelParameters')}</DialogDescription>
|
|
346
348
|
</div>
|
|
347
349
|
</div>
|
|
348
350
|
</DialogHeader>
|
|
@@ -397,7 +399,7 @@ export function ChannelForm() {
|
|
|
397
399
|
type="password"
|
|
398
400
|
value={(formData[field.name] as string) || ''}
|
|
399
401
|
onChange={(e) => updateField(field.name, e.target.value)}
|
|
400
|
-
placeholder={placeholder ?? '
|
|
402
|
+
placeholder={placeholder ?? t('leaveBlankToKeepUnchanged')}
|
|
401
403
|
className="rounded-xl"
|
|
402
404
|
/>
|
|
403
405
|
)}
|
|
@@ -468,7 +470,7 @@ export function ChannelForm() {
|
|
|
468
470
|
type="submit"
|
|
469
471
|
disabled={updateChannel.isPending || Boolean(runningActionId)}
|
|
470
472
|
>
|
|
471
|
-
{updateChannel.isPending ? '
|
|
473
|
+
{updateChannel.isPending ? t('saving') : t('save')}
|
|
472
474
|
</Button>
|
|
473
475
|
{actions
|
|
474
476
|
.filter((action) => action.trigger === 'manual')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useConfig, useConfigMeta, useConfigSchema } from '@/hooks/useConfig';
|
|
2
|
-
import { MessageCircle, Mail, MessageSquare, Slack, ExternalLink, Bell
|
|
2
|
+
import { MessageCircle, Mail, MessageSquare, Slack, ExternalLink, Bell } from 'lucide-react';
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { ChannelForm } from './ChannelForm';
|
|
5
5
|
import { useUiStore } from '@/stores/ui.store';
|
|
@@ -11,6 +11,8 @@ import { ConfigCard, ConfigCardHeader, ConfigCardBody, ConfigCardFooter } from '
|
|
|
11
11
|
import { StatusDot } from '@/components/ui/status-dot';
|
|
12
12
|
import { ActionLink } from '@/components/ui/action-link';
|
|
13
13
|
import { cn } from '@/lib/utils';
|
|
14
|
+
import { t } from '@/lib/i18n';
|
|
15
|
+
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
14
16
|
|
|
15
17
|
const channelIcons: Record<string, typeof MessageCircle> = {
|
|
16
18
|
telegram: MessageCircle,
|
|
@@ -20,13 +22,13 @@ const channelIcons: Record<string, typeof MessageCircle> = {
|
|
|
20
22
|
default: MessageSquare
|
|
21
23
|
};
|
|
22
24
|
|
|
23
|
-
const
|
|
24
|
-
telegram: '
|
|
25
|
-
slack: '
|
|
26
|
-
email: '
|
|
27
|
-
webhook: '
|
|
28
|
-
discord: '
|
|
29
|
-
feishu: '
|
|
25
|
+
const channelDescriptionKeys: Record<string, string> = {
|
|
26
|
+
telegram: 'channelDescTelegram',
|
|
27
|
+
slack: 'channelDescSlack',
|
|
28
|
+
email: 'channelDescEmail',
|
|
29
|
+
webhook: 'channelDescWebhook',
|
|
30
|
+
discord: 'channelDescDiscord',
|
|
31
|
+
feishu: 'channelDescFeishu'
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
export function ChannelsList() {
|
|
@@ -38,12 +40,12 @@ export function ChannelsList() {
|
|
|
38
40
|
const uiHints = schema?.uiHints;
|
|
39
41
|
|
|
40
42
|
if (!config || !meta) {
|
|
41
|
-
return <div className="p-8 text-gray-400">
|
|
43
|
+
return <div className="p-8 text-gray-400">{t('channelsLoading')}</div>;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
const tabs = [
|
|
45
|
-
{ id: 'active', label: '
|
|
46
|
-
{ id: 'all', label: '
|
|
47
|
+
{ id: 'active', label: t('channelsTabEnabled'), count: meta.channels.filter(c => config.channels[c.name]?.enabled).length },
|
|
48
|
+
{ id: 'all', label: t('channelsTabAll'), count: meta.channels.length }
|
|
47
49
|
];
|
|
48
50
|
|
|
49
51
|
const filteredChannels = meta.channels.filter(channel => {
|
|
@@ -52,10 +54,8 @@ export function ChannelsList() {
|
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
return (
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
<h2 className="text-xl font-semibold text-gray-900">Message Channels</h2>
|
|
58
|
-
</div>
|
|
57
|
+
<PageLayout>
|
|
58
|
+
<PageHeader title={t('channelsPageTitle')} />
|
|
59
59
|
|
|
60
60
|
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
|
|
61
61
|
|
|
@@ -68,8 +68,7 @@ export function ChannelsList() {
|
|
|
68
68
|
const channelHint = hintForPath(`channels.${channel.name}`, uiHints);
|
|
69
69
|
const description =
|
|
70
70
|
channelHint?.help ||
|
|
71
|
-
|
|
72
|
-
'Configure this communication channel';
|
|
71
|
+
t(channelDescriptionKeys[channel.name] || 'channelDescriptionDefault');
|
|
73
72
|
|
|
74
73
|
return (
|
|
75
74
|
<ConfigCard key={channel.name} onClick={() => openChannelModal(channel.name)}>
|
|
@@ -88,7 +87,7 @@ export function ChannelsList() {
|
|
|
88
87
|
/>
|
|
89
88
|
<StatusDot
|
|
90
89
|
status={enabled ? 'active' : 'inactive'}
|
|
91
|
-
label={enabled ? '
|
|
90
|
+
label={enabled ? t('statusActive') : t('statusInactive')}
|
|
92
91
|
/>
|
|
93
92
|
</ConfigCardHeader>
|
|
94
93
|
|
|
@@ -98,7 +97,7 @@ export function ChannelsList() {
|
|
|
98
97
|
/>
|
|
99
98
|
|
|
100
99
|
<ConfigCardFooter>
|
|
101
|
-
<ActionLink label={enabled ? '
|
|
100
|
+
<ActionLink label={enabled ? t('actionConfigure') : t('actionEnable')} />
|
|
102
101
|
{channel.tutorialUrl && (
|
|
103
102
|
<a
|
|
104
103
|
href={channel.tutorialUrl}
|
|
@@ -106,7 +105,7 @@ export function ChannelsList() {
|
|
|
106
105
|
rel="noreferrer"
|
|
107
106
|
onClick={(e) => e.stopPropagation()}
|
|
108
107
|
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"
|
|
109
|
-
title=
|
|
108
|
+
title={t('channelsGuideTitle')}
|
|
110
109
|
>
|
|
111
110
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
112
111
|
</a>
|
|
@@ -124,15 +123,15 @@ export function ChannelsList() {
|
|
|
124
123
|
<MessageSquare className="h-6 w-6 text-gray-300" />
|
|
125
124
|
</div>
|
|
126
125
|
<h3 className="text-[14px] font-semibold text-gray-900 mb-1.5">
|
|
127
|
-
|
|
126
|
+
{t('channelsEmptyTitle')}
|
|
128
127
|
</h3>
|
|
129
128
|
<p className="text-[13px] text-gray-400 max-w-sm">
|
|
130
|
-
|
|
129
|
+
{t('channelsEmptyDescription')}
|
|
131
130
|
</p>
|
|
132
131
|
</div>
|
|
133
132
|
)}
|
|
134
133
|
|
|
135
134
|
<ChannelForm />
|
|
136
|
-
</
|
|
135
|
+
</PageLayout>
|
|
137
136
|
);
|
|
138
137
|
}
|
|
@@ -7,31 +7,21 @@ import { Input } from '@/components/ui/input';
|
|
|
7
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
8
8
|
import { Card, CardContent } from '@/components/ui/card';
|
|
9
9
|
import { cn } from '@/lib/utils';
|
|
10
|
-
import { t } from '@/lib/i18n';
|
|
10
|
+
import { formatDateTime, t } from '@/lib/i18n';
|
|
11
|
+
import { PageLayout, PageHeader, PageBody } from '@/components/layout/page-layout';
|
|
11
12
|
import { AlarmClock, RefreshCw, Trash2, Play, Power } from 'lucide-react';
|
|
12
13
|
|
|
13
14
|
type StatusFilter = 'all' | 'enabled' | 'disabled';
|
|
14
15
|
|
|
15
16
|
function formatDate(value?: string | null): string {
|
|
16
|
-
|
|
17
|
-
return '-';
|
|
18
|
-
}
|
|
19
|
-
const date = new Date(value);
|
|
20
|
-
if (Number.isNaN(date.getTime())) {
|
|
21
|
-
return value;
|
|
22
|
-
}
|
|
23
|
-
return date.toLocaleString();
|
|
17
|
+
return formatDateTime(value ?? undefined);
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
function formatDateFromMs(value?: number | null): string {
|
|
27
21
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
28
22
|
return '-';
|
|
29
23
|
}
|
|
30
|
-
|
|
31
|
-
if (Number.isNaN(date.getTime())) {
|
|
32
|
-
return '-';
|
|
33
|
-
}
|
|
34
|
-
return date.toLocaleString();
|
|
24
|
+
return formatDateTime(new Date(value));
|
|
35
25
|
}
|
|
36
26
|
|
|
37
27
|
function formatEveryDuration(ms?: number | null): string {
|
|
@@ -141,21 +131,21 @@ export function CronConfig() {
|
|
|
141
131
|
};
|
|
142
132
|
|
|
143
133
|
return (
|
|
144
|
-
<
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
134
|
+
<PageLayout fullHeight>
|
|
135
|
+
<PageHeader
|
|
136
|
+
title={t('cronPageTitle')}
|
|
137
|
+
description={t('cronPageDescription')}
|
|
138
|
+
actions={
|
|
139
|
+
<Button
|
|
140
|
+
variant="ghost"
|
|
141
|
+
size="icon"
|
|
142
|
+
className="h-9 w-9 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100"
|
|
143
|
+
onClick={() => cronQuery.refetch()}
|
|
144
|
+
>
|
|
145
|
+
<RefreshCw className={cn('h-4 w-4', cronQuery.isFetching && 'animate-spin')} />
|
|
146
|
+
</Button>
|
|
147
|
+
}
|
|
148
|
+
/>
|
|
159
149
|
|
|
160
150
|
<div className="mb-6">
|
|
161
151
|
<div className="flex flex-wrap gap-3 items-center">
|
|
@@ -285,6 +275,6 @@ export function CronConfig() {
|
|
|
285
275
|
)}
|
|
286
276
|
</div>
|
|
287
277
|
<ConfirmDialog />
|
|
288
|
-
</
|
|
278
|
+
</PageLayout>
|
|
289
279
|
);
|
|
290
280
|
}
|