@nextclaw/ui 0.6.14 → 0.7.0
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 +18 -0
- package/README.md +2 -0
- package/dist/assets/ChannelsList-DF2U-LY1.js +1 -0
- package/dist/assets/ChatPage-BX39y0U5.js +36 -0
- package/dist/assets/DocBrowser-B9ws5JL7.js +1 -0
- package/dist/assets/{LogoBadge-BxZJ9BJT.js → LogoBadge-DvGAzkZ3.js} +1 -1
- package/dist/assets/MarketplacePage-DG5mHWJ8.js +49 -0
- package/dist/assets/ModelConfig-BL_HsOsm.js +1 -0
- package/dist/assets/ProvidersList-CH5z00YT.js +1 -0
- package/dist/assets/RuntimeConfig-BplBgkwo.js +1 -0
- package/dist/assets/SearchConfig-BhaI0fUf.js +1 -0
- package/dist/assets/{SecretsConfig-9OABNssV.js → SecretsConfig-CFoimOh9.js} +2 -2
- package/dist/assets/SessionsConfig-BHTAYn9T.js +2 -0
- package/dist/assets/index-BLeJkJ0o.css +1 -0
- package/dist/assets/index-DK4TS5ev.js +8 -0
- package/dist/assets/index-X5J6Mm--.js +1 -0
- package/dist/assets/{index-CkqvHQAt.js → index-uMsNsQX6.js} +1 -1
- package/dist/assets/{label-BIjHWZUm.js → label-D8ly4a2P.js} +1 -1
- package/dist/assets/page-layout-BSYfvwbp.js +1 -0
- package/dist/assets/security-config-DlKEYHNN.js +1 -0
- package/dist/assets/{session-run-status-BZEH0QZp.js → session-run-status-TkIuGbVw.js} +1 -1
- package/dist/assets/skeleton-CWbsNx2h.js +1 -0
- package/dist/assets/{switch-CnGQpdTp.js → switch-Ce_g9lpN.js} +1 -1
- package/dist/assets/tabs-custom-Cf5azvT5.js +1 -0
- package/dist/assets/useConfirmDialog-A8Ek8Wu7.js +5 -0
- package/dist/assets/vendor-B7ozqnFC.js +412 -0
- package/dist/index.html +3 -3
- package/package.json +9 -10
- package/src/App.tsx +49 -27
- package/src/api/client.ts +1 -0
- package/src/api/config.ts +60 -0
- package/src/api/types.ts +29 -1
- package/src/api/websocket.ts +2 -0
- package/src/components/auth/login-page.tsx +69 -0
- package/src/components/chat/ChatConversationPanel.tsx +12 -54
- package/src/components/chat/ChatSidebar.tsx +7 -1
- package/src/components/chat/adapters/chat-input-bar.adapter.test.ts +80 -0
- package/src/components/chat/adapters/chat-input-bar.adapter.ts +329 -0
- package/src/components/chat/adapters/chat-message.adapter.test.ts +137 -0
- package/src/components/chat/adapters/chat-message.adapter.ts +200 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.test.tsx +128 -0
- package/src/components/chat/chat-input/chat-input-bar.controller.ts +105 -0
- package/src/components/chat/containers/chat-input-bar.container.tsx +270 -0
- package/src/components/chat/containers/chat-message-list.container.tsx +67 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/managers/chat-thread.manager.ts +3 -1
- package/src/components/chat/nextclaw/index.ts +23 -0
- package/src/components/common/BrandHeader.tsx +4 -1
- package/src/components/common/StatusBadge.tsx +32 -20
- package/src/components/config/runtime-security-card.tsx +276 -0
- package/src/components/config/security-config.tsx +12 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/components/marketplace/MarketplacePage.test.tsx +170 -0
- package/src/components/marketplace/MarketplacePage.tsx +77 -28
- package/src/hooks/use-auth.ts +111 -0
- package/src/hooks/useMarketplace.ts +9 -0
- package/src/hooks/useWebSocket.ts +53 -1
- package/src/lib/i18n.ts +72 -0
- package/src/test/setup.ts +16 -0
- package/tsconfig.json +3 -2
- package/vite.config.ts +2 -1
- package/vitest.config.ts +16 -0
- package/.eslintrc.cjs +0 -48
- package/dist/assets/ChannelsList-DiSnpiW0.js +0 -1
- package/dist/assets/ChatPage-DsaIrNHN.js +0 -36
- package/dist/assets/DocBrowser-CnfcptGM.js +0 -1
- package/dist/assets/MarketplacePage-BI_J_DBQ.js +0 -49
- package/dist/assets/ModelConfig-DfL8F4tN.js +0 -1
- package/dist/assets/ProvidersList-DpT_oFHZ.js +0 -1
- package/dist/assets/RuntimeConfig-BNYR_Iag.js +0 -1
- package/dist/assets/SearchConfig-TDBl7Fjh.js +0 -1
- package/dist/assets/SessionsConfig-BRwntUDz.js +0 -2
- package/dist/assets/card-BYnT3Mxo.js +0 -1
- package/dist/assets/index-BCfS4UY1.css +0 -1
- package/dist/assets/index-BnUxgevr.js +0 -8
- package/dist/assets/input-oaepEtqu.js +0 -1
- package/dist/assets/page-layout-B6JXiSQB.js +0 -1
- package/dist/assets/popover-LJQgv5l1.js +0 -1
- package/dist/assets/tabs-custom-CpSv7pDl.js +0 -1
- package/dist/assets/useConfirmDialog-pqAlPdQZ.js +0 -5
- package/dist/assets/vendor-BKtTvQYU.js +0 -407
- package/src/components/chat/ChatThread.tsx +0 -402
- package/src/components/chat/SkillsPicker.tsx +0 -137
- package/src/components/chat/chat-input/ChatInputBarView.tsx +0 -82
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +0 -83
- package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +0 -39
- package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +0 -31
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +0 -112
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +0 -24
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +0 -58
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +0 -56
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +0 -40
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputThinkingSelector.tsx +0 -74
- package/src/components/chat/chat-input/useChatInputBarController.ts +0 -322
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
2
|
import { Loader2 } from 'lucide-react';
|
|
3
3
|
import { t } from '@/lib/i18n';
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
4
5
|
|
|
5
6
|
type Status = 'connected' | 'disconnected' | 'connecting';
|
|
6
7
|
|
|
@@ -11,22 +12,16 @@ interface StatusBadgeProps {
|
|
|
11
12
|
|
|
12
13
|
const statusConfig: Record<
|
|
13
14
|
Status,
|
|
14
|
-
{ dotClass: string
|
|
15
|
+
{ dotClass: string }
|
|
15
16
|
> = {
|
|
16
17
|
connected: {
|
|
17
18
|
dotClass: 'bg-emerald-500',
|
|
18
|
-
textClass: 'text-emerald-600',
|
|
19
|
-
bgClass: 'bg-emerald-50',
|
|
20
19
|
},
|
|
21
20
|
disconnected: {
|
|
22
|
-
dotClass: '
|
|
23
|
-
textClass: 'text-gray-400',
|
|
24
|
-
bgClass: 'bg-gray-100/80',
|
|
21
|
+
dotClass: 'h-2.5 w-2.5 rounded-full border border-gray-400 bg-transparent',
|
|
25
22
|
},
|
|
26
23
|
connecting: {
|
|
27
|
-
dotClass: '
|
|
28
|
-
textClass: 'text-amber-600',
|
|
29
|
-
bgClass: 'bg-amber-50',
|
|
24
|
+
dotClass: 'text-amber-600',
|
|
30
25
|
}
|
|
31
26
|
};
|
|
32
27
|
|
|
@@ -35,16 +30,33 @@ export function StatusBadge({ status, className }: StatusBadgeProps) {
|
|
|
35
30
|
const label = status === 'connected' ? t('connected') : status === 'disconnected' ? t('disconnected') : t('connecting');
|
|
36
31
|
|
|
37
32
|
return (
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
<TooltipProvider delayDuration={250}>
|
|
34
|
+
<Tooltip>
|
|
35
|
+
<TooltipTrigger asChild>
|
|
36
|
+
<span
|
|
37
|
+
role="status"
|
|
38
|
+
aria-label={label}
|
|
39
|
+
className={cn(
|
|
40
|
+
'inline-flex h-5 w-5 items-center justify-center',
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{status === 'connecting' ? (
|
|
45
|
+
<Loader2 className={cn('h-3 w-3 animate-spin', config.dotClass)} />
|
|
46
|
+
) : (
|
|
47
|
+
<span
|
|
48
|
+
className={cn(
|
|
49
|
+
status === 'connected' ? 'h-2 w-2 rounded-full' : '',
|
|
50
|
+
config.dotClass
|
|
51
|
+
)}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
</span>
|
|
55
|
+
</TooltipTrigger>
|
|
56
|
+
<TooltipContent side="bottom" className="text-xs">
|
|
57
|
+
{label}
|
|
58
|
+
</TooltipContent>
|
|
59
|
+
</Tooltip>
|
|
60
|
+
</TooltipProvider>
|
|
49
61
|
);
|
|
50
62
|
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
4
|
+
import { Input } from '@/components/ui/input';
|
|
5
|
+
import { Label } from '@/components/ui/label';
|
|
6
|
+
import { Switch } from '@/components/ui/switch';
|
|
7
|
+
import {
|
|
8
|
+
useAuthStatus,
|
|
9
|
+
useLogoutAuth,
|
|
10
|
+
useSetupAuth,
|
|
11
|
+
useUpdateAuthEnabled,
|
|
12
|
+
useUpdateAuthPassword
|
|
13
|
+
} from '@/hooks/use-auth';
|
|
14
|
+
import { t } from '@/lib/i18n';
|
|
15
|
+
import { toast } from 'sonner';
|
|
16
|
+
|
|
17
|
+
const MIN_PASSWORD_LENGTH = 8;
|
|
18
|
+
|
|
19
|
+
function hasValidPasswordLength(password: string): boolean {
|
|
20
|
+
return password.trim().length >= MIN_PASSWORD_LENGTH;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validatePasswordConfirmation(password: string, confirmPassword: string): boolean {
|
|
24
|
+
if (password !== confirmPassword) {
|
|
25
|
+
toast.error(t('authPasswordMismatch'));
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function RuntimeSecurityCard() {
|
|
32
|
+
const authStatus = useAuthStatus();
|
|
33
|
+
const setupAuth = useSetupAuth();
|
|
34
|
+
const updateAuthEnabled = useUpdateAuthEnabled();
|
|
35
|
+
const updateAuthPassword = useUpdateAuthPassword();
|
|
36
|
+
const logoutAuth = useLogoutAuth();
|
|
37
|
+
|
|
38
|
+
const [setupUsername, setSetupUsername] = useState('');
|
|
39
|
+
const [setupPassword, setSetupPassword] = useState('');
|
|
40
|
+
const [setupConfirmPassword, setSetupConfirmPassword] = useState('');
|
|
41
|
+
const [nextPassword, setNextPassword] = useState('');
|
|
42
|
+
const [nextConfirmPassword, setNextConfirmPassword] = useState('');
|
|
43
|
+
|
|
44
|
+
const auth = authStatus.data;
|
|
45
|
+
const canSubmitSetup = (
|
|
46
|
+
setupUsername.trim().length > 0 &&
|
|
47
|
+
hasValidPasswordLength(setupPassword) &&
|
|
48
|
+
setupPassword === setupConfirmPassword &&
|
|
49
|
+
!setupAuth.isPending
|
|
50
|
+
);
|
|
51
|
+
const canUpdatePassword = (
|
|
52
|
+
hasValidPasswordLength(nextPassword) &&
|
|
53
|
+
nextPassword === nextConfirmPassword &&
|
|
54
|
+
!updateAuthPassword.isPending
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const handleSetup = async () => {
|
|
58
|
+
if (!validatePasswordConfirmation(setupPassword, setupConfirmPassword)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
await setupAuth.mutateAsync({
|
|
63
|
+
username: setupUsername.trim(),
|
|
64
|
+
password: setupPassword
|
|
65
|
+
});
|
|
66
|
+
setSetupPassword('');
|
|
67
|
+
setSetupConfirmPassword('');
|
|
68
|
+
} catch {
|
|
69
|
+
// handled by mutation toast
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handlePasswordUpdate = async () => {
|
|
74
|
+
if (!validatePasswordConfirmation(nextPassword, nextConfirmPassword)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await updateAuthPassword.mutateAsync({
|
|
79
|
+
password: nextPassword
|
|
80
|
+
});
|
|
81
|
+
setNextPassword('');
|
|
82
|
+
setNextConfirmPassword('');
|
|
83
|
+
} catch {
|
|
84
|
+
// handled by mutation toast
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleEnabledChange = async (enabled: boolean) => {
|
|
89
|
+
try {
|
|
90
|
+
await updateAuthEnabled.mutateAsync({ enabled });
|
|
91
|
+
} catch {
|
|
92
|
+
// handled by mutation toast
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleLogout = async () => {
|
|
97
|
+
try {
|
|
98
|
+
await logoutAuth.mutateAsync();
|
|
99
|
+
} catch {
|
|
100
|
+
// handled by mutation toast
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (authStatus.isLoading && !auth) {
|
|
105
|
+
return (
|
|
106
|
+
<Card>
|
|
107
|
+
<CardHeader>
|
|
108
|
+
<CardTitle>{t('authSecurityTitle')}</CardTitle>
|
|
109
|
+
<CardDescription>{t('authSecurityDescription')}</CardDescription>
|
|
110
|
+
</CardHeader>
|
|
111
|
+
<CardContent className="text-sm text-gray-500">{t('loading')}</CardContent>
|
|
112
|
+
</Card>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (authStatus.isError || !auth) {
|
|
117
|
+
return (
|
|
118
|
+
<Card>
|
|
119
|
+
<CardHeader>
|
|
120
|
+
<CardTitle>{t('authSecurityTitle')}</CardTitle>
|
|
121
|
+
<CardDescription>{t('authSecurityDescription')}</CardDescription>
|
|
122
|
+
</CardHeader>
|
|
123
|
+
<CardContent className="space-y-4">
|
|
124
|
+
<p className="text-sm text-gray-500">{t('authStatusLoadFailed')}</p>
|
|
125
|
+
<Button
|
|
126
|
+
variant="outline"
|
|
127
|
+
onClick={() => {
|
|
128
|
+
void authStatus.refetch();
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
{t('authRetryStatus')}
|
|
132
|
+
</Button>
|
|
133
|
+
</CardContent>
|
|
134
|
+
</Card>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!auth.configured) {
|
|
139
|
+
return (
|
|
140
|
+
<Card>
|
|
141
|
+
<CardHeader>
|
|
142
|
+
<CardTitle>{t('authSecurityTitle')}</CardTitle>
|
|
143
|
+
<CardDescription>{t('authSecurityDescription')}</CardDescription>
|
|
144
|
+
</CardHeader>
|
|
145
|
+
<CardContent className="space-y-5">
|
|
146
|
+
<div className="rounded-xl border border-dashed border-gray-200 bg-gray-50/70 p-4">
|
|
147
|
+
<p className="text-sm font-medium text-gray-900">{t('authSetupTitle')}</p>
|
|
148
|
+
<p className="mt-1 text-sm text-gray-500">{t('authSetupDescription')}</p>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
152
|
+
<div className="space-y-2">
|
|
153
|
+
<Label htmlFor="auth-setup-username">{t('authUsername')}</Label>
|
|
154
|
+
<Input
|
|
155
|
+
id="auth-setup-username"
|
|
156
|
+
value={setupUsername}
|
|
157
|
+
onChange={(event) => setSetupUsername(event.target.value)}
|
|
158
|
+
placeholder={t('authUsernamePlaceholder')}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="space-y-2">
|
|
162
|
+
<Label htmlFor="auth-setup-password">{t('authPassword')}</Label>
|
|
163
|
+
<Input
|
|
164
|
+
id="auth-setup-password"
|
|
165
|
+
type="password"
|
|
166
|
+
value={setupPassword}
|
|
167
|
+
onChange={(event) => setSetupPassword(event.target.value)}
|
|
168
|
+
placeholder={t('authPasswordPlaceholder')}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="space-y-2">
|
|
174
|
+
<Label htmlFor="auth-setup-confirm">{t('authConfirmPassword')}</Label>
|
|
175
|
+
<Input
|
|
176
|
+
id="auth-setup-confirm"
|
|
177
|
+
type="password"
|
|
178
|
+
value={setupConfirmPassword}
|
|
179
|
+
onChange={(event) => setSetupConfirmPassword(event.target.value)}
|
|
180
|
+
placeholder={t('authConfirmPasswordPlaceholder')}
|
|
181
|
+
/>
|
|
182
|
+
<p className="text-xs text-gray-500">{t('authPasswordMinLengthHint')}</p>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<Button type="button" disabled={!canSubmitSetup} onClick={() => void handleSetup()}>
|
|
186
|
+
{setupAuth.isPending ? t('authSettingUp') : t('authSetupAction')}
|
|
187
|
+
</Button>
|
|
188
|
+
</CardContent>
|
|
189
|
+
</Card>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<Card>
|
|
195
|
+
<CardHeader>
|
|
196
|
+
<CardTitle>{t('authSecurityTitle')}</CardTitle>
|
|
197
|
+
<CardDescription>{t('authSecurityDescription')}</CardDescription>
|
|
198
|
+
</CardHeader>
|
|
199
|
+
<CardContent className="space-y-6">
|
|
200
|
+
<div className="rounded-xl border border-gray-200 p-4">
|
|
201
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
202
|
+
<div className="space-y-1">
|
|
203
|
+
<p className="text-sm font-medium text-gray-900">{t('authStatusLabel')}</p>
|
|
204
|
+
<p className="text-sm text-gray-600">
|
|
205
|
+
{t('authStatusConfiguredUser').replace('{username}', auth.username ?? '')}
|
|
206
|
+
</p>
|
|
207
|
+
<p className="text-xs text-gray-500">{t('authUsernameFixedHelp')}</p>
|
|
208
|
+
</div>
|
|
209
|
+
<span className="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
|
|
210
|
+
{auth.enabled ? t('enabled') : t('disabled')}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div className="mt-4 flex flex-col gap-4 border-t border-gray-200 pt-4 md:flex-row md:items-center md:justify-between">
|
|
215
|
+
<div className="space-y-1">
|
|
216
|
+
<p className="text-sm font-medium text-gray-900">{t('authEnableLabel')}</p>
|
|
217
|
+
<p className="text-xs text-gray-500">
|
|
218
|
+
{auth.enabled ? t('authEnableOnHelp') : t('authEnableOffHelp')}
|
|
219
|
+
</p>
|
|
220
|
+
</div>
|
|
221
|
+
<Switch
|
|
222
|
+
checked={auth.enabled}
|
|
223
|
+
disabled={updateAuthEnabled.isPending}
|
|
224
|
+
onCheckedChange={(checked) => {
|
|
225
|
+
void handleEnabledChange(checked);
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div className="rounded-xl border border-gray-200 p-4 space-y-4">
|
|
232
|
+
<div className="space-y-1">
|
|
233
|
+
<p className="text-sm font-medium text-gray-900">{t('authPasswordSectionTitle')}</p>
|
|
234
|
+
<p className="text-xs text-gray-500">{t('authPasswordSectionDescription')}</p>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
238
|
+
<div className="space-y-2">
|
|
239
|
+
<Label htmlFor="auth-password-next">{t('authPassword')}</Label>
|
|
240
|
+
<Input
|
|
241
|
+
id="auth-password-next"
|
|
242
|
+
type="password"
|
|
243
|
+
value={nextPassword}
|
|
244
|
+
onChange={(event) => setNextPassword(event.target.value)}
|
|
245
|
+
placeholder={t('authPasswordPlaceholder')}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
<div className="space-y-2">
|
|
249
|
+
<Label htmlFor="auth-password-confirm">{t('authConfirmPassword')}</Label>
|
|
250
|
+
<Input
|
|
251
|
+
id="auth-password-confirm"
|
|
252
|
+
type="password"
|
|
253
|
+
value={nextConfirmPassword}
|
|
254
|
+
onChange={(event) => setNextConfirmPassword(event.target.value)}
|
|
255
|
+
placeholder={t('authConfirmPasswordPlaceholder')}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
261
|
+
<Button type="button" disabled={!canUpdatePassword} onClick={() => void handlePasswordUpdate()}>
|
|
262
|
+
{updateAuthPassword.isPending ? t('authPasswordUpdating') : t('authPasswordAction')}
|
|
263
|
+
</Button>
|
|
264
|
+
{auth.enabled && auth.authenticated ? (
|
|
265
|
+
<Button type="button" variant="outline" disabled={logoutAuth.isPending} onClick={() => void handleLogout()}>
|
|
266
|
+
{logoutAuth.isPending ? t('authLoggingOut') : t('authLogoutAction')}
|
|
267
|
+
</Button>
|
|
268
|
+
) : null}
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<p className="text-xs text-gray-500">{t('authSessionMemoryNotice')}</p>
|
|
272
|
+
</div>
|
|
273
|
+
</CardContent>
|
|
274
|
+
</Card>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RuntimeSecurityCard } from '@/components/config/runtime-security-card';
|
|
2
|
+
import { PageHeader, PageLayout } from '@/components/layout/page-layout';
|
|
3
|
+
import { t } from '@/lib/i18n';
|
|
4
|
+
|
|
5
|
+
export function SecurityConfig() {
|
|
6
|
+
return (
|
|
7
|
+
<PageLayout className="space-y-6">
|
|
8
|
+
<PageHeader title={t('authSecurityTitle')} description={t('authSecurityDescription')} />
|
|
9
|
+
<RuntimeSecurityCard />
|
|
10
|
+
</PageLayout>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -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, KeyRound, Settings, ArrowLeft, Search } from 'lucide-react';
|
|
4
|
+
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Search, Shield } from 'lucide-react';
|
|
5
5
|
import { NavLink } from 'react-router-dom';
|
|
6
6
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
7
7
|
import { BrandHeader } from '@/components/common/BrandHeader';
|
|
@@ -82,6 +82,11 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
82
82
|
label: t('runtime'),
|
|
83
83
|
icon: GitBranch,
|
|
84
84
|
},
|
|
85
|
+
{
|
|
86
|
+
target: '/security',
|
|
87
|
+
label: t('security'),
|
|
88
|
+
icon: Shield,
|
|
89
|
+
},
|
|
85
90
|
{
|
|
86
91
|
target: '/sessions',
|
|
87
92
|
label: t('sessions'),
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
|
|
3
|
+
import type {
|
|
4
|
+
MarketplaceInstalledView,
|
|
5
|
+
MarketplaceItemSummary,
|
|
6
|
+
MarketplaceListView
|
|
7
|
+
} from '@/api/types';
|
|
8
|
+
|
|
9
|
+
type ItemsQueryState = {
|
|
10
|
+
data?: MarketplaceListView;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
isFetching: boolean;
|
|
13
|
+
isError: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type InstalledQueryState = {
|
|
18
|
+
data?: MarketplaceInstalledView;
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
isFetching: boolean;
|
|
21
|
+
isError: boolean;
|
|
22
|
+
error: Error | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const mocks = vi.hoisted(() => ({
|
|
26
|
+
navigate: vi.fn(),
|
|
27
|
+
docOpen: vi.fn(),
|
|
28
|
+
confirm: vi.fn(),
|
|
29
|
+
itemsQuery: null as unknown as ItemsQueryState,
|
|
30
|
+
installedQuery: null as unknown as InstalledQueryState,
|
|
31
|
+
installMutation: {
|
|
32
|
+
mutateAsync: vi.fn(),
|
|
33
|
+
isPending: false,
|
|
34
|
+
variables: undefined
|
|
35
|
+
},
|
|
36
|
+
manageMutation: {
|
|
37
|
+
mutate: vi.fn(),
|
|
38
|
+
isPending: false,
|
|
39
|
+
variables: undefined
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock('react-router-dom', async () => {
|
|
44
|
+
const actual = await vi.importActual('react-router-dom');
|
|
45
|
+
return {
|
|
46
|
+
...(actual as object),
|
|
47
|
+
useNavigate: () => mocks.navigate,
|
|
48
|
+
useParams: () => ({})
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
vi.mock('@/components/doc-browser', () => ({
|
|
53
|
+
useDocBrowser: () => ({
|
|
54
|
+
open: mocks.docOpen
|
|
55
|
+
})
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
vi.mock('@/components/providers/I18nProvider', () => ({
|
|
59
|
+
useI18n: () => ({
|
|
60
|
+
language: 'en'
|
|
61
|
+
})
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
vi.mock('@/hooks/useConfirmDialog', () => ({
|
|
65
|
+
useConfirmDialog: () => ({
|
|
66
|
+
confirm: mocks.confirm,
|
|
67
|
+
ConfirmDialog: () => null
|
|
68
|
+
})
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
vi.mock('@/hooks/useMarketplace', () => ({
|
|
72
|
+
useMarketplaceItems: () => mocks.itemsQuery,
|
|
73
|
+
useMarketplaceInstalled: () => mocks.installedQuery,
|
|
74
|
+
useInstallMarketplaceItem: () => mocks.installMutation,
|
|
75
|
+
useManageMarketplaceItem: () => mocks.manageMutation
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
function createMarketplaceItem(overrides: Partial<MarketplaceItemSummary> = {}): MarketplaceItemSummary {
|
|
79
|
+
return {
|
|
80
|
+
id: 'skill-web-search',
|
|
81
|
+
slug: 'web-search',
|
|
82
|
+
type: 'skill',
|
|
83
|
+
name: 'Web Search',
|
|
84
|
+
summary: 'Search the web from the marketplace',
|
|
85
|
+
summaryI18n: { en: 'Search the web from the marketplace' },
|
|
86
|
+
tags: ['search'],
|
|
87
|
+
author: 'NextClaw',
|
|
88
|
+
install: {
|
|
89
|
+
kind: 'marketplace',
|
|
90
|
+
spec: '@nextclaw/web-search',
|
|
91
|
+
command: 'nextclaw skills install @nextclaw/web-search'
|
|
92
|
+
},
|
|
93
|
+
updatedAt: '2026-03-17T00:00:00.000Z',
|
|
94
|
+
...overrides
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createItemsQuery(overrides: Partial<Record<string, unknown>> = {}) {
|
|
99
|
+
return {
|
|
100
|
+
data: undefined as MarketplaceListView | undefined,
|
|
101
|
+
isLoading: false,
|
|
102
|
+
isFetching: false,
|
|
103
|
+
isError: false,
|
|
104
|
+
error: null,
|
|
105
|
+
...overrides
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createInstalledQuery(overrides: Partial<Record<string, unknown>> = {}) {
|
|
110
|
+
return {
|
|
111
|
+
data: {
|
|
112
|
+
type: 'skill',
|
|
113
|
+
total: 0,
|
|
114
|
+
specs: [],
|
|
115
|
+
records: []
|
|
116
|
+
} satisfies MarketplaceInstalledView,
|
|
117
|
+
isLoading: false,
|
|
118
|
+
isFetching: false,
|
|
119
|
+
isError: false,
|
|
120
|
+
error: null,
|
|
121
|
+
...overrides
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe('MarketplacePage', () => {
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
mocks.navigate.mockReset();
|
|
128
|
+
mocks.docOpen.mockReset();
|
|
129
|
+
mocks.confirm.mockReset();
|
|
130
|
+
mocks.installMutation.mutateAsync.mockReset();
|
|
131
|
+
mocks.manageMutation.mutate.mockReset();
|
|
132
|
+
mocks.installMutation.isPending = false;
|
|
133
|
+
mocks.installMutation.variables = undefined;
|
|
134
|
+
mocks.manageMutation.isPending = false;
|
|
135
|
+
mocks.manageMutation.variables = undefined;
|
|
136
|
+
mocks.itemsQuery = createItemsQuery();
|
|
137
|
+
mocks.installedQuery = createInstalledQuery();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('renders skeleton cards during initial skills loading', () => {
|
|
141
|
+
mocks.itemsQuery = createItemsQuery({
|
|
142
|
+
isLoading: true,
|
|
143
|
+
isFetching: true
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const { container } = render(<MarketplacePage forcedType="skills" />);
|
|
147
|
+
|
|
148
|
+
expect(screen.getByTestId('marketplace-list-skeleton')).toBeTruthy();
|
|
149
|
+
expect(container.querySelectorAll('[data-testid="marketplace-list-skeleton"] > article')).toHaveLength(12);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('keeps loaded cards visible during background refresh', () => {
|
|
153
|
+
mocks.itemsQuery = createItemsQuery({
|
|
154
|
+
data: {
|
|
155
|
+
total: 1,
|
|
156
|
+
page: 1,
|
|
157
|
+
pageSize: 12,
|
|
158
|
+
totalPages: 1,
|
|
159
|
+
sort: 'relevance',
|
|
160
|
+
items: [createMarketplaceItem()]
|
|
161
|
+
} satisfies MarketplaceListView,
|
|
162
|
+
isFetching: true
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
render(<MarketplacePage forcedType="skills" />);
|
|
166
|
+
|
|
167
|
+
expect(screen.queryByTestId('marketplace-list-skeleton')).toBeNull();
|
|
168
|
+
expect(screen.getByText('Web Search')).toBeTruthy();
|
|
169
|
+
});
|
|
170
|
+
});
|