@open-mercato/core 0.4.7-develop-78d7541539 → 0.4.7-develop-c89cca0193
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/AGENTS.md +1 -0
- package/dist/modules/catalog/api/bulk-delete/route.js +86 -0
- package/dist/modules/catalog/api/bulk-delete/route.js.map +7 -0
- package/dist/modules/catalog/api/prices/route.js +39 -6
- package/dist/modules/catalog/api/prices/route.js.map +2 -2
- package/dist/modules/catalog/api/products/route.js +6 -11
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/commands/products.js +2 -0
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductsDataTable.js +9 -1
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/catalog/lib/bulkDelete.js +70 -0
- package/dist/modules/catalog/lib/bulkDelete.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/product-bulk-delete/widget.js +185 -0
- package/dist/modules/catalog/widgets/injection/product-bulk-delete/widget.js.map +7 -0
- package/dist/modules/catalog/widgets/injection-table.js +9 -1
- package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
- package/dist/modules/catalog/workers/catalog-product-bulk-delete.js +40 -0
- package/dist/modules/catalog/workers/catalog-product-bulk-delete.js.map +7 -0
- package/dist/modules/data_sync/api/options.js +52 -0
- package/dist/modules/data_sync/api/options.js.map +7 -0
- package/dist/modules/data_sync/api/run.js +30 -35
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js +15 -30
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +109 -0
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +7 -0
- package/dist/modules/data_sync/api/schedules/route.js +72 -0
- package/dist/modules/data_sync/api/schedules/route.js.map +7 -0
- package/dist/modules/data_sync/api/schedules/serialize.js +21 -0
- package/dist/modules/data_sync/api/schedules/serialize.js.map +7 -0
- package/dist/modules/data_sync/backend/data-sync/page.js +656 -47
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +116 -34
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +394 -0
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +7 -0
- package/dist/modules/data_sync/data/validators.js +32 -0
- package/dist/modules/data_sync/data/validators.js.map +2 -2
- package/dist/modules/data_sync/di.js +2 -0
- package/dist/modules/data_sync/di.js.map +2 -2
- package/dist/modules/data_sync/lib/id-mapping.js +24 -2
- package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
- package/dist/modules/data_sync/lib/start-run.js +57 -0
- package/dist/modules/data_sync/lib/start-run.js.map +7 -0
- package/dist/modules/data_sync/lib/sync-engine.js +93 -4
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +5 -1
- package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-schedule-service.js +138 -0
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +7 -0
- package/dist/modules/data_sync/workers/sync-export.js +28 -2
- package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-import.js +28 -2
- package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-scheduled.js +5 -0
- package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
- package/dist/modules/entities/api/definitions.js +5 -2
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/lib/field-definitions.js +3 -1
- package/dist/modules/entities/lib/field-definitions.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +14 -15
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +3 -3
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +148 -33
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +15 -1
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +24 -22
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/progress/api/active/route.js +3 -1
- package/dist/modules/progress/api/active/route.js.map +2 -2
- package/dist/modules/progress/api/jobs/[id]/route.js +1 -1
- package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
- package/dist/modules/progress/api/jobs/route.js +1 -1
- package/dist/modules/progress/api/jobs/route.js.map +2 -2
- package/dist/modules/progress/lib/events.js.map +1 -1
- package/dist/modules/progress/lib/progressService.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +42 -1
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/query_index/lib/document.js +35 -1
- package/dist/modules/query_index/lib/document.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +91 -4
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +2 -0
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/sales/api/adjustment-kinds/route.js +3 -9
- package/dist/modules/sales/api/adjustment-kinds/route.js.map +2 -2
- package/dist/modules/sales/api/channels/route.js +3 -10
- package/dist/modules/sales/api/channels/route.js.map +2 -2
- package/dist/modules/sales/api/delivery-windows/route.js +3 -10
- package/dist/modules/sales/api/delivery-windows/route.js.map +2 -2
- package/dist/modules/sales/api/payment-methods/route.js +3 -11
- package/dist/modules/sales/api/payment-methods/route.js.map +2 -2
- package/dist/modules/sales/api/price-kinds/route.js +3 -5
- package/dist/modules/sales/api/price-kinds/route.js.map +2 -2
- package/dist/modules/sales/api/shipping-methods/route.js +3 -11
- package/dist/modules/sales/api/shipping-methods/route.js.map +2 -2
- package/dist/modules/sales/api/tags/route.js +3 -9
- package/dist/modules/sales/api/tags/route.js.map +2 -2
- package/dist/modules/sales/api/tax-rates/route.js +3 -13
- package/dist/modules/sales/api/tax-rates/route.js.map +2 -2
- package/dist/modules/sales/api/utils.js +9 -0
- package/dist/modules/sales/api/utils.js.map +2 -2
- package/dist/modules/sales/lib/makeStatusDictionaryRoute.js +3 -9
- package/dist/modules/sales/lib/makeStatusDictionaryRoute.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js +3 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/route.js +4 -3
- package/dist/modules/workflows/api/definitions/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/serialize.js +25 -0
- package/dist/modules/workflows/api/definitions/serialize.js.map +7 -0
- package/package.json +3 -3
- package/src/modules/catalog/api/bulk-delete/route.ts +93 -0
- package/src/modules/catalog/api/prices/route.ts +53 -6
- package/src/modules/catalog/api/products/route.ts +6 -11
- package/src/modules/catalog/commands/products.ts +2 -0
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +8 -0
- package/src/modules/catalog/i18n/de.json +10 -0
- package/src/modules/catalog/i18n/en.json +10 -0
- package/src/modules/catalog/i18n/es.json +10 -0
- package/src/modules/catalog/i18n/pl.json +10 -0
- package/src/modules/catalog/lib/bulkDelete.ts +106 -0
- package/src/modules/catalog/widgets/injection/product-bulk-delete/widget.ts +242 -0
- package/src/modules/catalog/widgets/injection-table.ts +8 -0
- package/src/modules/catalog/workers/catalog-product-bulk-delete.ts +48 -0
- package/src/modules/data_sync/AGENTS.md +11 -3
- package/src/modules/data_sync/api/options.ts +58 -0
- package/src/modules/data_sync/api/run.ts +34 -36
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +2 -2
- package/src/modules/data_sync/api/runs/[id]/retry.ts +14 -31
- package/src/modules/data_sync/api/schedules/[id]/route.ts +130 -0
- package/src/modules/data_sync/api/schedules/route.ts +77 -0
- package/src/modules/data_sync/api/schedules/serialize.ts +31 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +756 -2
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +179 -53
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +512 -0
- package/src/modules/data_sync/data/validators.ts +35 -0
- package/src/modules/data_sync/di.ts +6 -0
- package/src/modules/data_sync/i18n/de.json +72 -0
- package/src/modules/data_sync/i18n/en.json +72 -0
- package/src/modules/data_sync/i18n/es.json +72 -0
- package/src/modules/data_sync/i18n/pl.json +72 -0
- package/src/modules/data_sync/lib/adapter.ts +4 -1
- package/src/modules/data_sync/lib/id-mapping.ts +32 -2
- package/src/modules/data_sync/lib/start-run.ts +90 -0
- package/src/modules/data_sync/lib/sync-engine.ts +111 -4
- package/src/modules/data_sync/lib/sync-run-service.ts +5 -1
- package/src/modules/data_sync/lib/sync-schedule-service.ts +207 -0
- package/src/modules/data_sync/workers/sync-export.ts +33 -2
- package/src/modules/data_sync/workers/sync-import.ts +33 -2
- package/src/modules/data_sync/workers/sync-scheduled.ts +7 -0
- package/src/modules/entities/api/definitions.ts +12 -2
- package/src/modules/entities/lib/field-definitions.ts +2 -0
- package/src/modules/integrations/AGENTS.md +16 -3
- package/src/modules/integrations/api/[id]/route.ts +14 -15
- package/src/modules/integrations/api/route.ts +3 -3
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +176 -54
- package/src/modules/integrations/lib/state-service.ts +25 -1
- package/src/modules/messages/api/[id]/route.ts +25 -22
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +3 -3
- package/src/modules/progress/api/active/route.ts +4 -1
- package/src/modules/progress/api/jobs/[id]/route.ts +1 -1
- package/src/modules/progress/api/jobs/route.ts +1 -1
- package/src/modules/progress/lib/events.ts +6 -0
- package/src/modules/progress/lib/progressService.ts +1 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +47 -1
- package/src/modules/query_index/lib/document.ts +52 -1
- package/src/modules/query_index/lib/engine.ts +104 -4
- package/src/modules/query_index/lib/indexer.ts +2 -0
- package/src/modules/sales/api/adjustment-kinds/route.ts +3 -9
- package/src/modules/sales/api/channels/route.ts +3 -10
- package/src/modules/sales/api/delivery-windows/route.ts +3 -10
- package/src/modules/sales/api/payment-methods/route.ts +3 -11
- package/src/modules/sales/api/price-kinds/route.ts +3 -5
- package/src/modules/sales/api/shipping-methods/route.ts +3 -11
- package/src/modules/sales/api/tags/route.ts +3 -9
- package/src/modules/sales/api/tax-rates/route.ts +3 -13
- package/src/modules/sales/api/utils.ts +9 -0
- package/src/modules/sales/lib/makeStatusDictionaryRoute.ts +3 -9
- package/src/modules/workflows/api/definitions/[id]/route.ts +3 -2
- package/src/modules/workflows/api/definitions/route.ts +4 -3
- package/src/modules/workflows/api/definitions/serialize.ts +23 -0
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
+
import Link from 'next/link'
|
|
3
4
|
import { useRouter } from 'next/navigation'
|
|
4
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
5
6
|
import { DataTable } from '@open-mercato/ui/backend/DataTable'
|
|
6
7
|
import type { ColumnDef } from '@tanstack/react-table'
|
|
7
8
|
import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
|
|
8
|
-
import {
|
|
9
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
10
|
+
import { Badge, type BadgeProps } from '@open-mercato/ui/primitives/badge'
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'
|
|
12
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
13
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
14
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
15
|
+
import { Notice } from '@open-mercato/ui/primitives/Notice'
|
|
16
|
+
import { Separator } from '@open-mercato/ui/primitives/separator'
|
|
17
|
+
import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
9
18
|
import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
10
19
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
20
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
12
21
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
13
22
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
23
|
+
import {
|
|
24
|
+
ArrowRightLeft,
|
|
25
|
+
Boxes,
|
|
26
|
+
CalendarClock,
|
|
27
|
+
CircleAlert,
|
|
28
|
+
Clock3,
|
|
29
|
+
Gauge,
|
|
30
|
+
Play,
|
|
31
|
+
PlugZap,
|
|
32
|
+
Repeat,
|
|
33
|
+
Settings2,
|
|
34
|
+
ShieldCheck,
|
|
35
|
+
} from 'lucide-react'
|
|
14
36
|
|
|
15
37
|
type SyncRunRow = {
|
|
16
38
|
id: string
|
|
@@ -31,6 +53,49 @@ type ResponsePayload = {
|
|
|
31
53
|
totalPages: number
|
|
32
54
|
}
|
|
33
55
|
|
|
56
|
+
type SyncOption = {
|
|
57
|
+
integrationId: string
|
|
58
|
+
title: string
|
|
59
|
+
description?: string | null
|
|
60
|
+
providerKey?: string | null
|
|
61
|
+
direction: 'import' | 'export' | 'bidirectional'
|
|
62
|
+
supportedEntities: string[]
|
|
63
|
+
hasCredentials: boolean
|
|
64
|
+
isEnabled: boolean
|
|
65
|
+
settingsPath: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type SyncOptionsResponse = {
|
|
69
|
+
items: SyncOption[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type SyncScheduleRecord = {
|
|
73
|
+
id: string
|
|
74
|
+
integrationId: string
|
|
75
|
+
entityType: string
|
|
76
|
+
direction: 'import' | 'export'
|
|
77
|
+
scheduleType: 'cron' | 'interval'
|
|
78
|
+
scheduleValue: string
|
|
79
|
+
timezone: string
|
|
80
|
+
fullSync: boolean
|
|
81
|
+
isEnabled: boolean
|
|
82
|
+
lastRunAt: string | null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type SyncSchedulesResponse = {
|
|
86
|
+
items?: SyncScheduleRecord[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type SyncScheduleEditorState = {
|
|
90
|
+
id?: string
|
|
91
|
+
scheduleType: 'cron' | 'interval'
|
|
92
|
+
scheduleValue: string
|
|
93
|
+
timezone: string
|
|
94
|
+
fullSync: boolean
|
|
95
|
+
isEnabled: boolean
|
|
96
|
+
lastRunAt: string | null
|
|
97
|
+
}
|
|
98
|
+
|
|
34
99
|
const STATUS_STYLES: Record<string, string> = {
|
|
35
100
|
pending: 'bg-gray-100 text-gray-800',
|
|
36
101
|
running: 'bg-blue-100 text-blue-800',
|
|
@@ -40,18 +105,93 @@ const STATUS_STYLES: Record<string, string> = {
|
|
|
40
105
|
paused: 'bg-orange-100 text-orange-800',
|
|
41
106
|
}
|
|
42
107
|
|
|
108
|
+
const DEFAULT_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
109
|
+
|
|
110
|
+
type SummaryBadgeStyle = {
|
|
111
|
+
variant: BadgeProps['variant']
|
|
112
|
+
className?: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getSummaryBadgeStyle(kind: 'enabled' | 'disabled' | 'ready' | 'missing' | 'scheduled' | 'paused' | 'none'): SummaryBadgeStyle {
|
|
116
|
+
if (kind === 'enabled' || kind === 'ready') {
|
|
117
|
+
return {
|
|
118
|
+
variant: 'outline',
|
|
119
|
+
className: 'border-emerald-500/30 bg-emerald-500/15 text-emerald-200',
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (kind === 'disabled' || kind === 'missing') {
|
|
124
|
+
return {
|
|
125
|
+
variant: 'outline',
|
|
126
|
+
className: 'border-red-500/30 bg-red-500/15 text-red-200',
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (kind === 'paused') {
|
|
131
|
+
return {
|
|
132
|
+
variant: 'outline',
|
|
133
|
+
className: 'border-amber-500/30 bg-amber-500/15 text-amber-200',
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (kind === 'scheduled') {
|
|
138
|
+
return {
|
|
139
|
+
variant: 'outline',
|
|
140
|
+
className: 'border-sky-500/30 bg-sky-500/15 text-sky-200',
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
variant: 'outline',
|
|
146
|
+
className: 'border-muted-foreground/20 bg-muted/40 text-muted-foreground',
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatEntityTypeLabel(entityType: string): string {
|
|
151
|
+
return entityType
|
|
152
|
+
.replace(/[_-]+/g, ' ')
|
|
153
|
+
.replace(/\b\w/g, (letter) => letter.toUpperCase())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildDefaultScheduleState(entityType: string): SyncScheduleEditorState {
|
|
157
|
+
const normalized = entityType.trim().toLowerCase()
|
|
158
|
+
const longerInterval = normalized === 'categories' || normalized === 'attributes'
|
|
159
|
+
return {
|
|
160
|
+
scheduleType: 'interval',
|
|
161
|
+
scheduleValue: longerInterval ? '6h' : '1h',
|
|
162
|
+
timezone: DEFAULT_TIMEZONE,
|
|
163
|
+
fullSync: normalized !== 'products',
|
|
164
|
+
isEnabled: true,
|
|
165
|
+
lastRunAt: null,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
43
169
|
export default function SyncRunsDashboardPage() {
|
|
44
170
|
const router = useRouter()
|
|
45
171
|
const [rows, setRows] = React.useState<SyncRunRow[]>([])
|
|
172
|
+
const [options, setOptions] = React.useState<SyncOption[]>([])
|
|
46
173
|
const [page, setPage] = React.useState(1)
|
|
47
174
|
const [total, setTotal] = React.useState(0)
|
|
48
175
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
49
176
|
const [search, setSearch] = React.useState('')
|
|
50
177
|
const [filterValues, setFilterValues] = React.useState<FilterValues>({})
|
|
51
178
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
179
|
+
const [isLoadingOptions, setIsLoadingOptions] = React.useState(true)
|
|
180
|
+
const [selectedIntegrationId, setSelectedIntegrationId] = React.useState('')
|
|
181
|
+
const [selectedEntityType, setSelectedEntityType] = React.useState('')
|
|
182
|
+
const [selectedDirection, setSelectedDirection] = React.useState<'import' | 'export'>('import')
|
|
183
|
+
const [batchSize, setBatchSize] = React.useState('100')
|
|
184
|
+
const [fullSync, setFullSync] = React.useState(false)
|
|
185
|
+
const [scheduleEditor, setScheduleEditor] = React.useState<SyncScheduleEditorState>(() => buildDefaultScheduleState(''))
|
|
186
|
+
const [isLoadingSchedule, setIsLoadingSchedule] = React.useState(false)
|
|
187
|
+
const [isSavingSchedule, setIsSavingSchedule] = React.useState(false)
|
|
188
|
+
const [isDeletingSchedule, setIsDeletingSchedule] = React.useState(false)
|
|
52
189
|
const [reloadToken, setReloadToken] = React.useState(0)
|
|
53
190
|
const scopeVersion = useOrganizationScopeVersion()
|
|
54
191
|
const t = useT()
|
|
192
|
+
const { runMutation } = useGuardedMutation<Record<string, unknown>>({
|
|
193
|
+
contextId: 'data_sync.dashboard',
|
|
194
|
+
})
|
|
55
195
|
|
|
56
196
|
React.useEffect(() => {
|
|
57
197
|
let cancelled = false
|
|
@@ -85,6 +225,113 @@ export default function SyncRunsDashboardPage() {
|
|
|
85
225
|
return () => { cancelled = true }
|
|
86
226
|
}, [page, filterValues, reloadToken, scopeVersion, t])
|
|
87
227
|
|
|
228
|
+
React.useEffect(() => {
|
|
229
|
+
let cancelled = false
|
|
230
|
+
async function loadOptions() {
|
|
231
|
+
setIsLoadingOptions(true)
|
|
232
|
+
const fallback: SyncOptionsResponse = { items: [] }
|
|
233
|
+
const call = await apiCall<SyncOptionsResponse>('/api/data_sync/options', undefined, { fallback })
|
|
234
|
+
if (!cancelled) {
|
|
235
|
+
if (!call.ok) {
|
|
236
|
+
flash(t('data_sync.dashboard.loadError'), 'error')
|
|
237
|
+
setOptions([])
|
|
238
|
+
setIsLoadingOptions(false)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const nextItems = Array.isArray(call.result?.items) ? call.result.items : []
|
|
243
|
+
setOptions(nextItems)
|
|
244
|
+
setSelectedIntegrationId((current) => {
|
|
245
|
+
if (current && nextItems.some((item) => item.integrationId === current)) return current
|
|
246
|
+
return nextItems[0]?.integrationId ?? ''
|
|
247
|
+
})
|
|
248
|
+
setIsLoadingOptions(false)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
void loadOptions()
|
|
253
|
+
return () => { cancelled = true }
|
|
254
|
+
}, [scopeVersion, t])
|
|
255
|
+
|
|
256
|
+
const selectedIntegration = React.useMemo(
|
|
257
|
+
() => options.find((item) => item.integrationId === selectedIntegrationId) ?? null,
|
|
258
|
+
[options, selectedIntegrationId],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const entityOptions = React.useMemo(
|
|
262
|
+
() => selectedIntegration?.supportedEntities ?? [],
|
|
263
|
+
[selectedIntegration],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
React.useEffect(() => {
|
|
267
|
+
if (!selectedIntegration) {
|
|
268
|
+
setSelectedEntityType('')
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
setSelectedEntityType((current) => (
|
|
272
|
+
current && selectedIntegration.supportedEntities.includes(current)
|
|
273
|
+
? current
|
|
274
|
+
: (selectedIntegration.supportedEntities[0] ?? '')
|
|
275
|
+
))
|
|
276
|
+
setSelectedDirection(selectedIntegration.direction === 'export' ? 'export' : 'import')
|
|
277
|
+
}, [selectedIntegration])
|
|
278
|
+
|
|
279
|
+
React.useEffect(() => {
|
|
280
|
+
if (!selectedIntegration || !selectedEntityType) {
|
|
281
|
+
setScheduleEditor(buildDefaultScheduleState(selectedEntityType))
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const currentIntegration = selectedIntegration
|
|
286
|
+
let cancelled = false
|
|
287
|
+
async function loadSchedule() {
|
|
288
|
+
setIsLoadingSchedule(true)
|
|
289
|
+
const integrationId = currentIntegration.integrationId
|
|
290
|
+
const params = new URLSearchParams({
|
|
291
|
+
integrationId,
|
|
292
|
+
entityType: selectedEntityType,
|
|
293
|
+
direction: selectedDirection,
|
|
294
|
+
page: '1',
|
|
295
|
+
pageSize: '1',
|
|
296
|
+
})
|
|
297
|
+
const fallback: SyncSchedulesResponse = { items: [] }
|
|
298
|
+
const call = await apiCall<SyncSchedulesResponse>(`/api/data_sync/schedules?${params.toString()}`, undefined, { fallback })
|
|
299
|
+
|
|
300
|
+
if (cancelled) return
|
|
301
|
+
|
|
302
|
+
if (!call.ok) {
|
|
303
|
+
setScheduleEditor(buildDefaultScheduleState(selectedEntityType))
|
|
304
|
+
setIsLoadingSchedule(false)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const record = Array.isArray(call.result?.items) ? call.result?.items[0] : undefined
|
|
309
|
+
if (!record) {
|
|
310
|
+
setScheduleEditor(buildDefaultScheduleState(selectedEntityType))
|
|
311
|
+
setIsLoadingSchedule(false)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
setScheduleEditor({
|
|
316
|
+
id: record.id,
|
|
317
|
+
scheduleType: record.scheduleType,
|
|
318
|
+
scheduleValue: record.scheduleValue,
|
|
319
|
+
timezone: record.timezone,
|
|
320
|
+
fullSync: record.fullSync,
|
|
321
|
+
isEnabled: record.isEnabled,
|
|
322
|
+
lastRunAt: record.lastRunAt,
|
|
323
|
+
})
|
|
324
|
+
setIsLoadingSchedule(false)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
void loadSchedule()
|
|
328
|
+
return () => { cancelled = true }
|
|
329
|
+
}, [selectedDirection, selectedEntityType, selectedIntegration, scopeVersion])
|
|
330
|
+
|
|
331
|
+
const updateScheduleEditor = React.useCallback((changes: Partial<SyncScheduleEditorState>) => {
|
|
332
|
+
setScheduleEditor((current) => ({ ...current, ...changes }))
|
|
333
|
+
}, [])
|
|
334
|
+
|
|
88
335
|
const handleCancel = React.useCallback(async (row: SyncRunRow) => {
|
|
89
336
|
const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(row.id)}/cancel`, {
|
|
90
337
|
method: 'POST',
|
|
@@ -125,6 +372,153 @@ export default function SyncRunsDashboardPage() {
|
|
|
125
372
|
setPage(1)
|
|
126
373
|
}, [])
|
|
127
374
|
|
|
375
|
+
const handleStartSync = React.useCallback(async () => {
|
|
376
|
+
if (!selectedIntegration || !selectedEntityType) return
|
|
377
|
+
|
|
378
|
+
const parsedBatchSize = Number.parseInt(batchSize, 10)
|
|
379
|
+
if (!Number.isFinite(parsedBatchSize) || parsedBatchSize < 1 || parsedBatchSize > 1000) {
|
|
380
|
+
flash(t('data_sync.dashboard.start.invalidBatchSize', 'Batch size must be between 1 and 1000.'), 'error')
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const call = await runMutation({
|
|
386
|
+
operation: () => apiCall<{ id: string }>('/api/data_sync/run', {
|
|
387
|
+
method: 'POST',
|
|
388
|
+
headers: { 'Content-Type': 'application/json' },
|
|
389
|
+
body: JSON.stringify({
|
|
390
|
+
integrationId: selectedIntegration.integrationId,
|
|
391
|
+
entityType: selectedEntityType,
|
|
392
|
+
direction: selectedDirection,
|
|
393
|
+
batchSize: parsedBatchSize,
|
|
394
|
+
fullSync,
|
|
395
|
+
}),
|
|
396
|
+
}, { fallback: null }),
|
|
397
|
+
mutationPayload: {
|
|
398
|
+
integrationId: selectedIntegration.integrationId,
|
|
399
|
+
entityType: selectedEntityType,
|
|
400
|
+
direction: selectedDirection,
|
|
401
|
+
batchSize: parsedBatchSize,
|
|
402
|
+
fullSync,
|
|
403
|
+
},
|
|
404
|
+
context: {
|
|
405
|
+
operation: 'create',
|
|
406
|
+
actionId: 'start-sync-run',
|
|
407
|
+
integrationId: selectedIntegration.integrationId,
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
if (!call.ok || !call.result?.id) {
|
|
412
|
+
flash((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.start.error', 'Failed to start sync run'), 'error')
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
flash(t('data_sync.dashboard.start.success', 'Sync run started'), 'success')
|
|
417
|
+
setReloadToken((token) => token + 1)
|
|
418
|
+
router.push(`/backend/data-sync/runs/${encodeURIComponent(call.result.id)}`)
|
|
419
|
+
} catch (error) {
|
|
420
|
+
const message = error instanceof Error ? error.message : t('data_sync.dashboard.start.error', 'Failed to start sync run')
|
|
421
|
+
flash(message, 'error')
|
|
422
|
+
}
|
|
423
|
+
}, [batchSize, fullSync, router, runMutation, selectedDirection, selectedEntityType, selectedIntegration, t])
|
|
424
|
+
|
|
425
|
+
const handleSaveSchedule = React.useCallback(async () => {
|
|
426
|
+
if (!selectedIntegration || !selectedEntityType) return
|
|
427
|
+
if (scheduleEditor.scheduleValue.trim().length === 0) {
|
|
428
|
+
flash(t('data_sync.dashboard.schedule.invalidValue', 'Provide a schedule value before saving.'), 'error')
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
setIsSavingSchedule(true)
|
|
433
|
+
try {
|
|
434
|
+
const call = await runMutation({
|
|
435
|
+
operation: () => apiCall<SyncScheduleRecord>('/api/data_sync/schedules', {
|
|
436
|
+
method: 'POST',
|
|
437
|
+
headers: { 'Content-Type': 'application/json' },
|
|
438
|
+
body: JSON.stringify({
|
|
439
|
+
integrationId: selectedIntegration.integrationId,
|
|
440
|
+
entityType: selectedEntityType,
|
|
441
|
+
direction: selectedDirection,
|
|
442
|
+
scheduleType: scheduleEditor.scheduleType,
|
|
443
|
+
scheduleValue: scheduleEditor.scheduleValue.trim(),
|
|
444
|
+
timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,
|
|
445
|
+
fullSync: scheduleEditor.fullSync,
|
|
446
|
+
isEnabled: scheduleEditor.isEnabled,
|
|
447
|
+
}),
|
|
448
|
+
}, { fallback: null }),
|
|
449
|
+
mutationPayload: {
|
|
450
|
+
integrationId: selectedIntegration.integrationId,
|
|
451
|
+
entityType: selectedEntityType,
|
|
452
|
+
direction: selectedDirection,
|
|
453
|
+
scheduleType: scheduleEditor.scheduleType,
|
|
454
|
+
scheduleValue: scheduleEditor.scheduleValue.trim(),
|
|
455
|
+
timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,
|
|
456
|
+
fullSync: scheduleEditor.fullSync,
|
|
457
|
+
isEnabled: scheduleEditor.isEnabled,
|
|
458
|
+
},
|
|
459
|
+
context: {
|
|
460
|
+
operation: 'update',
|
|
461
|
+
actionId: 'save-sync-schedule',
|
|
462
|
+
integrationId: selectedIntegration.integrationId,
|
|
463
|
+
},
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
if (!call.ok || !call.result) {
|
|
467
|
+
flash((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule'), 'error')
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
setScheduleEditor({
|
|
472
|
+
id: call.result.id,
|
|
473
|
+
scheduleType: call.result.scheduleType,
|
|
474
|
+
scheduleValue: call.result.scheduleValue,
|
|
475
|
+
timezone: call.result.timezone,
|
|
476
|
+
fullSync: call.result.fullSync,
|
|
477
|
+
isEnabled: call.result.isEnabled,
|
|
478
|
+
lastRunAt: call.result.lastRunAt,
|
|
479
|
+
})
|
|
480
|
+
flash(t('data_sync.dashboard.schedule.success', 'Recurring schedule saved'), 'success')
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const message = error instanceof Error ? error.message : t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule')
|
|
483
|
+
flash(message, 'error')
|
|
484
|
+
} finally {
|
|
485
|
+
setIsSavingSchedule(false)
|
|
486
|
+
}
|
|
487
|
+
}, [runMutation, scheduleEditor, selectedDirection, selectedEntityType, selectedIntegration, t])
|
|
488
|
+
|
|
489
|
+
const handleDeleteSchedule = React.useCallback(async () => {
|
|
490
|
+
if (!scheduleEditor.id) return
|
|
491
|
+
|
|
492
|
+
setIsDeletingSchedule(true)
|
|
493
|
+
try {
|
|
494
|
+
const call = await runMutation({
|
|
495
|
+
operation: () => apiCall(`/api/data_sync/schedules/${encodeURIComponent(scheduleEditor.id as string)}`, {
|
|
496
|
+
method: 'DELETE',
|
|
497
|
+
}, { fallback: null }),
|
|
498
|
+
mutationPayload: {
|
|
499
|
+
scheduleId: scheduleEditor.id,
|
|
500
|
+
},
|
|
501
|
+
context: {
|
|
502
|
+
operation: 'delete',
|
|
503
|
+
actionId: 'delete-sync-schedule',
|
|
504
|
+
},
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
if (!call.ok) {
|
|
508
|
+
flash((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.deleteError', 'Failed to remove recurring schedule'), 'error')
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
setScheduleEditor(buildDefaultScheduleState(selectedEntityType))
|
|
513
|
+
flash(t('data_sync.dashboard.schedule.deleteSuccess', 'Recurring schedule removed'), 'success')
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const message = error instanceof Error ? error.message : t('data_sync.dashboard.schedule.deleteError', 'Failed to remove recurring schedule')
|
|
516
|
+
flash(message, 'error')
|
|
517
|
+
} finally {
|
|
518
|
+
setIsDeletingSchedule(false)
|
|
519
|
+
}
|
|
520
|
+
}, [runMutation, scheduleEditor.id, selectedEntityType, t])
|
|
521
|
+
|
|
128
522
|
const filters: FilterDef[] = [
|
|
129
523
|
{
|
|
130
524
|
id: 'status',
|
|
@@ -198,9 +592,369 @@ export default function SyncRunsDashboardPage() {
|
|
|
198
592
|
},
|
|
199
593
|
], [t])
|
|
200
594
|
|
|
595
|
+
const canStartSelectedIntegration = Boolean(
|
|
596
|
+
selectedIntegration
|
|
597
|
+
&& selectedEntityType
|
|
598
|
+
&& selectedIntegration.isEnabled
|
|
599
|
+
&& selectedIntegration.hasCredentials,
|
|
600
|
+
)
|
|
601
|
+
const hasSavedSchedule = Boolean(scheduleEditor.id)
|
|
602
|
+
const selectedEntityLabel = selectedEntityType ? formatEntityTypeLabel(selectedEntityType) : t('data_sync.dashboard.columns.entityType')
|
|
603
|
+
const integrationStateBadge = getSummaryBadgeStyle(selectedIntegration?.isEnabled ? 'enabled' : 'disabled')
|
|
604
|
+
const credentialsBadge = getSummaryBadgeStyle(selectedIntegration?.hasCredentials ? 'ready' : 'missing')
|
|
605
|
+
const scheduleBadge = getSummaryBadgeStyle(
|
|
606
|
+
hasSavedSchedule
|
|
607
|
+
? (scheduleEditor.isEnabled ? 'scheduled' : 'paused')
|
|
608
|
+
: 'none',
|
|
609
|
+
)
|
|
610
|
+
|
|
201
611
|
return (
|
|
202
612
|
<Page>
|
|
203
|
-
<PageBody>
|
|
613
|
+
<PageBody className="space-y-6">
|
|
614
|
+
<Card>
|
|
615
|
+
<CardHeader className="space-y-4">
|
|
616
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
617
|
+
<div className="space-y-2">
|
|
618
|
+
<div className="flex items-center gap-2 text-xs font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
|
619
|
+
<Repeat className="size-4" />
|
|
620
|
+
<span>{t('data_sync.dashboard.start.eyebrow', 'Run once or keep it recurring')}</span>
|
|
621
|
+
</div>
|
|
622
|
+
<div className="space-y-1">
|
|
623
|
+
<CardTitle>{t('data_sync.dashboard.start.title', 'Start or schedule a sync')}</CardTitle>
|
|
624
|
+
<p className="max-w-3xl text-sm text-muted-foreground">
|
|
625
|
+
{t('data_sync.dashboard.start.description', 'Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.')}
|
|
626
|
+
</p>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
{selectedIntegration ? (
|
|
630
|
+
<Button asChild variant="outline">
|
|
631
|
+
<Link href={selectedIntegration.settingsPath}>
|
|
632
|
+
<Settings2 className="mr-2 size-4" />
|
|
633
|
+
{t('integrations.marketplace.configure')}
|
|
634
|
+
</Link>
|
|
635
|
+
</Button>
|
|
636
|
+
) : null}
|
|
637
|
+
</div>
|
|
638
|
+
|
|
639
|
+
{selectedIntegration ? (
|
|
640
|
+
<div className="flex flex-wrap gap-2">
|
|
641
|
+
<Badge variant="outline" className="gap-1.5">
|
|
642
|
+
<PlugZap className="size-3.5" />
|
|
643
|
+
{selectedIntegration.title}
|
|
644
|
+
</Badge>
|
|
645
|
+
<Badge variant="outline" className="gap-1.5">
|
|
646
|
+
<ArrowRightLeft className="size-3.5" />
|
|
647
|
+
{t(`data_sync.dashboard.direction.${selectedDirection}`)}
|
|
648
|
+
</Badge>
|
|
649
|
+
<Badge variant={integrationStateBadge.variant} className={`gap-1.5 ${integrationStateBadge.className ?? ''}`}>
|
|
650
|
+
<ShieldCheck className="size-3.5" />
|
|
651
|
+
{selectedIntegration.isEnabled
|
|
652
|
+
? t('data_sync.dashboard.start.status.enabled', 'Integration enabled')
|
|
653
|
+
: t('data_sync.dashboard.start.status.disabled', 'Integration disabled')}
|
|
654
|
+
</Badge>
|
|
655
|
+
<Badge variant={credentialsBadge.variant} className={`gap-1.5 ${credentialsBadge.className ?? ''}`}>
|
|
656
|
+
<PlugZap className="size-3.5" />
|
|
657
|
+
{selectedIntegration.hasCredentials
|
|
658
|
+
? t('data_sync.dashboard.start.status.credentialsReady', 'Credentials ready')
|
|
659
|
+
: t('data_sync.dashboard.start.status.credentialsMissing', 'Credentials missing')}
|
|
660
|
+
</Badge>
|
|
661
|
+
<Badge variant={scheduleBadge.variant} className={`gap-1.5 ${scheduleBadge.className ?? ''}`}>
|
|
662
|
+
<CalendarClock className="size-3.5" />
|
|
663
|
+
{hasSavedSchedule
|
|
664
|
+
? (scheduleEditor.isEnabled
|
|
665
|
+
? t('data_sync.dashboard.schedule.status.enabled', 'Recurring schedule active')
|
|
666
|
+
: t('data_sync.dashboard.schedule.status.disabled', 'Recurring schedule paused'))
|
|
667
|
+
: t('data_sync.dashboard.schedule.status.none', 'No recurring schedule')}
|
|
668
|
+
</Badge>
|
|
669
|
+
</div>
|
|
670
|
+
) : null}
|
|
671
|
+
</CardHeader>
|
|
672
|
+
<CardContent className="space-y-6">
|
|
673
|
+
<div className="grid gap-4 xl:grid-cols-3">
|
|
674
|
+
<div className="space-y-2 xl:col-span-1">
|
|
675
|
+
<Label className="flex items-center gap-2 text-sm font-medium">
|
|
676
|
+
<PlugZap className="size-4 text-muted-foreground" />
|
|
677
|
+
<span>{t('data_sync.dashboard.columns.integration')}</span>
|
|
678
|
+
</Label>
|
|
679
|
+
<select
|
|
680
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
681
|
+
value={selectedIntegrationId}
|
|
682
|
+
onChange={(event) => setSelectedIntegrationId(event.target.value)}
|
|
683
|
+
disabled={isLoadingOptions || options.length === 0}
|
|
684
|
+
>
|
|
685
|
+
{options.length === 0 ? (
|
|
686
|
+
<option value="">{t('integrations.marketplace.noResults', 'No integrations found')}</option>
|
|
687
|
+
) : null}
|
|
688
|
+
{options.map((item) => (
|
|
689
|
+
<option key={item.integrationId} value={item.integrationId}>
|
|
690
|
+
{item.title}
|
|
691
|
+
</option>
|
|
692
|
+
))}
|
|
693
|
+
</select>
|
|
694
|
+
</div>
|
|
695
|
+
<div className="space-y-2">
|
|
696
|
+
<Label className="flex items-center gap-2 text-sm font-medium">
|
|
697
|
+
<Boxes className="size-4 text-muted-foreground" />
|
|
698
|
+
<span>{t('data_sync.dashboard.columns.entityType')}</span>
|
|
699
|
+
</Label>
|
|
700
|
+
<select
|
|
701
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
702
|
+
value={selectedEntityType}
|
|
703
|
+
onChange={(event) => setSelectedEntityType(event.target.value)}
|
|
704
|
+
disabled={entityOptions.length === 0}
|
|
705
|
+
>
|
|
706
|
+
{entityOptions.map((entityType) => (
|
|
707
|
+
<option key={entityType} value={entityType}>
|
|
708
|
+
{formatEntityTypeLabel(entityType)}
|
|
709
|
+
</option>
|
|
710
|
+
))}
|
|
711
|
+
</select>
|
|
712
|
+
</div>
|
|
713
|
+
<div className="space-y-2">
|
|
714
|
+
<Label className="flex items-center gap-2 text-sm font-medium">
|
|
715
|
+
<ArrowRightLeft className="size-4 text-muted-foreground" />
|
|
716
|
+
<span>{t('data_sync.dashboard.columns.direction')}</span>
|
|
717
|
+
</Label>
|
|
718
|
+
<select
|
|
719
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
720
|
+
value={selectedDirection}
|
|
721
|
+
onChange={(event) => setSelectedDirection(event.target.value === 'export' ? 'export' : 'import')}
|
|
722
|
+
disabled={selectedIntegration?.direction !== 'bidirectional'}
|
|
723
|
+
>
|
|
724
|
+
<option value="import">{t('data_sync.dashboard.direction.import')}</option>
|
|
725
|
+
{(selectedIntegration?.direction === 'bidirectional' || selectedIntegration?.direction === 'export') ? (
|
|
726
|
+
<option value="export">{t('data_sync.dashboard.direction.export')}</option>
|
|
727
|
+
) : null}
|
|
728
|
+
</select>
|
|
729
|
+
</div>
|
|
730
|
+
</div>
|
|
731
|
+
|
|
732
|
+
{selectedIntegration?.description ? (
|
|
733
|
+
<p className="text-sm text-muted-foreground">{selectedIntegration.description}</p>
|
|
734
|
+
) : null}
|
|
735
|
+
|
|
736
|
+
<div className="grid gap-4 xl:grid-cols-2">
|
|
737
|
+
<div className="rounded-xl border bg-muted/20 p-4">
|
|
738
|
+
<div className="flex items-start justify-between gap-3">
|
|
739
|
+
<div className="space-y-1">
|
|
740
|
+
<div className="flex items-center gap-2">
|
|
741
|
+
<Play className="size-4 text-primary" />
|
|
742
|
+
<h3 className="text-sm font-semibold">{t('data_sync.dashboard.start.runNowTitle', 'Run once now')}</h3>
|
|
743
|
+
</div>
|
|
744
|
+
<p className="text-sm text-muted-foreground">
|
|
745
|
+
{t('data_sync.dashboard.start.runNowDescription', 'Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.')}
|
|
746
|
+
</p>
|
|
747
|
+
</div>
|
|
748
|
+
<Badge variant="outline">{selectedEntityLabel}</Badge>
|
|
749
|
+
</div>
|
|
750
|
+
|
|
751
|
+
<Separator className="my-4" />
|
|
752
|
+
|
|
753
|
+
<div className="grid gap-4 sm:grid-cols-[minmax(0,180px)_1fr]">
|
|
754
|
+
<div className="space-y-2">
|
|
755
|
+
<Label className="flex items-center gap-2 text-sm font-medium">
|
|
756
|
+
<Gauge className="size-4 text-muted-foreground" />
|
|
757
|
+
<span>{t('data_sync.dashboard.start.batchSize', 'Batch size')}</span>
|
|
758
|
+
</Label>
|
|
759
|
+
<Input
|
|
760
|
+
value={batchSize}
|
|
761
|
+
onChange={(event) => setBatchSize(event.target.value)}
|
|
762
|
+
inputMode="numeric"
|
|
763
|
+
/>
|
|
764
|
+
</div>
|
|
765
|
+
<div className="rounded-lg border bg-background p-3">
|
|
766
|
+
<div className="flex items-center justify-between gap-3">
|
|
767
|
+
<div className="space-y-1">
|
|
768
|
+
<Label className="text-sm font-medium">{t('data_sync.dashboard.start.fullSync', 'Run as full sync')}</Label>
|
|
769
|
+
<p className="text-xs text-muted-foreground">
|
|
770
|
+
{t('data_sync.dashboard.start.fullSyncHelp', 'Ignore the saved cursor and process the entire source again for this run.')}
|
|
771
|
+
</p>
|
|
772
|
+
</div>
|
|
773
|
+
<Switch checked={fullSync} onCheckedChange={setFullSync} />
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
</div>
|
|
777
|
+
|
|
778
|
+
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
|
|
779
|
+
<p className="text-xs text-muted-foreground">
|
|
780
|
+
{t('data_sync.dashboard.start.runNowFootnote', 'Manual runs show progress immediately and land on the run detail page after launch.')}
|
|
781
|
+
</p>
|
|
782
|
+
<Button
|
|
783
|
+
type="button"
|
|
784
|
+
onClick={() => void handleStartSync()}
|
|
785
|
+
disabled={!canStartSelectedIntegration}
|
|
786
|
+
>
|
|
787
|
+
<Play className="mr-2 size-4" />
|
|
788
|
+
{t('data_sync.dashboard.start.submit', 'Start sync')}
|
|
789
|
+
</Button>
|
|
790
|
+
</div>
|
|
791
|
+
</div>
|
|
792
|
+
|
|
793
|
+
<div className="rounded-xl border bg-muted/20 p-4">
|
|
794
|
+
<div className="flex items-start justify-between gap-3">
|
|
795
|
+
<div className="space-y-1">
|
|
796
|
+
<div className="flex items-center gap-2">
|
|
797
|
+
<CalendarClock className="size-4 text-primary" />
|
|
798
|
+
<h3 className="text-sm font-semibold">{t('data_sync.dashboard.schedule.title', 'Recurring schedule')}</h3>
|
|
799
|
+
</div>
|
|
800
|
+
<p className="text-sm text-muted-foreground">
|
|
801
|
+
{t('data_sync.dashboard.schedule.description', 'Save a repeating schedule for the selected integration, entity, and direction without leaving this dashboard.')}
|
|
802
|
+
</p>
|
|
803
|
+
</div>
|
|
804
|
+
<Badge variant="outline">
|
|
805
|
+
{hasSavedSchedule
|
|
806
|
+
? (scheduleEditor.isEnabled
|
|
807
|
+
? t('data_sync.dashboard.schedule.status.shortEnabled', 'Scheduled')
|
|
808
|
+
: t('data_sync.dashboard.schedule.status.shortDisabled', 'Paused'))
|
|
809
|
+
: t('data_sync.dashboard.schedule.status.shortNone', 'One-time only')}
|
|
810
|
+
</Badge>
|
|
811
|
+
</div>
|
|
812
|
+
|
|
813
|
+
<Separator className="my-4" />
|
|
814
|
+
|
|
815
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
816
|
+
<div className="space-y-2">
|
|
817
|
+
<Label className="flex items-center gap-2 text-sm font-medium">
|
|
818
|
+
<Clock3 className="size-4 text-muted-foreground" />
|
|
819
|
+
<span>{t('data_sync.dashboard.schedule.type', 'Schedule type')}</span>
|
|
820
|
+
</Label>
|
|
821
|
+
<select
|
|
822
|
+
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
|
823
|
+
value={scheduleEditor.scheduleType}
|
|
824
|
+
onChange={(event) => updateScheduleEditor({
|
|
825
|
+
scheduleType: event.target.value === 'cron' ? 'cron' : 'interval',
|
|
826
|
+
})}
|
|
827
|
+
disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}
|
|
828
|
+
>
|
|
829
|
+
<option value="interval">{t('data_sync.dashboard.schedule.interval', 'Interval')}</option>
|
|
830
|
+
<option value="cron">{t('data_sync.dashboard.schedule.cron', 'Cron')}</option>
|
|
831
|
+
</select>
|
|
832
|
+
</div>
|
|
833
|
+
<div className="space-y-2">
|
|
834
|
+
<Label className="flex items-center gap-2 text-sm font-medium">
|
|
835
|
+
<CalendarClock className="size-4 text-muted-foreground" />
|
|
836
|
+
<span>
|
|
837
|
+
{scheduleEditor.scheduleType === 'cron'
|
|
838
|
+
? t('data_sync.dashboard.schedule.cronValue', 'Cron expression')
|
|
839
|
+
: t('data_sync.dashboard.schedule.intervalValue', 'Interval')}
|
|
840
|
+
</span>
|
|
841
|
+
</Label>
|
|
842
|
+
<Input
|
|
843
|
+
value={scheduleEditor.scheduleValue}
|
|
844
|
+
onChange={(event) => updateScheduleEditor({ scheduleValue: event.target.value })}
|
|
845
|
+
disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}
|
|
846
|
+
placeholder={scheduleEditor.scheduleType === 'cron' ? '0 * * * *' : '1h'}
|
|
847
|
+
/>
|
|
848
|
+
<p className="text-xs text-muted-foreground">
|
|
849
|
+
{scheduleEditor.scheduleType === 'cron'
|
|
850
|
+
? t('data_sync.dashboard.schedule.cronHelp', 'Example: `0 * * * *` runs at the start of every hour.')
|
|
851
|
+
: t('data_sync.dashboard.schedule.intervalHelp', 'Example: `1h`, `6h`, or `24h` for repeating intervals.')}
|
|
852
|
+
</p>
|
|
853
|
+
</div>
|
|
854
|
+
<div className="space-y-2 sm:col-span-2">
|
|
855
|
+
<Label className="flex items-center gap-2 text-sm font-medium">
|
|
856
|
+
<Clock3 className="size-4 text-muted-foreground" />
|
|
857
|
+
<span>{t('data_sync.dashboard.schedule.timezone', 'Timezone')}</span>
|
|
858
|
+
</Label>
|
|
859
|
+
<Input
|
|
860
|
+
value={scheduleEditor.timezone}
|
|
861
|
+
onChange={(event) => updateScheduleEditor({ timezone: event.target.value })}
|
|
862
|
+
disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}
|
|
863
|
+
/>
|
|
864
|
+
</div>
|
|
865
|
+
</div>
|
|
866
|
+
|
|
867
|
+
<div className="mt-4 grid gap-3">
|
|
868
|
+
<div className="rounded-lg border bg-background p-3">
|
|
869
|
+
<div className="flex items-center justify-between gap-3">
|
|
870
|
+
<div className="space-y-1">
|
|
871
|
+
<Label className="text-sm font-medium">{t('data_sync.dashboard.schedule.fullSync', 'Run scheduled jobs as full sync')}</Label>
|
|
872
|
+
<p className="text-xs text-muted-foreground">
|
|
873
|
+
{t('data_sync.dashboard.schedule.fullSyncHelp', 'When enabled, every recurring run starts from the beginning instead of the saved cursor.')}
|
|
874
|
+
</p>
|
|
875
|
+
</div>
|
|
876
|
+
<Switch
|
|
877
|
+
checked={scheduleEditor.fullSync}
|
|
878
|
+
onCheckedChange={(checked) => updateScheduleEditor({ fullSync: checked })}
|
|
879
|
+
disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}
|
|
880
|
+
/>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
<div className="rounded-lg border bg-background p-3">
|
|
884
|
+
<div className="flex items-center justify-between gap-3">
|
|
885
|
+
<div className="space-y-1">
|
|
886
|
+
<Label className="text-sm font-medium">{t('data_sync.dashboard.schedule.enabled', 'Schedule enabled')}</Label>
|
|
887
|
+
<p className="text-xs text-muted-foreground">
|
|
888
|
+
{t('data_sync.dashboard.schedule.enabledHelp', 'Pause the recurring job without deleting the schedule definition.')}
|
|
889
|
+
</p>
|
|
890
|
+
</div>
|
|
891
|
+
<Switch
|
|
892
|
+
checked={scheduleEditor.isEnabled}
|
|
893
|
+
onCheckedChange={(checked) => updateScheduleEditor({ isEnabled: checked })}
|
|
894
|
+
disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}
|
|
895
|
+
/>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
</div>
|
|
899
|
+
|
|
900
|
+
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
|
|
901
|
+
<div className="space-y-1 text-xs text-muted-foreground">
|
|
902
|
+
<div>
|
|
903
|
+
{hasSavedSchedule
|
|
904
|
+
? (scheduleEditor.lastRunAt
|
|
905
|
+
? t('data_sync.dashboard.schedule.lastRun', 'Last scheduled run: {value}', {
|
|
906
|
+
value: new Date(scheduleEditor.lastRunAt).toLocaleString(),
|
|
907
|
+
})
|
|
908
|
+
: t('data_sync.dashboard.schedule.neverRun', 'Saved, but no scheduled execution has completed yet.'))
|
|
909
|
+
: t('data_sync.dashboard.schedule.none', 'No recurring schedule saved for this target yet.')}
|
|
910
|
+
</div>
|
|
911
|
+
</div>
|
|
912
|
+
<div className="flex flex-wrap gap-2">
|
|
913
|
+
<Button
|
|
914
|
+
type="button"
|
|
915
|
+
variant="outline"
|
|
916
|
+
onClick={() => void handleDeleteSchedule()}
|
|
917
|
+
disabled={!hasSavedSchedule || isDeletingSchedule}
|
|
918
|
+
>
|
|
919
|
+
{isDeletingSchedule
|
|
920
|
+
? t('data_sync.dashboard.schedule.deleting', 'Removing...')
|
|
921
|
+
: t('data_sync.dashboard.schedule.delete', 'Remove schedule')}
|
|
922
|
+
</Button>
|
|
923
|
+
<Button
|
|
924
|
+
type="button"
|
|
925
|
+
variant="outline"
|
|
926
|
+
onClick={() => void handleSaveSchedule()}
|
|
927
|
+
disabled={isSavingSchedule || !selectedIntegration || !selectedEntityType}
|
|
928
|
+
>
|
|
929
|
+
<CalendarClock className="mr-2 size-4" />
|
|
930
|
+
{isSavingSchedule
|
|
931
|
+
? t('data_sync.dashboard.schedule.saving', 'Saving...')
|
|
932
|
+
: t('data_sync.dashboard.schedule.save', 'Save recurring schedule')}
|
|
933
|
+
</Button>
|
|
934
|
+
</div>
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
|
|
939
|
+
{selectedIntegration && !selectedIntegration.isEnabled ? (
|
|
940
|
+
<Notice compact variant="warning">
|
|
941
|
+
<span className="inline-flex items-center gap-2">
|
|
942
|
+
<CircleAlert className="size-4" />
|
|
943
|
+
<span>{t('integrations.detail.state.disabled', 'This integration is disabled. Enable it on the integration settings page before starting a sync.')}</span>
|
|
944
|
+
</span>
|
|
945
|
+
</Notice>
|
|
946
|
+
) : null}
|
|
947
|
+
{selectedIntegration && !selectedIntegration.hasCredentials ? (
|
|
948
|
+
<Notice compact variant="warning">
|
|
949
|
+
<span className="inline-flex items-center gap-2">
|
|
950
|
+
<CircleAlert className="size-4" />
|
|
951
|
+
<span>{t('integrations.detail.credentials.notConfigured', 'Credentials are not configured yet. Save the integration credentials before starting a sync.')}</span>
|
|
952
|
+
</span>
|
|
953
|
+
</Notice>
|
|
954
|
+
) : null}
|
|
955
|
+
</CardContent>
|
|
956
|
+
</Card>
|
|
957
|
+
|
|
204
958
|
<DataTable
|
|
205
959
|
title={t('data_sync.dashboard.title')}
|
|
206
960
|
columns={columns}
|