@vendure/dashboard 3.6.3-master-202604160307 → 3.6.3-master-202604170307

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.6.3-master-202604160307",
4
+ "version": "3.6.3-master-202604170307",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -137,8 +137,8 @@
137
137
  "@storybook/addon-vitest": "^10.3.1",
138
138
  "@storybook/react-vite": "^10.3.1",
139
139
  "@types/node": "^22.19.0",
140
- "@vendure/common": "^3.6.3-master-202604160307",
141
- "@vendure/core": "^3.6.3-master-202604160307",
140
+ "@vendure/common": "^3.6.3-master-202604170307",
141
+ "@vendure/core": "^3.6.3-master-202604170307",
142
142
  "@vitest/browser": "^3.2.4",
143
143
  "@vitest/coverage-v8": "^3.2.4",
144
144
  "eslint": "^9.39.0",
@@ -0,0 +1,80 @@
1
+ import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
2
+ import { BulkActionComponent } from '@/vdb/framework/extension-api/types/data-table.js';
3
+ import { api } from '@/vdb/graphql/api.js';
4
+ import { usePaginatedList } from '@/vdb/hooks/use-paginated-list.js';
5
+ import { plural } from '@lingui/core/macro';
6
+ import { Plural, useLingui } from '@lingui/react/macro';
7
+ import { useMutation } from '@tanstack/react-query';
8
+ import { Ban } from 'lucide-react';
9
+ import { toast } from 'sonner';
10
+ import { cancelJobDocument } from '../job-queue.graphql.js';
11
+
12
+ export const CancelJobsBulkAction: BulkActionComponent<any> = ({ selection, table }) => {
13
+ const { refetchPaginatedList } = usePaginatedList();
14
+ const { t } = useLingui();
15
+
16
+ const cancellableJobs = selection.filter(job => job.state === 'RUNNING' || job.state === 'PENDING');
17
+ const cancellableCount = cancellableJobs.length;
18
+
19
+ const { mutate, isPending } = useMutation({
20
+ mutationFn: async () => {
21
+ const results = await Promise.allSettled(
22
+ cancellableJobs.map(job => api.mutate(cancelJobDocument, { jobId: job.id })),
23
+ );
24
+
25
+ const fulfilled = results.filter(r => r.status === 'fulfilled').length;
26
+ const rejected = results.filter(r => r.status === 'rejected').length;
27
+
28
+ return { fulfilled, rejected };
29
+ },
30
+ onSuccess: ({ fulfilled, rejected }) => {
31
+ if (fulfilled > 0) {
32
+ toast.success(
33
+ plural(fulfilled, {
34
+ one: t`Successfully cancelled ${fulfilled} job`,
35
+ other: t`Successfully cancelled ${fulfilled} jobs`,
36
+ }),
37
+ );
38
+ }
39
+ if (rejected > 0) {
40
+ toast.error(
41
+ plural(rejected, {
42
+ one: t`Failed to cancel ${rejected} job`,
43
+ other: t`Failed to cancel ${rejected} jobs`,
44
+ }),
45
+ );
46
+ }
47
+
48
+ refetchPaginatedList();
49
+ table.resetRowSelection();
50
+ },
51
+ });
52
+
53
+ if (cancellableCount === 0) {
54
+ return null;
55
+ }
56
+
57
+ return (
58
+ <DataTableBulkActionItem
59
+ requiresPermission={['DeleteSettings', 'DeleteSystem']}
60
+ onClick={() => mutate()}
61
+ disabled={isPending}
62
+ label={
63
+ <Plural
64
+ value={cancellableCount}
65
+ one={`Cancel ${cancellableCount} job`}
66
+ other={`Cancel ${cancellableCount} jobs`}
67
+ />
68
+ }
69
+ confirmationText={
70
+ <Plural
71
+ value={cancellableCount}
72
+ one={`Are you sure you want to cancel ${cancellableCount} job? This action cannot be undone.`}
73
+ other={`Are you sure you want to cancel ${cancellableCount} jobs? This action cannot be undone.`}
74
+ />
75
+ }
76
+ icon={Ban}
77
+ className="text-destructive"
78
+ />
79
+ );
80
+ };
@@ -25,9 +25,38 @@ import {
25
25
  RotateCcw,
26
26
  } from 'lucide-react';
27
27
  import { useEffect, useRef, useState } from 'react';
28
+ import { CancelJobsBulkAction } from './components/cancel-jobs-bulk-action.js';
28
29
  import { PayloadDialog } from './components/payload-dialog.js';
29
30
  import { cancelJobDocument, jobListDocument, jobQueueListDocument } from './job-queue.graphql.js';
30
31
 
