@nextclaw/ui 0.13.10-beta.1 → 0.13.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 +57 -0
- package/dist/assets/{api-DhI2lb1f.js → api-DmxQnm30.js} +7 -7
- package/dist/assets/{app-presenter-provider-BFbrQp8q.js → app-presenter-provider-CDSjiqfE.js} +2 -2
- package/dist/assets/arrow-left-Dd45Mvr1.js +1 -0
- package/dist/assets/channels-list-page-bkKDqaO5.js +8 -0
- package/dist/assets/chat-page-YKdeGoVx.js +105 -0
- package/dist/assets/{config-split-page-bO0K6uQv.js → config-split-page-CdJEkWxv.js} +1 -1
- package/dist/assets/{confirm-dialog-DXRWJ5uB.js → confirm-dialog-BWRNsRgb.js} +1 -1
- package/dist/assets/{desktop-update-config-DPk5V1Nl.js → desktop-update-config-CLXdVz9r.js} +1 -1
- package/dist/assets/{dist-T6rf86Nf.js → dist-CyWdaJvr.js} +1 -1
- package/dist/assets/{dist-DQoV-62W.js → dist-DzM2ZEVN.js} +1 -1
- package/dist/assets/doc-browser-D5FJcAyz.js +1 -0
- package/dist/assets/{doc-browser-CTFB6ACN.js → doc-browser-DewXa6oX.js} +1 -1
- package/dist/assets/{doc-browser-context-CP2tI-Br.js → doc-browser-context-CpL2m4Sp.js} +1 -1
- package/dist/assets/{ellipsis-B7CbNPNr.js → ellipsis-BDgTYNDX.js} +1 -1
- package/dist/assets/{es2015-BSPOGpH4.js → es2015-WqekV0OK.js} +1 -1
- package/dist/assets/{external-link-CefT35zP.js → external-link-BLjo8z2a.js} +1 -1
- package/dist/assets/{index-DZoXsgdz.js → index-DKflQCOL.js} +15 -15
- package/dist/assets/index-Q891JiF_.css +1 -0
- package/dist/assets/{key-round-BpM3GZn4.js → key-round-Du8x3sWa.js} +1 -1
- package/dist/assets/{loader-circle-_edjOFsw.js → loader-circle-Dz4FssYC.js} +1 -1
- package/dist/assets/{mcp-marketplace-page-WAgpy0hp.js → mcp-marketplace-page-BaBuHB2L.js} +2 -2
- package/dist/assets/mcp-marketplace-page-ooEvG_bB.js +1 -0
- package/dist/assets/{model-config-CixioI5U.js → model-config-B-qg70S4.js} +1 -1
- package/dist/assets/{notice-card-BLklZuEa.js → notice-card-3a7FmAz-.js} +1 -1
- package/dist/assets/play-BziM-Y34.js +1 -0
- package/dist/assets/plus-DrpyArC8.js +1 -0
- package/dist/assets/{popover-D_pJfOlM.js → popover-BlJLguiZ.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-BbWqk2yz.js → provider-scoped-model-input-BgPTQa8H.js} +1 -1
- package/dist/assets/providers-list-cBeeslbc.js +1 -0
- package/dist/assets/{react-Bl8iJUcF.js → react-BVjl1ZaK.js} +1 -1
- package/dist/assets/{refresh-cw-CqfS1EhK.js → refresh-cw-DFR87zei.js} +1 -1
- package/dist/assets/remote-DjhVMpVd.js +1 -0
- package/dist/assets/rotate-cw-DMBnsYKm.js +1 -0
- package/dist/assets/{runtime-config-page-ByvKO6Yn.js → runtime-config-page-DYH2ejUI.js} +1 -1
- package/dist/assets/{save-CQERCODg.js → save-BDmIlHvN.js} +1 -1
- package/dist/assets/search-CjHGYvTI.js +1 -0
- package/dist/assets/{search-config-Cl2_NgFY.js → search-config-BRsrQ4zK.js} +1 -1
- package/dist/assets/{secrets-config-BkR72J4g.js → secrets-config-eYdx5_AW.js} +2 -2
- package/dist/assets/{status-dot-DY0xUUvK.js → status-dot-SVPjIyG5.js} +1 -1
- package/dist/assets/{tabs-C77i1w90.js → tabs-Czd2mj6z.js} +1 -1
- package/dist/assets/{tabs-custom-CFpR54-m.js → tabs-custom-Ix4nkp2h.js} +1 -1
- package/dist/assets/{tag-chip-Kib_ljle.js → tag-chip-Cdwb3JSH.js} +1 -1
- package/dist/assets/{tooltip-fs7VQG7d.js → tooltip-CKGxLGnG.js} +1 -1
- package/dist/assets/{trash-2-Bb8mTTLl.js → trash-2-yLA8BEcE.js} +1 -1
- package/dist/assets/x-dpDISboO.js +1 -0
- package/dist/index.html +25 -25
- package/package.json +8 -8
- package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +86 -64
- package/src/features/chat/components/chat-session-workspace-panel.tsx +14 -25
- package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +3 -3
- package/src/features/panel-apps/components/panel-app-toolbar.test.tsx +24 -0
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +11 -2
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -1
- package/src/shared/components/cron-config.tsx +367 -91
- package/src/shared/hooks/use-config.ts +2 -2
- package/src/shared/lib/api/ncp-session-query-cache.utils.test.ts +35 -0
- package/src/shared/lib/api/ncp-session-query-cache.utils.ts +9 -1
- package/src/shared/lib/api/utils/ncp-session.utils.ts +2 -2
- package/src/shared/lib/cron/cron-job-view.utils.ts +124 -6
- package/src/shared/lib/i18n/README.md +7 -0
- package/src/shared/lib/i18n/cron-labels.utils.ts +32 -0
- package/src/shared/lib/i18n/index.ts +2 -25
- package/vite.config.ts +8 -0
- package/dist/assets/arrow-left-CI0Ocu9p.js +0 -1
- package/dist/assets/channels-list-page-B6CvmU9T.js +0 -8
- package/dist/assets/chat-page-Ch4ShlKB.js +0 -105
- package/dist/assets/doc-browser-yi02ASOL.js +0 -1
- package/dist/assets/index-CevK3jug.css +0 -1
- package/dist/assets/mcp-marketplace-page-9maf_9fL.js +0 -1
- package/dist/assets/play-DD0bg50i.js +0 -1
- package/dist/assets/plus-zZ2GC_AJ.js +0 -1
- package/dist/assets/providers-list-C_JOSjYd.js +0 -1
- package/dist/assets/remote-qtmzkte2.js +0 -1
- package/dist/assets/rotate-cw-C7EvaSlb.js +0 -1
- package/dist/assets/search-C0b2yzmt.js +0 -1
- package/dist/assets/x-BV4iiea8.js +0 -1
|
@@ -1,22 +1,57 @@
|
|
|
1
|
-
import { useMemo, useState } from
|
|
2
|
-
import type { CronJobView } from
|
|
3
|
-
import { useConfirmDialog } from
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import type { CronJobView } from "@/shared/lib/api";
|
|
3
|
+
import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
|
|
4
|
+
import {
|
|
5
|
+
useCronJobs,
|
|
6
|
+
useDeleteCronJob,
|
|
7
|
+
useToggleCronJob,
|
|
8
|
+
useRunCronJob,
|
|
9
|
+
} from "@/shared/hooks/use-config";
|
|
10
|
+
import { Button } from "@/shared/components/ui/button";
|
|
11
|
+
import { Input } from "@/shared/components/ui/input";
|
|
12
|
+
import { Switch } from "@/shared/components/ui/switch";
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from "@/shared/components/ui/select";
|
|
20
|
+
import {
|
|
21
|
+
Popover,
|
|
22
|
+
PopoverContent,
|
|
23
|
+
PopoverTrigger,
|
|
24
|
+
} from "@/shared/components/ui/popover";
|
|
25
|
+
import {
|
|
26
|
+
Tooltip,
|
|
27
|
+
TooltipContent,
|
|
28
|
+
TooltipProvider,
|
|
29
|
+
TooltipTrigger,
|
|
30
|
+
} from "@/shared/components/ui/tooltip";
|
|
9
31
|
import {
|
|
10
32
|
describeCronSchedule,
|
|
11
33
|
describeCronSession,
|
|
12
34
|
formatCronDate,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
35
|
+
formatRelativeTime,
|
|
36
|
+
} from "@/shared/lib/cron";
|
|
37
|
+
import { cn } from "@/shared/lib/utils";
|
|
38
|
+
import { t } from "@/shared/lib/i18n";
|
|
39
|
+
import { PageLayout, PageHeader } from "@/app/components/layout/page-layout";
|
|
40
|
+
import {
|
|
41
|
+
AlarmClock,
|
|
42
|
+
RefreshCw,
|
|
43
|
+
Trash2,
|
|
44
|
+
Play,
|
|
45
|
+
MoreVertical,
|
|
46
|
+
ChevronDown,
|
|
47
|
+
ChevronRight,
|
|
48
|
+
Clock,
|
|
49
|
+
CalendarClock,
|
|
50
|
+
ListTodo,
|
|
51
|
+
AlertCircle,
|
|
52
|
+
} from "lucide-react";
|
|
18
53
|
|
|
19
|
-
type StatusFilter =
|
|
54
|
+
type StatusFilter = "all" | "enabled" | "disabled";
|
|
20
55
|
|
|
21
56
|
function matchQuery(job: CronJobView, query: string): boolean {
|
|
22
57
|
const q = query.trim().toLowerCase();
|
|
@@ -25,72 +60,293 @@ function matchQuery(job: CronJobView, query: string): boolean {
|
|
|
25
60
|
job.id,
|
|
26
61
|
job.name,
|
|
27
62
|
job.payload.message,
|
|
28
|
-
job.payload.sessionId ??
|
|
29
|
-
]
|
|
63
|
+
job.payload.sessionId ?? "",
|
|
64
|
+
]
|
|
65
|
+
.join(" ")
|
|
66
|
+
.toLowerCase();
|
|
30
67
|
return haystack.includes(q);
|
|
31
68
|
}
|
|
32
69
|
|
|
33
70
|
function filterByStatus(job: CronJobView, status: StatusFilter): boolean {
|
|
34
|
-
if (status ===
|
|
35
|
-
if (status ===
|
|
71
|
+
if (status === "all") return true;
|
|
72
|
+
if (status === "enabled") return job.enabled;
|
|
36
73
|
return !job.enabled;
|
|
37
74
|
}
|
|
38
75
|
|
|
76
|
+
function StatusBadge({ job }: { job: CronJobView }) {
|
|
77
|
+
return (
|
|
78
|
+
<span
|
|
79
|
+
className={cn(
|
|
80
|
+
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold",
|
|
81
|
+
job.enabled
|
|
82
|
+
? "bg-emerald-50 text-emerald-700"
|
|
83
|
+
: "bg-gray-100 text-gray-500",
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
<span
|
|
87
|
+
className={cn(
|
|
88
|
+
"h-1.5 w-1.5 rounded-full",
|
|
89
|
+
job.enabled ? "bg-emerald-400" : "bg-gray-400",
|
|
90
|
+
)}
|
|
91
|
+
/>
|
|
92
|
+
{job.enabled ? t("enabled") : t("disabled")}
|
|
93
|
+
</span>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
39
97
|
function CronJobCard(props: {
|
|
40
98
|
job: CronJobView;
|
|
41
99
|
onDelete: (job: CronJobView) => void;
|
|
42
100
|
onRun: (job: CronJobView) => void;
|
|
43
|
-
onToggle: (job: CronJobView) => void;
|
|
101
|
+
onToggle: (job: CronJobView, nextEnabled: boolean) => void;
|
|
44
102
|
}) {
|
|
45
103
|
const { job, onDelete, onRun, onToggle } = props;
|
|
104
|
+
const [expanded, setExpanded] = useState(false);
|
|
105
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
106
|
+
|
|
107
|
+
const barColor = job.enabled ? "bg-emerald-400" : "bg-gray-300";
|
|
108
|
+
|
|
109
|
+
const handleCardClick = (e: React.MouseEvent) => {
|
|
110
|
+
// Don't expand when clicking switch or menu
|
|
111
|
+
const target = e.target as HTMLElement;
|
|
112
|
+
if (target.closest("[data-no-expand]")) return;
|
|
113
|
+
setExpanded(!expanded);
|
|
114
|
+
};
|
|
115
|
+
|
|
46
116
|
return (
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
117
|
+
<div
|
|
118
|
+
className={cn(
|
|
119
|
+
"group relative overflow-hidden rounded-xl border bg-white transition-all duration-150 cursor-pointer",
|
|
120
|
+
expanded
|
|
121
|
+
? "border-gray-300 shadow-sm"
|
|
122
|
+
: "border-gray-200 hover:border-gray-300 hover:shadow-sm",
|
|
123
|
+
)}
|
|
124
|
+
onClick={handleCardClick}
|
|
125
|
+
>
|
|
126
|
+
{/* Left color bar */}
|
|
127
|
+
<div
|
|
128
|
+
className={cn(
|
|
129
|
+
"absolute left-0 top-0 bottom-0 w-1 rounded-l-xl",
|
|
130
|
+
barColor,
|
|
131
|
+
)}
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<div className="pl-4 pr-3 py-3"> {/* Row 1: name + badges + actions */}
|
|
135
|
+
<div className="flex items-center gap-3">
|
|
136
|
+
<div className="min-w-0 flex-1">
|
|
137
|
+
<div className="flex items-center gap-2">
|
|
138
|
+
{expanded ? (
|
|
139
|
+
<ChevronDown className="h-3.5 w-3.5 text-gray-400 shrink-0" />
|
|
140
|
+
) : (
|
|
141
|
+
<ChevronRight className="h-3.5 w-3.5 text-gray-300 shrink-0 group-hover:text-gray-500 transition-colors" />
|
|
142
|
+
)}
|
|
143
|
+
<span className="text-sm font-semibold text-gray-900 truncate">
|
|
144
|
+
{job.name || job.id}
|
|
56
145
|
</span>
|
|
57
|
-
{job
|
|
58
|
-
|
|
59
|
-
|
|
146
|
+
<StatusBadge job={job} />
|
|
147
|
+
{job.deleteAfterRun && (
|
|
148
|
+
<span className="rounded-full bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-600">
|
|
149
|
+
{t("cronOneShot")}
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
60
152
|
</div>
|
|
61
|
-
<div className="mt-2 text-xs text-gray-500">{t('cronScheduleLabel')}: {describeCronSchedule(job)}</div>
|
|
62
|
-
<div className="mt-2 whitespace-pre-wrap break-words text-sm text-gray-700">{job.payload.message}</div>
|
|
63
|
-
<div className="mt-2 text-xs text-gray-500">{t('cronSessionLabel')}: {describeCronSession(job)}</div>
|
|
64
153
|
</div>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
154
|
+
|
|
155
|
+
{/* Actions - stop propagation so they don't toggle expand */}
|
|
156
|
+
<div className="flex items-center gap-1.5 shrink-0" data-no-expand>
|
|
157
|
+
<Switch
|
|
158
|
+
checked={job.enabled}
|
|
159
|
+
onCheckedChange={(checked) => onToggle(job, checked)}
|
|
160
|
+
/>
|
|
161
|
+
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
|
|
162
|
+
<PopoverTrigger asChild>
|
|
163
|
+
<Button
|
|
164
|
+
variant="ghost"
|
|
165
|
+
size="icon"
|
|
166
|
+
className="h-8 w-8 text-gray-400 hover:text-gray-600"
|
|
167
|
+
aria-label={t("cronMoreActions")}
|
|
168
|
+
>
|
|
169
|
+
<MoreVertical className="h-4 w-4" />
|
|
170
|
+
</Button>
|
|
171
|
+
</PopoverTrigger>
|
|
172
|
+
<PopoverContent align="end" sideOffset={4} className="w-40 p-1">
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
onClick={() => {
|
|
176
|
+
setMenuOpen(false);
|
|
177
|
+
onRun(job);
|
|
178
|
+
}}
|
|
179
|
+
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-100"
|
|
180
|
+
>
|
|
181
|
+
<Play className="h-3.5 w-3.5 text-gray-500" />
|
|
182
|
+
{t("cronRunNow")}
|
|
183
|
+
</button>
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={() => {
|
|
187
|
+
setMenuOpen(false);
|
|
188
|
+
onDelete(job);
|
|
189
|
+
}}
|
|
190
|
+
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-red-600 transition-colors hover:bg-red-50"
|
|
191
|
+
>
|
|
192
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
193
|
+
{t("delete")}
|
|
194
|
+
</button>
|
|
195
|
+
</PopoverContent>
|
|
196
|
+
</Popover>
|
|
70
197
|
</div>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
{job
|
|
79
|
-
</
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Row 2: schedule + next run + last run (always visible) */}
|
|
201
|
+
<div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500 pl-6">
|
|
202
|
+
<span className="flex items-center gap-1">
|
|
203
|
+
<CalendarClock className="h-3 w-3 text-gray-400" />
|
|
204
|
+
<span className="font-mono text-gray-600">
|
|
205
|
+
{describeCronSchedule(job)}
|
|
206
|
+
</span>
|
|
207
|
+
</span>
|
|
208
|
+
{job.state.nextRunAt && (
|
|
209
|
+
<span className="flex items-center gap-1">
|
|
210
|
+
<Clock className="h-3 w-3 text-gray-400" />
|
|
211
|
+
<span>{formatRelativeTime(job.state.nextRunAt)}</span>
|
|
212
|
+
</span>
|
|
213
|
+
)}
|
|
214
|
+
{job.state.lastRunAt && (
|
|
215
|
+
<span className="flex items-center gap-1" data-no-expand>
|
|
216
|
+
<span className="text-gray-300">•</span>
|
|
217
|
+
{job.state.lastStatus === "error" && job.state.lastError ? (
|
|
218
|
+
<TooltipProvider delayDuration={200}>
|
|
219
|
+
<Tooltip>
|
|
220
|
+
<TooltipTrigger asChild>
|
|
221
|
+
<span className="inline-flex items-center gap-0.5 text-orange-400 cursor-help">
|
|
222
|
+
<AlertCircle className="h-3 w-3" />
|
|
223
|
+
</span>
|
|
224
|
+
</TooltipTrigger>
|
|
225
|
+
<TooltipContent
|
|
226
|
+
side="top"
|
|
227
|
+
align="start"
|
|
228
|
+
className="max-w-xs text-xs bg-red-50 text-red-700 border-red-200"
|
|
229
|
+
>
|
|
230
|
+
{job.state.lastError}
|
|
231
|
+
</TooltipContent>
|
|
232
|
+
</Tooltip>
|
|
233
|
+
</TooltipProvider>
|
|
234
|
+
) : (
|
|
235
|
+
<span
|
|
236
|
+
className={cn(
|
|
237
|
+
job.state.lastStatus === "ok"
|
|
238
|
+
? "text-emerald-400"
|
|
239
|
+
: "text-gray-400",
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
{job.state.lastStatus === "ok" ? "✓" : ""}
|
|
243
|
+
</span>
|
|
244
|
+
)}
|
|
245
|
+
<span>{formatRelativeTime(job.state.lastRunAt)}</span>
|
|
246
|
+
</span>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Row 3: message preview (always visible, 2 lines) */}
|
|
251
|
+
<div className="mt-2 pl-6">
|
|
252
|
+
<div
|
|
253
|
+
className={cn(
|
|
254
|
+
"text-sm text-gray-600 break-words",
|
|
255
|
+
!expanded && "line-clamp-2",
|
|
256
|
+
)}
|
|
257
|
+
>
|
|
258
|
+
{job.payload.message}
|
|
84
259
|
</div>
|
|
85
260
|
</div>
|
|
86
|
-
|
|
87
|
-
|
|
261
|
+
|
|
262
|
+
{/* Expanded details */}
|
|
263
|
+
{expanded && (
|
|
264
|
+
<div className="mt-3 ml-6 space-y-3 animate-in fade-in-0 slide-in-from-top-1 duration-150">
|
|
265
|
+
{/* Metadata grid */}
|
|
266
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-2 text-xs">
|
|
267
|
+
<div>
|
|
268
|
+
<div className="text-[11px] text-gray-400 mb-0.5">
|
|
269
|
+
{t("cronSessionLabel")}
|
|
270
|
+
</div>
|
|
271
|
+
<div
|
|
272
|
+
className="text-gray-700 font-mono text-[11px] truncate"
|
|
273
|
+
title={describeCronSession(job)}
|
|
274
|
+
>
|
|
275
|
+
{describeCronSession(job)}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
<div>
|
|
279
|
+
<div className="text-[11px] text-gray-400 mb-0.5">
|
|
280
|
+
{t("cronNextRun")}
|
|
281
|
+
</div>
|
|
282
|
+
<div className="text-gray-700">
|
|
283
|
+
{formatCronDate(job.state.nextRunAt)}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
<div>
|
|
287
|
+
<div className="text-[11px] text-gray-400 mb-0.5">
|
|
288
|
+
{t("cronLastRun")}
|
|
289
|
+
</div>
|
|
290
|
+
<div className="text-gray-700">
|
|
291
|
+
{formatCronDate(job.state.lastRunAt)}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
<div>
|
|
295
|
+
<div className="text-[11px] text-gray-400 mb-0.5">
|
|
296
|
+
{t("cronLastStatus")}
|
|
297
|
+
</div>
|
|
298
|
+
<div
|
|
299
|
+
className={cn(
|
|
300
|
+
"inline-flex items-center gap-1 font-medium",
|
|
301
|
+
job.state.lastStatus === "ok"
|
|
302
|
+
? "text-emerald-600"
|
|
303
|
+
: job.state.lastStatus === "error"
|
|
304
|
+
? "text-red-600"
|
|
305
|
+
: job.state.lastStatus === "skipped"
|
|
306
|
+
? "text-amber-600"
|
|
307
|
+
: "text-gray-500",
|
|
308
|
+
)}
|
|
309
|
+
>
|
|
310
|
+
{job.state.lastStatus ?? "-"}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
<div>
|
|
314
|
+
<div className="text-[11px] text-gray-400 mb-0.5">
|
|
315
|
+
{t("cronCreatedAt")}
|
|
316
|
+
</div>
|
|
317
|
+
<div className="text-gray-700">
|
|
318
|
+
{formatCronDate(job.createdAt)}
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
<div>
|
|
322
|
+
<div className="text-[11px] text-gray-400 mb-0.5">
|
|
323
|
+
{t("cronId")}
|
|
324
|
+
</div>
|
|
325
|
+
<div
|
|
326
|
+
className="text-gray-700 font-mono text-[11px] truncate"
|
|
327
|
+
title={job.id}
|
|
328
|
+
>
|
|
329
|
+
{job.id}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
{/* Error */}
|
|
335
|
+
{job.state.lastError && (
|
|
336
|
+
<div className="rounded-lg bg-red-50 border border-red-100 px-3 py-2 text-xs text-red-600 break-words">
|
|
337
|
+
{job.state.lastError}
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
88
344
|
);
|
|
89
345
|
}
|
|
90
346
|
|
|
91
347
|
export function CronConfig() {
|
|
92
|
-
const [query, setQuery] = useState(
|
|
93
|
-
const [status, setStatus] = useState<StatusFilter>(
|
|
348
|
+
const [query, setQuery] = useState("");
|
|
349
|
+
const [status, setStatus] = useState<StatusFilter>("all");
|
|
94
350
|
const cronQuery = useCronJobs({ all: true });
|
|
95
351
|
const deleteCronJob = useDeleteCronJob();
|
|
96
352
|
const toggleCronJob = useToggleCronJob();
|
|
@@ -106,43 +362,37 @@ export function CronConfig() {
|
|
|
106
362
|
|
|
107
363
|
const handleDelete = async (job: CronJobView) => {
|
|
108
364
|
const confirmed = await confirm({
|
|
109
|
-
title: `${t(
|
|
365
|
+
title: `${t("cronDeleteConfirm")}?`,
|
|
110
366
|
description: job.name ? `${job.name} (${job.id})` : job.id,
|
|
111
|
-
variant:
|
|
112
|
-
confirmLabel: t(
|
|
367
|
+
variant: "destructive",
|
|
368
|
+
confirmLabel: t("delete"),
|
|
113
369
|
});
|
|
114
370
|
if (!confirmed) return;
|
|
115
371
|
deleteCronJob.mutate({ id: job.id });
|
|
116
372
|
};
|
|
117
373
|
|
|
118
|
-
const handleToggle =
|
|
119
|
-
const nextEnabled = !job.enabled;
|
|
120
|
-
const confirmed = await confirm({
|
|
121
|
-
title: nextEnabled ? `${t('cronEnableConfirm')}?` : `${t('cronDisableConfirm')}?`,
|
|
122
|
-
description: job.name ? `${job.name} (${job.id})` : job.id,
|
|
123
|
-
variant: nextEnabled ? 'default' : 'destructive',
|
|
124
|
-
confirmLabel: nextEnabled ? t('cronEnable') : t('cronDisable')
|
|
125
|
-
});
|
|
126
|
-
if (!confirmed) return;
|
|
374
|
+
const handleToggle = (job: CronJobView, nextEnabled: boolean) => {
|
|
127
375
|
toggleCronJob.mutate({ id: job.id, enabled: nextEnabled });
|
|
128
376
|
};
|
|
129
377
|
|
|
130
378
|
const handleRun = async (job: CronJobView) => {
|
|
131
379
|
const force = !job.enabled;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
380
|
+
if (force) {
|
|
381
|
+
const confirmed = await confirm({
|
|
382
|
+
title: `${t("cronRunForceConfirm")}?`,
|
|
383
|
+
description: job.name ? `${job.name} (${job.id})` : job.id,
|
|
384
|
+
confirmLabel: t("cronRunNow"),
|
|
385
|
+
});
|
|
386
|
+
if (!confirmed) return;
|
|
387
|
+
}
|
|
138
388
|
runCronJob.mutate({ id: job.id, force });
|
|
139
389
|
};
|
|
140
390
|
|
|
141
391
|
return (
|
|
142
392
|
<PageLayout fullHeight>
|
|
143
393
|
<PageHeader
|
|
144
|
-
title={t(
|
|
145
|
-
description={t(
|
|
394
|
+
title={t("cronPageTitle")}
|
|
395
|
+
description={t("cronPageDescription")}
|
|
146
396
|
actions={
|
|
147
397
|
<Button
|
|
148
398
|
variant="ghost"
|
|
@@ -150,49 +400,75 @@ export function CronConfig() {
|
|
|
150
400
|
className="h-9 w-9 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100"
|
|
151
401
|
onClick={() => cronQuery.refetch()}
|
|
152
402
|
>
|
|
153
|
-
<RefreshCw
|
|
403
|
+
<RefreshCw
|
|
404
|
+
className={cn("h-4 w-4", cronQuery.isFetching && "animate-spin")}
|
|
405
|
+
/>
|
|
154
406
|
</Button>
|
|
155
407
|
}
|
|
156
408
|
/>
|
|
157
409
|
|
|
158
|
-
<div className="mb-
|
|
410
|
+
<div className="mb-5">
|
|
159
411
|
<div className="flex flex-wrap gap-3 items-center">
|
|
160
412
|
<div className="relative flex-1 min-w-[240px]">
|
|
161
413
|
<Input
|
|
162
414
|
value={query}
|
|
163
415
|
onChange={(e) => setQuery(e.target.value)}
|
|
164
|
-
placeholder={t(
|
|
416
|
+
placeholder={t("cronSearchPlaceholder")}
|
|
165
417
|
className="pl-9"
|
|
166
418
|
/>
|
|
167
419
|
<AlarmClock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
168
420
|
</div>
|
|
169
|
-
<div className="min-w-[
|
|
170
|
-
<Select
|
|
421
|
+
<div className="min-w-[140px]">
|
|
422
|
+
<Select
|
|
423
|
+
value={status}
|
|
424
|
+
onValueChange={(value) => setStatus(value as StatusFilter)}
|
|
425
|
+
>
|
|
171
426
|
<SelectTrigger className="w-full">
|
|
172
|
-
<SelectValue placeholder={t(
|
|
427
|
+
<SelectValue placeholder={t("cronStatusLabel")} />
|
|
173
428
|
</SelectTrigger>
|
|
174
429
|
<SelectContent>
|
|
175
|
-
<SelectItem value="all">{t(
|
|
176
|
-
<SelectItem value="enabled">
|
|
177
|
-
|
|
430
|
+
<SelectItem value="all">{t("cronStatusAll")}</SelectItem>
|
|
431
|
+
<SelectItem value="enabled">
|
|
432
|
+
{t("cronStatusEnabled")}
|
|
433
|
+
</SelectItem>
|
|
434
|
+
<SelectItem value="disabled">
|
|
435
|
+
{t("cronStatusDisabled")}
|
|
436
|
+
</SelectItem>
|
|
178
437
|
</SelectContent>
|
|
179
438
|
</Select>
|
|
180
439
|
</div>
|
|
181
|
-
<div className="text-xs text-gray-
|
|
182
|
-
{
|
|
440
|
+
<div className="text-xs text-gray-400 ml-auto tabular-nums">
|
|
441
|
+
{jobs.length} / {cronQuery.data?.total ?? 0}
|
|
183
442
|
</div>
|
|
184
443
|
</div>
|
|
185
444
|
</div>
|
|
186
445
|
|
|
187
446
|
<div className="flex-1 overflow-auto custom-scrollbar">
|
|
188
447
|
{cronQuery.isLoading ? (
|
|
189
|
-
<div className="
|
|
448
|
+
<div className="flex flex-col items-center justify-center py-16 text-gray-400">
|
|
449
|
+
<RefreshCw className="h-6 w-6 animate-spin mb-3" />
|
|
450
|
+
<span className="text-sm">{t("cronLoading")}</span>
|
|
451
|
+
</div>
|
|
190
452
|
) : jobs.length === 0 ? (
|
|
191
|
-
<div className="
|
|
453
|
+
<div className="flex flex-col items-center justify-center py-16 text-gray-400">
|
|
454
|
+
<ListTodo className="h-10 w-10 mb-3 text-gray-300" />
|
|
455
|
+
<span className="text-sm font-medium text-gray-500">
|
|
456
|
+
{t("cronEmpty")}
|
|
457
|
+
</span>
|
|
458
|
+
<span className="text-xs text-gray-400 mt-1">
|
|
459
|
+
{t("cronEmptyGuide")}
|
|
460
|
+
</span>
|
|
461
|
+
</div>
|
|
192
462
|
) : (
|
|
193
|
-
<div className="space-y-
|
|
463
|
+
<div className="space-y-2.5">
|
|
194
464
|
{jobs.map((job) => (
|
|
195
|
-
<CronJobCard
|
|
465
|
+
<CronJobCard
|
|
466
|
+
key={job.id}
|
|
467
|
+
job={job}
|
|
468
|
+
onDelete={handleDelete}
|
|
469
|
+
onRun={handleRun}
|
|
470
|
+
onToggle={handleToggle}
|
|
471
|
+
/>
|
|
196
472
|
))}
|
|
197
473
|
</div>
|
|
198
474
|
)}
|
|
@@ -257,9 +257,9 @@ export function useExecuteConfigAction() {
|
|
|
257
257
|
});
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
export function useNcpSessions(params?: { limit?: number }) {
|
|
260
|
+
export function useNcpSessions(params?: { limit?: number; peerId?: string }) {
|
|
261
261
|
return useQuery({
|
|
262
|
-
queryKey: ['ncp-sessions', params?.limit ?? null],
|
|
262
|
+
queryKey: ['ncp-sessions', params?.limit ?? null, params?.peerId?.trim() || null],
|
|
263
263
|
queryFn: () => fetchNcpSessions(params),
|
|
264
264
|
staleTime: 5_000,
|
|
265
265
|
retry: false
|
|
@@ -198,4 +198,39 @@ describe('ncp-session-query-cache', () => {
|
|
|
198
198
|
queryClient.getQueryData<NcpSessionsListView>(['ncp-sessions', 200])?.sessions.map((session) => session.sessionId)
|
|
199
199
|
).toEqual(['session-3', 'session-2']);
|
|
200
200
|
});
|
|
201
|
+
|
|
202
|
+
it('does not upsert a peer summary into another peer filtered session cache', () => {
|
|
203
|
+
const queryClient = new QueryClient();
|
|
204
|
+
queryClient.setQueryData(['ncp-sessions', 200, 'peer-a'], {
|
|
205
|
+
sessions: [],
|
|
206
|
+
total: 0
|
|
207
|
+
});
|
|
208
|
+
queryClient.setQueryData(['ncp-sessions', 200, 'peer-b'], {
|
|
209
|
+
sessions: [],
|
|
210
|
+
total: 0
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
applyNcpSessionRealtimeEvent(queryClient, {
|
|
214
|
+
type: 'session.summary.upsert',
|
|
215
|
+
payload: {
|
|
216
|
+
summary: {
|
|
217
|
+
sessionId: 'session-peer-a',
|
|
218
|
+
peerId: 'peer-a',
|
|
219
|
+
messageCount: 1,
|
|
220
|
+
updatedAt: '2026-03-29T12:00:00.000Z',
|
|
221
|
+
status: 'idle'
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(
|
|
227
|
+
queryClient.getQueryData<NcpSessionsListView>(['ncp-sessions', 200, 'peer-a'])?.sessions
|
|
228
|
+
).toEqual([
|
|
229
|
+
expect.objectContaining({
|
|
230
|
+
peerId: 'peer-a',
|
|
231
|
+
sessionId: 'session-peer-a'
|
|
232
|
+
})
|
|
233
|
+
]);
|
|
234
|
+
expect(queryClient.getQueryData<NcpSessionsListView>(['ncp-sessions', 200, 'peer-b'])?.sessions).toEqual([]);
|
|
235
|
+
});
|
|
201
236
|
});
|
|
@@ -21,6 +21,14 @@ function shouldReplaceSessionSummary(
|
|
|
21
21
|
return current.status === next.status || next.status === 'idle';
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function queryKeyAcceptsSessionSummary(queryKey: readonly unknown[], summary: NcpSessionSummaryView): boolean {
|
|
25
|
+
if (queryKey[0] !== 'ncp-sessions') {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const peerId = typeof queryKey[2] === 'string' ? queryKey[2].trim() : '';
|
|
29
|
+
return !peerId || summary.peerId === peerId;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
export function upsertNcpSessionSummaryList(
|
|
25
33
|
current: NcpSessionsListView | undefined,
|
|
26
34
|
summary: NcpSessionSummaryView
|
|
@@ -80,7 +88,7 @@ export function upsertNcpSessionSummaryInQueryClient(
|
|
|
80
88
|
summary: NcpSessionSummaryView
|
|
81
89
|
): void {
|
|
82
90
|
queryClient?.setQueriesData<NcpSessionsListView>(
|
|
83
|
-
{
|
|
91
|
+
{ predicate: (query) => queryKeyAcceptsSessionSummary(query.queryKey, summary) },
|
|
84
92
|
(current) => upsertNcpSessionSummaryList(current, summary)
|
|
85
93
|
);
|
|
86
94
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { nextclawClient } from '
|
|
1
|
+
import { nextclawClient } from '@/shared/lib/api/managers/client.manager';
|
|
2
2
|
import type {
|
|
3
3
|
NcpSessionMessagesView,
|
|
4
4
|
NcpSessionSkillsView,
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
} from '@/shared/lib/api/types';
|
|
9
9
|
|
|
10
10
|
// GET /api/ncp/sessions
|
|
11
|
-
export async function fetchNcpSessions(params?: { limit?: number }): Promise<NcpSessionsListView> {
|
|
11
|
+
export async function fetchNcpSessions(params?: { limit?: number; peerId?: string }): Promise<NcpSessionsListView> {
|
|
12
12
|
return await nextclawClient.sessions.list(params) as NcpSessionsListView;
|
|
13
13
|
}
|
|
14
14
|
|