32
+ function formatDuration(ms: number): string {
33
+ if (ms < 1000) {
34
+ return `${ms}ms`;
35
+ }
36
+
37
+ const seconds = Math.floor(ms / 1000);
38
+ const minutes = Math.floor(seconds / 60);
39
+ const hours = Math.floor(minutes / 60);
40
+ const days = Math.floor(hours / 24);
41
+
42
+ const parts: string[] = [];
43
+
44
+ if (days > 0) {
45
+ parts.push(`${days}d`);
46
+ }
47
+ if (hours % 24 > 0) {
48
+ parts.push(`${hours % 24}h`);
49
+ }
50
+ if (minutes % 60 > 0) {
51
+ parts.push(`${minutes % 60}m`);
52
+ }
53
+ if (seconds % 60 > 0) {
54
+ parts.push(`${seconds % 60}s`);
55
+ }
56
+
57
+ return parts.join(' ');
58
+ }
59
+
31
60
  function getJobStateBadgeVariant(state: string) {
32
61
  switch (state) {
33
62
  case 'PENDING':
@@ -122,9 +151,7 @@ function JobQueuePage() {
122
151
  customizeColumns={{
123
152
  createdAt: {
124
153
  cell: ({ row }) => (
125
- <div title={row.original.createdAt}>
126
- {formatRelativeDate(row.original.createdAt)}
127
- </div>
154
+ <div title={row.original.createdAt}>{formatRelativeDate(row.original.createdAt)}</div>
128
155
  ),
129
156
  },
130
157
  data: {
@@ -177,26 +204,22 @@ function JobQueuePage() {
177
204
  const state = STATES.find(s => s.value === row.original.state);
178
205
  return (
179
206
  <div className="flex items-center gap-2">
180
- <Badge
181
- variant={getJobStateBadgeVariant(row.original.state)}
182
- >
207
+ <Badge variant={getJobStateBadgeVariant(row.original.state)}>
183
208
  {state && (
184
209
  <state.icon
185
210
  className={
186
- row.original.state === 'RUNNING'
187
- ? 'animate-spin'
188
- : undefined
211
+ row.original.state === 'RUNNING' ? 'animate-spin' : undefined
189
212
  }
190
213
  />
191
214
  )}
192
215
  {row.original.state}
193
216
  </Badge>
194
217
  {row.original.state === 'RUNNING' && (
195
- <DropdownMenu
196
- onOpenChange={open => (isActionMenuOpenRef.current = open)}
197
- >
198
- <DropdownMenuTrigger render={<Button variant="ghost" size="icon-xs" />}>
199
- <MoreVertical />
218
+ <DropdownMenu onOpenChange={open => (isActionMenuOpenRef.current = open)}>
219
+ <DropdownMenuTrigger
220
+ render={<Button variant="ghost" size="icon-xs" />}
221
+ >
222
+ <MoreVertical />
200
223
  </DropdownMenuTrigger>
201
224
  <DropdownMenuContent align="end">
202
225
  <DropdownMenuItem
@@ -216,7 +239,7 @@ function JobQueuePage() {
216
239
  },
217
240
  duration: {
218
241
  cell: ({ row }) => {
219
- return row.original.duration ? `${row.original.duration}ms` : null;
242
+ return row.original.duration ? formatDuration(row.original.duration) : null;
220
243
  },
221
244
  },
222
245
  }}
@@ -246,6 +269,12 @@ function JobQueuePage() {
246
269
  options: STATES,
247
270
  },
248
271
  }}
272
+ bulkActions={[
273
+ {
274
+ component: CancelJobsBulkAction,
275
+ order: 100,
276
+ },
277
+ ]}
249
278
  registerRefresher={refresher => {
250
279
  refreshRef.current = refresher;
251
280
  }}
@@ -253,11 +282,11 @@ function JobQueuePage() {
253
282
  <ActionBarItem itemId="auto-refresh-button">
254
283
  <DropdownMenu>
255
284
  <DropdownMenuTrigger render={<Button variant="outline" size="sm" className="gap-2" />}>
256
- <RefreshCw className="h-4 w-4" />
257
- <span>
258
- <Trans>Auto refresh: {currentInterval?.label}</Trans>
259
- </span>
260
- <ChevronDown className="h-4 w-4" />
285
+ <RefreshCw className="h-4 w-4" />
286
+ <span>
287
+ <Trans>Auto refresh: {currentInterval?.label}</Trans>
288
+ </span>
289
+ <ChevronDown className="h-4 w-4" />
261
290
  </DropdownMenuTrigger>
262
291
  <DropdownMenuContent align="end">
263
292
  {REFRESH_INTERVALS.map(interval => (
@@ -53,6 +53,13 @@ html[dir="rtl"] * {
53
53
  @utility col-main {
54
54
  grid-column: span 3 / span 3;
55
55
  }
56
+ @utility scrollbar-none {
57
+ -ms-overflow-style: none;
58
+ scrollbar-width: none;
59
+ &::-webkit-scrollbar {
60
+ display: none;
61
+ }
62
+ }
56
63
  @utility col-side {
57
64
  grid-column: span 2 / span 2;
58
65
  }