@open-mercato/search 0.4.2-canary-c02407ff85
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 +678 -0
- package/build.mjs +92 -0
- package/dist/di.js +157 -0
- package/dist/di.js.map +7 -0
- package/dist/fulltext/drivers/index.js +21 -0
- package/dist/fulltext/drivers/index.js.map +7 -0
- package/dist/fulltext/drivers/meilisearch/index.js +320 -0
- package/dist/fulltext/drivers/meilisearch/index.js.map +7 -0
- package/dist/fulltext/index.js +7 -0
- package/dist/fulltext/index.js.map +7 -0
- package/dist/fulltext/types.js +1 -0
- package/dist/fulltext/types.js.map +7 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +7 -0
- package/dist/indexer/index.js +8 -0
- package/dist/indexer/index.js.map +7 -0
- package/dist/indexer/search-indexer.js +848 -0
- package/dist/indexer/search-indexer.js.map +7 -0
- package/dist/indexer/subscribers/delete.js +41 -0
- package/dist/indexer/subscribers/delete.js.map +7 -0
- package/dist/lib/debug.js +34 -0
- package/dist/lib/debug.js.map +7 -0
- package/dist/lib/fallback-presenter.js +107 -0
- package/dist/lib/fallback-presenter.js.map +7 -0
- package/dist/lib/field-policy.js +75 -0
- package/dist/lib/field-policy.js.map +7 -0
- package/dist/lib/index.js +19 -0
- package/dist/lib/index.js.map +7 -0
- package/dist/lib/merger.js +93 -0
- package/dist/lib/merger.js.map +7 -0
- package/dist/lib/presenter-enricher.js +192 -0
- package/dist/lib/presenter-enricher.js.map +7 -0
- package/dist/modules/search/acl.js +14 -0
- package/dist/modules/search/acl.js.map +7 -0
- package/dist/modules/search/ai-tools.js +284 -0
- package/dist/modules/search/ai-tools.js.map +7 -0
- package/dist/modules/search/api/embeddings/reindex/cancel/route.js +65 -0
- package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +7 -0
- package/dist/modules/search/api/embeddings/reindex/route.js +165 -0
- package/dist/modules/search/api/embeddings/reindex/route.js.map +7 -0
- package/dist/modules/search/api/embeddings/route.js +246 -0
- package/dist/modules/search/api/embeddings/route.js.map +7 -0
- package/dist/modules/search/api/index/route.js +245 -0
- package/dist/modules/search/api/index/route.js.map +7 -0
- package/dist/modules/search/api/reindex/cancel/route.js +65 -0
- package/dist/modules/search/api/reindex/cancel/route.js.map +7 -0
- package/dist/modules/search/api/reindex/route.js +332 -0
- package/dist/modules/search/api/reindex/route.js.map +7 -0
- package/dist/modules/search/api/search/global/route.js +100 -0
- package/dist/modules/search/api/search/global/route.js.map +7 -0
- package/dist/modules/search/api/search/route.js +101 -0
- package/dist/modules/search/api/search/route.js.map +7 -0
- package/dist/modules/search/api/settings/fulltext/route.js +55 -0
- package/dist/modules/search/api/settings/fulltext/route.js.map +7 -0
- package/dist/modules/search/api/settings/global-search/route.js +80 -0
- package/dist/modules/search/api/settings/global-search/route.js.map +7 -0
- package/dist/modules/search/api/settings/route.js +118 -0
- package/dist/modules/search/api/settings/route.js.map +7 -0
- package/dist/modules/search/api/settings/vector-store/route.js +77 -0
- package/dist/modules/search/api/settings/vector-store/route.js.map +7 -0
- package/dist/modules/search/backend/config/search/page.js +10 -0
- package/dist/modules/search/backend/config/search/page.js.map +7 -0
- package/dist/modules/search/backend/config/search/page.meta.js +24 -0
- package/dist/modules/search/backend/config/search/page.meta.js.map +7 -0
- package/dist/modules/search/cli.js +698 -0
- package/dist/modules/search/cli.js.map +7 -0
- package/dist/modules/search/di.js +32 -0
- package/dist/modules/search/di.js.map +7 -0
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js +357 -0
- package/dist/modules/search/frontend/components/GlobalSearchDialog.js.map +7 -0
- package/dist/modules/search/frontend/components/HybridSearchTable.js +343 -0
- package/dist/modules/search/frontend/components/HybridSearchTable.js.map +7 -0
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +303 -0
- package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +360 -0
- package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js +101 -0
- package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +608 -0
- package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +7 -0
- package/dist/modules/search/frontend/index.js +9 -0
- package/dist/modules/search/frontend/index.js.map +7 -0
- package/dist/modules/search/frontend/utils.js +41 -0
- package/dist/modules/search/frontend/utils.js.map +7 -0
- package/dist/modules/search/i18n/de.json +61 -0
- package/dist/modules/search/i18n/en.json +72 -0
- package/dist/modules/search/i18n/es.json +61 -0
- package/dist/modules/search/i18n/pl.json +61 -0
- package/dist/modules/search/index.js +11 -0
- package/dist/modules/search/index.js.map +7 -0
- package/dist/modules/search/lib/auto-indexing.js +29 -0
- package/dist/modules/search/lib/auto-indexing.js.map +7 -0
- package/dist/modules/search/lib/embedding-config.js +131 -0
- package/dist/modules/search/lib/embedding-config.js.map +7 -0
- package/dist/modules/search/lib/global-search-config.js +45 -0
- package/dist/modules/search/lib/global-search-config.js.map +7 -0
- package/dist/modules/search/lib/reindex-lock.js +99 -0
- package/dist/modules/search/lib/reindex-lock.js.map +7 -0
- package/dist/modules/search/subscribers/fulltext_upsert.js +64 -0
- package/dist/modules/search/subscribers/fulltext_upsert.js.map +7 -0
- package/dist/modules/search/subscribers/vector_delete.js +58 -0
- package/dist/modules/search/subscribers/vector_delete.js.map +7 -0
- package/dist/modules/search/subscribers/vector_purge.js +142 -0
- package/dist/modules/search/subscribers/vector_purge.js.map +7 -0
- package/dist/modules/search/subscribers/vector_upsert.js +58 -0
- package/dist/modules/search/subscribers/vector_upsert.js.map +7 -0
- package/dist/modules/search/workers/fulltext-index.worker.js +240 -0
- package/dist/modules/search/workers/fulltext-index.worker.js.map +7 -0
- package/dist/modules/search/workers/vector-index.worker.js +234 -0
- package/dist/modules/search/workers/vector-index.worker.js.map +7 -0
- package/dist/queue/fulltext-indexing.js +15 -0
- package/dist/queue/fulltext-indexing.js.map +7 -0
- package/dist/queue/index.js +3 -0
- package/dist/queue/index.js.map +7 -0
- package/dist/queue/vector-indexing.js +15 -0
- package/dist/queue/vector-indexing.js.map +7 -0
- package/dist/service.js +286 -0
- package/dist/service.js.map +7 -0
- package/dist/strategies/fulltext.strategy.js +116 -0
- package/dist/strategies/fulltext.strategy.js.map +7 -0
- package/dist/strategies/index.js +12 -0
- package/dist/strategies/index.js.map +7 -0
- package/dist/strategies/token.strategy.js +80 -0
- package/dist/strategies/token.strategy.js.map +7 -0
- package/dist/strategies/vector.strategy.js +137 -0
- package/dist/strategies/vector.strategy.js.map +7 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +7 -0
- package/dist/vector/drivers/chromadb/index.js +44 -0
- package/dist/vector/drivers/chromadb/index.js.map +7 -0
- package/dist/vector/drivers/index.js +9 -0
- package/dist/vector/drivers/index.js.map +7 -0
- package/dist/vector/drivers/pgvector/index.js +509 -0
- package/dist/vector/drivers/pgvector/index.js.map +7 -0
- package/dist/vector/drivers/qdrant/index.js +44 -0
- package/dist/vector/drivers/qdrant/index.js.map +7 -0
- package/dist/vector/index.js +4 -0
- package/dist/vector/index.js.map +7 -0
- package/dist/vector/lib/vector-logs.js +33 -0
- package/dist/vector/lib/vector-logs.js.map +7 -0
- package/dist/vector/services/checksum.js +20 -0
- package/dist/vector/services/checksum.js.map +7 -0
- package/dist/vector/services/embedding.js +222 -0
- package/dist/vector/services/embedding.js.map +7 -0
- package/dist/vector/services/index.js +4 -0
- package/dist/vector/services/index.js.map +7 -0
- package/dist/vector/services/vector-index.service.js +960 -0
- package/dist/vector/services/vector-index.service.js.map +7 -0
- package/dist/vector/types/pg.d.js +1 -0
- package/dist/vector/types/pg.d.js.map +7 -0
- package/dist/vector/types.js +75 -0
- package/dist/vector/types.js.map +7 -0
- package/jest.config.cjs +19 -0
- package/package.json +142 -0
- package/src/__tests__/queue.test.ts +148 -0
- package/src/__tests__/service.test.ts +345 -0
- package/src/__tests__/workers.test.ts +319 -0
- package/src/di.ts +291 -0
- package/src/fulltext/drivers/index.ts +41 -0
- package/src/fulltext/drivers/meilisearch/index.ts +410 -0
- package/src/fulltext/index.ts +13 -0
- package/src/fulltext/types.ts +115 -0
- package/src/index.ts +36 -0
- package/src/indexer/index.ts +13 -0
- package/src/indexer/search-indexer.ts +1141 -0
- package/src/indexer/subscribers/delete.ts +49 -0
- package/src/lib/debug.ts +46 -0
- package/src/lib/fallback-presenter.ts +106 -0
- package/src/lib/field-policy.ts +169 -0
- package/src/lib/index.ts +13 -0
- package/src/lib/merger.ts +159 -0
- package/src/lib/presenter-enricher.ts +323 -0
- package/src/modules/search/README.md +694 -0
- package/src/modules/search/acl.ts +10 -0
- package/src/modules/search/ai-tools.ts +467 -0
- package/src/modules/search/api/embeddings/reindex/cancel/route.ts +77 -0
- package/src/modules/search/api/embeddings/reindex/route.ts +197 -0
- package/src/modules/search/api/embeddings/route.ts +304 -0
- package/src/modules/search/api/index/route.ts +297 -0
- package/src/modules/search/api/reindex/cancel/route.ts +77 -0
- package/src/modules/search/api/reindex/route.ts +419 -0
- package/src/modules/search/api/search/global/route.ts +120 -0
- package/src/modules/search/api/search/route.ts +121 -0
- package/src/modules/search/api/settings/fulltext/route.ts +82 -0
- package/src/modules/search/api/settings/global-search/route.ts +91 -0
- package/src/modules/search/api/settings/route.ts +187 -0
- package/src/modules/search/api/settings/vector-store/route.ts +105 -0
- package/src/modules/search/backend/config/search/page.meta.ts +22 -0
- package/src/modules/search/backend/config/search/page.tsx +12 -0
- package/src/modules/search/cli.ts +818 -0
- package/src/modules/search/di.ts +50 -0
- package/src/modules/search/frontend/components/GlobalSearchDialog.tsx +436 -0
- package/src/modules/search/frontend/components/HybridSearchTable.tsx +418 -0
- package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +476 -0
- package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +624 -0
- package/src/modules/search/frontend/components/sections/GlobalSearchSection.tsx +124 -0
- package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +943 -0
- package/src/modules/search/frontend/index.ts +3 -0
- package/src/modules/search/frontend/utils.ts +82 -0
- package/src/modules/search/i18n/de.json +61 -0
- package/src/modules/search/i18n/en.json +72 -0
- package/src/modules/search/i18n/es.json +61 -0
- package/src/modules/search/i18n/pl.json +61 -0
- package/src/modules/search/index.ts +9 -0
- package/src/modules/search/lib/auto-indexing.ts +35 -0
- package/src/modules/search/lib/embedding-config.ts +161 -0
- package/src/modules/search/lib/global-search-config.ts +69 -0
- package/src/modules/search/lib/reindex-lock.ts +201 -0
- package/src/modules/search/subscribers/fulltext_upsert.ts +83 -0
- package/src/modules/search/subscribers/vector_delete.ts +75 -0
- package/src/modules/search/subscribers/vector_purge.ts +161 -0
- package/src/modules/search/subscribers/vector_upsert.ts +75 -0
- package/src/modules/search/workers/fulltext-index.worker.ts +318 -0
- package/src/modules/search/workers/vector-index.worker.ts +292 -0
- package/src/queue/fulltext-indexing.ts +87 -0
- package/src/queue/index.ts +2 -0
- package/src/queue/vector-indexing.ts +66 -0
- package/src/service.ts +397 -0
- package/src/strategies/fulltext.strategy.ts +155 -0
- package/src/strategies/index.ts +17 -0
- package/src/strategies/token.strategy.ts +153 -0
- package/src/strategies/vector.strategy.ts +234 -0
- package/src/types.ts +38 -0
- package/src/vector/drivers/chromadb/index.ts +49 -0
- package/src/vector/drivers/index.ts +4 -0
- package/src/vector/drivers/pgvector/index.ts +627 -0
- package/src/vector/drivers/qdrant/index.ts +49 -0
- package/src/vector/index.ts +3 -0
- package/src/vector/lib/vector-logs.ts +46 -0
- package/src/vector/services/checksum.ts +18 -0
- package/src/vector/services/embedding.ts +275 -0
- package/src/vector/services/index.ts +3 -0
- package/src/vector/services/vector-index.service.ts +1234 -0
- package/src/vector/types/pg.d.ts +1 -0
- package/src/vector/types.ts +220 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
7
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
9
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@open-mercato/ui/primitives/tabs'
|
|
10
|
+
|
|
11
|
+
// Types
|
|
12
|
+
type FulltextStats = {
|
|
13
|
+
numberOfDocuments: number
|
|
14
|
+
isIndexing: boolean
|
|
15
|
+
fieldDistribution: Record<string, number>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ReindexLock = {
|
|
19
|
+
type: 'fulltext' | 'vector'
|
|
20
|
+
action: string
|
|
21
|
+
startedAt: string
|
|
22
|
+
elapsedMinutes: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type FulltextEnvVarStatus = {
|
|
26
|
+
set: boolean
|
|
27
|
+
hint: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type FulltextOptionalEnvVarStatus = {
|
|
31
|
+
set: boolean
|
|
32
|
+
value?: string | boolean
|
|
33
|
+
default?: string | boolean
|
|
34
|
+
hint: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type FulltextConfigResponse = {
|
|
38
|
+
driver: 'meilisearch' | null
|
|
39
|
+
configured: boolean
|
|
40
|
+
envVars: {
|
|
41
|
+
MEILISEARCH_HOST: FulltextEnvVarStatus
|
|
42
|
+
MEILISEARCH_API_KEY: FulltextEnvVarStatus
|
|
43
|
+
}
|
|
44
|
+
optionalEnvVars: {
|
|
45
|
+
MEILISEARCH_INDEX_PREFIX: FulltextOptionalEnvVarStatus
|
|
46
|
+
SEARCH_EXCLUDE_ENCRYPTED_FIELDS: FulltextOptionalEnvVarStatus
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type ReindexResponse = {
|
|
51
|
+
ok: boolean
|
|
52
|
+
action: string
|
|
53
|
+
entityId?: string | null
|
|
54
|
+
result?: {
|
|
55
|
+
entitiesProcessed: number
|
|
56
|
+
recordsIndexed: number
|
|
57
|
+
errors?: Array<{ entityId: string; error: string }>
|
|
58
|
+
}
|
|
59
|
+
stats?: FulltextStats | null
|
|
60
|
+
error?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type ReindexAction = 'clear' | 'recreate' | 'reindex'
|
|
64
|
+
|
|
65
|
+
type ActivityLog = {
|
|
66
|
+
id: string
|
|
67
|
+
source: string
|
|
68
|
+
handler: string
|
|
69
|
+
level: 'info' | 'error' | 'warn'
|
|
70
|
+
entityType: string | null
|
|
71
|
+
recordId: string | null
|
|
72
|
+
message: string
|
|
73
|
+
details: unknown
|
|
74
|
+
occurredAt: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type FulltextSearchSectionProps = {
|
|
78
|
+
fulltextConfig: FulltextConfigResponse | null
|
|
79
|
+
fulltextConfigLoading: boolean
|
|
80
|
+
fulltextStats: FulltextStats | null
|
|
81
|
+
fulltextReindexLock: ReindexLock | null
|
|
82
|
+
loading: boolean
|
|
83
|
+
onStatsUpdate: (stats: FulltextStats | null) => void
|
|
84
|
+
onRefresh: () => Promise<void>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const normalizeErrorMessage = (error: unknown, fallback: string): string => {
|
|
88
|
+
if (typeof error === 'string' && error.trim().length) return error.trim()
|
|
89
|
+
if (error instanceof Error && error.message.trim().length) return error.message.trim()
|
|
90
|
+
return fallback
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function FulltextSearchSection({
|
|
94
|
+
fulltextConfig,
|
|
95
|
+
fulltextConfigLoading,
|
|
96
|
+
fulltextStats,
|
|
97
|
+
fulltextReindexLock,
|
|
98
|
+
loading,
|
|
99
|
+
onStatsUpdate,
|
|
100
|
+
onRefresh,
|
|
101
|
+
}: FulltextSearchSectionProps) {
|
|
102
|
+
const t = useT()
|
|
103
|
+
const [reindexing, setReindexing] = React.useState<ReindexAction | null>(null)
|
|
104
|
+
const [showReindexDialog, setShowReindexDialog] = React.useState<ReindexAction | null>(null)
|
|
105
|
+
const [activityLogs, setActivityLogs] = React.useState<ActivityLog[]>([])
|
|
106
|
+
const [activityLoading, setActivityLoading] = React.useState(true)
|
|
107
|
+
|
|
108
|
+
// Fetch activity logs
|
|
109
|
+
const fetchActivityLogs = React.useCallback(async () => {
|
|
110
|
+
setActivityLoading(true)
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch('/api/query_index/status')
|
|
113
|
+
if (response.ok) {
|
|
114
|
+
const body = await response.json() as { logs?: ActivityLog[]; errors?: ActivityLog[] }
|
|
115
|
+
// Combine logs and errors
|
|
116
|
+
const allLogs: ActivityLog[] = []
|
|
117
|
+
if (body.logs) {
|
|
118
|
+
allLogs.push(...body.logs)
|
|
119
|
+
}
|
|
120
|
+
if (body.errors) {
|
|
121
|
+
allLogs.push(...body.errors.map(err => ({ ...err, level: 'error' as const })))
|
|
122
|
+
}
|
|
123
|
+
// Filter for fulltext-related logs (exclude vector/embedding related)
|
|
124
|
+
const fulltextLogs = allLogs.filter(log => {
|
|
125
|
+
const lowerSource = log.source?.toLowerCase() ?? ''
|
|
126
|
+
const lowerMessage = log.message?.toLowerCase() ?? ''
|
|
127
|
+
const lowerHandler = log.handler?.toLowerCase() ?? ''
|
|
128
|
+
const isVector = lowerSource.includes('vector') || lowerMessage.includes('vector') ||
|
|
129
|
+
lowerMessage.includes('embedding') || lowerHandler.includes('vector')
|
|
130
|
+
return !isVector
|
|
131
|
+
})
|
|
132
|
+
// Sort by occurredAt descending
|
|
133
|
+
fulltextLogs.sort((a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime())
|
|
134
|
+
setActivityLogs(fulltextLogs.slice(0, 50))
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Silently fail
|
|
138
|
+
} finally {
|
|
139
|
+
setActivityLoading(false)
|
|
140
|
+
}
|
|
141
|
+
}, [])
|
|
142
|
+
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
fetchActivityLogs()
|
|
145
|
+
}, [fetchActivityLogs])
|
|
146
|
+
|
|
147
|
+
// Poll for activity when reindexing
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
if (fulltextReindexLock || reindexing) {
|
|
150
|
+
const interval = setInterval(fetchActivityLogs, 5000)
|
|
151
|
+
return () => clearInterval(interval)
|
|
152
|
+
}
|
|
153
|
+
}, [fulltextReindexLock, reindexing, fetchActivityLogs])
|
|
154
|
+
|
|
155
|
+
const handleReindexClick = (action: ReindexAction) => {
|
|
156
|
+
setShowReindexDialog(action)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const handleReindexCancel = () => {
|
|
160
|
+
setShowReindexDialog(null)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const handleReindexConfirm = React.useCallback(async () => {
|
|
164
|
+
const action = showReindexDialog
|
|
165
|
+
if (!action) return
|
|
166
|
+
|
|
167
|
+
setShowReindexDialog(null)
|
|
168
|
+
setReindexing(action)
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const response = await fetch('/api/search/reindex', {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({ action, useQueue: action === 'reindex' }),
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const body = await response.json() as ReindexResponse
|
|
178
|
+
|
|
179
|
+
if (!response.ok || body.error) {
|
|
180
|
+
throw new Error(body.error || t('search.settings.reindexErrorLabel', 'Failed to reindex'))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (body.stats) {
|
|
184
|
+
onStatsUpdate(body.stats)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const successLabel = t('search.settings.reindexSuccessLabel', 'Operation completed successfully')
|
|
188
|
+
const successMessage = action === 'reindex' && body.result
|
|
189
|
+
? `${successLabel}: ${body.result.recordsIndexed} documents indexed`
|
|
190
|
+
: successLabel
|
|
191
|
+
|
|
192
|
+
flash(successMessage, 'success')
|
|
193
|
+
await onRefresh()
|
|
194
|
+
await fetchActivityLogs()
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const message = normalizeErrorMessage(err, t('search.settings.reindexErrorLabel', 'Failed to reindex'))
|
|
197
|
+
flash(message, 'error')
|
|
198
|
+
} finally {
|
|
199
|
+
setReindexing(null)
|
|
200
|
+
}
|
|
201
|
+
}, [showReindexDialog, t, onStatsUpdate, onRefresh, fetchActivityLogs])
|
|
202
|
+
|
|
203
|
+
const getDialogContent = (action: ReindexAction) => {
|
|
204
|
+
switch (action) {
|
|
205
|
+
case 'clear':
|
|
206
|
+
return {
|
|
207
|
+
title: t('search.settings.clearIndexDialogTitle', 'Clear Index'),
|
|
208
|
+
description: t('search.settings.clearIndexDialogDescription', 'This will remove all documents from the Meilisearch index but keep the index settings.'),
|
|
209
|
+
warning: t('search.settings.clearIndexDialogWarning', 'Search will not work until documents are re-indexed.'),
|
|
210
|
+
confirmLabel: t('search.settings.clearIndexLabel', 'Clear Index'),
|
|
211
|
+
}
|
|
212
|
+
case 'recreate':
|
|
213
|
+
return {
|
|
214
|
+
title: t('search.settings.recreateIndexDialogTitle', 'Recreate Index'),
|
|
215
|
+
description: t('search.settings.recreateIndexDialogDescription', 'This will delete the index completely and recreate it with fresh settings.'),
|
|
216
|
+
warning: t('search.settings.recreateIndexDialogWarning', 'All indexed documents will be permanently removed.'),
|
|
217
|
+
confirmLabel: t('search.settings.recreateIndexLabel', 'Recreate Index'),
|
|
218
|
+
}
|
|
219
|
+
case 'reindex':
|
|
220
|
+
return {
|
|
221
|
+
title: t('search.settings.fullReindexDialogTitle', 'Full Reindex'),
|
|
222
|
+
description: t('search.settings.fullReindexDialogDescription', 'This will recreate the index and re-index all data from the database.'),
|
|
223
|
+
warning: t('search.settings.fullReindexDialogWarning', 'This operation may take a while depending on the amount of data.'),
|
|
224
|
+
confirmLabel: t('search.settings.fullReindexLabel', 'Full Reindex'),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const getStrategyIcon = () => (
|
|
230
|
+
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
231
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
|
232
|
+
</svg>
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div className="rounded-lg border border-border bg-card p-5 shadow-sm">
|
|
237
|
+
<h2 className="text-lg font-semibold mb-2">
|
|
238
|
+
{t('search.settings.fulltext.sectionTitle', 'Full-Text Search')}
|
|
239
|
+
</h2>
|
|
240
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
241
|
+
{t('search.settings.fulltext.sectionDescription', 'Fast, typo-tolerant search using Meilisearch.')}
|
|
242
|
+
</p>
|
|
243
|
+
|
|
244
|
+
<Tabs defaultValue="configuration">
|
|
245
|
+
<TabsList className="mb-4">
|
|
246
|
+
<TabsTrigger value="configuration">
|
|
247
|
+
{t('search.settings.tabs.configuration', 'Configuration')}
|
|
248
|
+
</TabsTrigger>
|
|
249
|
+
<TabsTrigger value="index">
|
|
250
|
+
{t('search.settings.tabs.indexManagement', 'Index Management')}
|
|
251
|
+
</TabsTrigger>
|
|
252
|
+
<TabsTrigger value="activity">
|
|
253
|
+
{t('search.settings.tabs.activity', 'Activity')}
|
|
254
|
+
</TabsTrigger>
|
|
255
|
+
</TabsList>
|
|
256
|
+
|
|
257
|
+
{/* Configuration Tab */}
|
|
258
|
+
<TabsContent value="configuration">
|
|
259
|
+
{fulltextConfigLoading ? (
|
|
260
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
261
|
+
<Spinner size="sm" />
|
|
262
|
+
<span>{t('search.settings.loadingLabel', 'Loading settings...')}</span>
|
|
263
|
+
</div>
|
|
264
|
+
) : (
|
|
265
|
+
<div className="space-y-4">
|
|
266
|
+
{/* Driver Status */}
|
|
267
|
+
<div className="flex items-center gap-3 p-3 rounded-md border border-border bg-muted/30">
|
|
268
|
+
<div className={`flex h-10 w-10 items-center justify-center rounded-full ${
|
|
269
|
+
fulltextConfig?.configured
|
|
270
|
+
? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/40 dark:text-emerald-400'
|
|
271
|
+
: 'bg-amber-100 text-amber-600 dark:bg-amber-900/40 dark:text-amber-400'
|
|
272
|
+
}`}>
|
|
273
|
+
{getStrategyIcon()}
|
|
274
|
+
</div>
|
|
275
|
+
<div>
|
|
276
|
+
<p className="font-medium">
|
|
277
|
+
{t('search.settings.fulltext.driver', 'Current Driver')}: {fulltextConfig?.driver ? 'Meilisearch' : t('search.settings.fulltext.noDriver', 'None')}
|
|
278
|
+
</p>
|
|
279
|
+
<p className={`text-sm ${
|
|
280
|
+
fulltextConfig?.configured
|
|
281
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
282
|
+
: 'text-amber-600 dark:text-amber-400'
|
|
283
|
+
}`}>
|
|
284
|
+
{fulltextConfig?.configured
|
|
285
|
+
? t('search.settings.fulltext.ready', 'Ready to use')
|
|
286
|
+
: t('search.settings.fulltext.notReady', 'Not configured - set environment variables below')}
|
|
287
|
+
</p>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Required Environment Variables */}
|
|
292
|
+
<div>
|
|
293
|
+
<h3 className="text-sm font-semibold mb-2">
|
|
294
|
+
{t('search.settings.fulltext.envVars', 'Required Environment Variables')}
|
|
295
|
+
</h3>
|
|
296
|
+
<div className="space-y-2">
|
|
297
|
+
{fulltextConfig?.envVars && Object.entries(fulltextConfig.envVars).map(([key, status]) => (
|
|
298
|
+
<div key={key} className="flex items-start gap-3 p-3 rounded-md border border-border">
|
|
299
|
+
<div className={`flex h-5 w-5 items-center justify-center rounded-full flex-shrink-0 mt-0.5 ${
|
|
300
|
+
status.set
|
|
301
|
+
? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/40 dark:text-emerald-400'
|
|
302
|
+
: 'bg-red-100 text-red-600 dark:bg-red-900/40 dark:text-red-400'
|
|
303
|
+
}`}>
|
|
304
|
+
{status.set ? (
|
|
305
|
+
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
306
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
307
|
+
</svg>
|
|
308
|
+
) : (
|
|
309
|
+
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
310
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
311
|
+
</svg>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
<div className="flex-1">
|
|
315
|
+
<div className="flex items-center gap-2">
|
|
316
|
+
<code className="text-sm font-mono bg-muted px-1.5 py-0.5 rounded">{key}</code>
|
|
317
|
+
<span className={`text-xs ${status.set ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'}`}>
|
|
318
|
+
{status.set ? t('search.settings.fulltext.envSet', 'Set') : t('search.settings.fulltext.envMissing', 'Missing')}
|
|
319
|
+
</span>
|
|
320
|
+
</div>
|
|
321
|
+
<p className="text-xs text-muted-foreground mt-1">{status.hint}</p>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
))}
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
{/* Optional Settings */}
|
|
329
|
+
<div>
|
|
330
|
+
<h3 className="text-sm font-semibold mb-2">
|
|
331
|
+
{t('search.settings.fulltext.optional', 'Optional Settings')}
|
|
332
|
+
</h3>
|
|
333
|
+
<div className="space-y-2">
|
|
334
|
+
{fulltextConfig?.optionalEnvVars && Object.entries(fulltextConfig.optionalEnvVars).map(([key, status]) => (
|
|
335
|
+
<div key={key} className="flex items-start gap-3 p-2 rounded-md bg-muted/30">
|
|
336
|
+
<code className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded">{key}</code>
|
|
337
|
+
<div className="flex-1 text-xs text-muted-foreground">
|
|
338
|
+
<span>{status.hint}</span>
|
|
339
|
+
{status.set ? (
|
|
340
|
+
<span className="ml-2 text-emerald-600 dark:text-emerald-400">
|
|
341
|
+
({t('search.settings.fulltext.currentValue', 'Current')}: {String(status.value)})
|
|
342
|
+
</span>
|
|
343
|
+
) : (
|
|
344
|
+
<span className="ml-2">
|
|
345
|
+
({t('search.settings.fulltext.defaultValue', 'Default')}: {String(status.default)})
|
|
346
|
+
</span>
|
|
347
|
+
)}
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
))}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
{/* Setup Instructions */}
|
|
355
|
+
<div className="p-3 rounded-md bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
|
|
356
|
+
<div className="flex items-start gap-2">
|
|
357
|
+
<svg className="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
358
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
359
|
+
</svg>
|
|
360
|
+
<div className="text-sm text-blue-800 dark:text-blue-200">
|
|
361
|
+
<p className="font-medium mb-1">{t('search.settings.fulltext.howTo', 'How to set up')}</p>
|
|
362
|
+
<p className="text-xs">{t('search.settings.fulltext.howToDescription', 'Add these variables to your .env file or deployment environment. You can use a hosted Meilisearch instance or run it locally with Docker.')}</p>
|
|
363
|
+
<a
|
|
364
|
+
href="https://www.meilisearch.com/docs/learn/getting_started/quick_start"
|
|
365
|
+
target="_blank"
|
|
366
|
+
rel="noopener noreferrer"
|
|
367
|
+
className="text-xs text-blue-600 dark:text-blue-400 hover:underline mt-1 inline-block"
|
|
368
|
+
>
|
|
369
|
+
{t('search.settings.fulltext.learnMore', 'Learn more: Meilisearch Quick Start')} →
|
|
370
|
+
</a>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
</TabsContent>
|
|
377
|
+
|
|
378
|
+
{/* Index Management Tab */}
|
|
379
|
+
<TabsContent value="index">
|
|
380
|
+
{(loading || fulltextConfigLoading) ? (
|
|
381
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
382
|
+
<Spinner size="sm" />
|
|
383
|
+
<span>{t('search.settings.loadingLabel', 'Loading settings...')}</span>
|
|
384
|
+
</div>
|
|
385
|
+
) : !fulltextConfig?.configured ? (
|
|
386
|
+
<div className="p-4 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800">
|
|
387
|
+
<div className="flex items-start gap-3">
|
|
388
|
+
<svg className="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
389
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
390
|
+
</svg>
|
|
391
|
+
<div>
|
|
392
|
+
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
|
393
|
+
{t('search.settings.fulltextNotConfigured', 'Full-text search driver not configured')}
|
|
394
|
+
</p>
|
|
395
|
+
<p className="text-xs text-amber-700 dark:text-amber-300 mt-1">
|
|
396
|
+
{t('search.settings.fulltextNotConfiguredHint', 'Configure the required environment variables in the Configuration tab to enable indexing.')}
|
|
397
|
+
</p>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
) : (
|
|
402
|
+
<div className="space-y-4">
|
|
403
|
+
{/* Stats */}
|
|
404
|
+
{fulltextStats ? (
|
|
405
|
+
<div className="rounded-md border border-border p-4 max-w-xs">
|
|
406
|
+
<p className="text-sm text-muted-foreground">{t('search.settings.documentsLabel', 'Documents')}</p>
|
|
407
|
+
<p className="text-2xl font-bold">{fulltextStats.numberOfDocuments.toLocaleString()}</p>
|
|
408
|
+
</div>
|
|
409
|
+
) : (
|
|
410
|
+
<div className="p-3 rounded-md bg-muted/50">
|
|
411
|
+
<p className="text-sm text-muted-foreground">
|
|
412
|
+
{t('search.settings.noIndexMessage', "No index found for this tenant. Click 'Full Reindex' to create one.")}
|
|
413
|
+
</p>
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
416
|
+
|
|
417
|
+
{/* Active reindex lock banner */}
|
|
418
|
+
{fulltextReindexLock && (
|
|
419
|
+
<div className="p-4 rounded-md bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
|
|
420
|
+
<div className="flex items-start gap-3">
|
|
421
|
+
<Spinner size="sm" className="flex-shrink-0 mt-0.5 text-blue-600 dark:text-blue-400" />
|
|
422
|
+
<div className="flex-1">
|
|
423
|
+
<p className="text-sm font-medium text-blue-800 dark:text-blue-200">
|
|
424
|
+
{t('search.settings.reindexInProgress', 'Reindex operation in progress')}
|
|
425
|
+
</p>
|
|
426
|
+
<p className="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
|
427
|
+
{t('search.settings.reindexInProgressDetails', 'Action: {{action}} | Started {{minutes}} minutes ago', {
|
|
428
|
+
action: fulltextReindexLock.action,
|
|
429
|
+
minutes: fulltextReindexLock.elapsedMinutes,
|
|
430
|
+
})}
|
|
431
|
+
</p>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
)}
|
|
436
|
+
|
|
437
|
+
{/* Actions */}
|
|
438
|
+
<div className="flex flex-wrap gap-3 pt-2">
|
|
439
|
+
{fulltextStats && (
|
|
440
|
+
<>
|
|
441
|
+
<div className="flex flex-col">
|
|
442
|
+
<Button
|
|
443
|
+
type="button"
|
|
444
|
+
variant="outline"
|
|
445
|
+
size="sm"
|
|
446
|
+
onClick={() => handleReindexClick('clear')}
|
|
447
|
+
disabled={reindexing !== null || fulltextReindexLock !== null}
|
|
448
|
+
>
|
|
449
|
+
{reindexing === 'clear' ? (
|
|
450
|
+
<>
|
|
451
|
+
<Spinner size="sm" className="mr-2" />
|
|
452
|
+
{t('search.settings.processingLabel', 'Processing...')}
|
|
453
|
+
</>
|
|
454
|
+
) : (
|
|
455
|
+
t('search.settings.clearIndexLabel', 'Clear Index')
|
|
456
|
+
)}
|
|
457
|
+
</Button>
|
|
458
|
+
<span className="text-xs text-muted-foreground mt-1">
|
|
459
|
+
{t('search.settings.clearIndexDescription', 'Remove all documents but keep index settings')}
|
|
460
|
+
</span>
|
|
461
|
+
</div>
|
|
462
|
+
<div className="flex flex-col">
|
|
463
|
+
<Button
|
|
464
|
+
type="button"
|
|
465
|
+
variant="outline"
|
|
466
|
+
size="sm"
|
|
467
|
+
onClick={() => handleReindexClick('recreate')}
|
|
468
|
+
disabled={reindexing !== null || fulltextReindexLock !== null}
|
|
469
|
+
>
|
|
470
|
+
{reindexing === 'recreate' ? (
|
|
471
|
+
<>
|
|
472
|
+
<Spinner size="sm" className="mr-2" />
|
|
473
|
+
{t('search.settings.processingLabel', 'Processing...')}
|
|
474
|
+
</>
|
|
475
|
+
) : (
|
|
476
|
+
t('search.settings.recreateIndexLabel', 'Recreate Index')
|
|
477
|
+
)}
|
|
478
|
+
</Button>
|
|
479
|
+
<span className="text-xs text-muted-foreground mt-1">
|
|
480
|
+
{t('search.settings.recreateIndexDescription', 'Delete and recreate the index with fresh settings')}
|
|
481
|
+
</span>
|
|
482
|
+
</div>
|
|
483
|
+
</>
|
|
484
|
+
)}
|
|
485
|
+
<div className="flex flex-col">
|
|
486
|
+
<Button
|
|
487
|
+
type="button"
|
|
488
|
+
variant="default"
|
|
489
|
+
size="sm"
|
|
490
|
+
onClick={() => handleReindexClick('reindex')}
|
|
491
|
+
disabled={reindexing !== null || fulltextReindexLock !== null}
|
|
492
|
+
>
|
|
493
|
+
{reindexing === 'reindex' || fulltextReindexLock !== null ? (
|
|
494
|
+
<>
|
|
495
|
+
<Spinner size="sm" className="mr-2" />
|
|
496
|
+
{t('search.settings.processingLabel', 'Processing...')}
|
|
497
|
+
</>
|
|
498
|
+
) : (
|
|
499
|
+
t('search.settings.fullReindexLabel', 'Full Reindex')
|
|
500
|
+
)}
|
|
501
|
+
</Button>
|
|
502
|
+
<span className="text-xs text-muted-foreground mt-1">
|
|
503
|
+
{t('search.settings.fullReindexDescription', 'Recreate index and re-index all data from database')}
|
|
504
|
+
</span>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
)}
|
|
509
|
+
</TabsContent>
|
|
510
|
+
|
|
511
|
+
{/* Activity Tab */}
|
|
512
|
+
<TabsContent value="activity">
|
|
513
|
+
{activityLoading ? (
|
|
514
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
515
|
+
<Spinner size="sm" />
|
|
516
|
+
<span>{t('search.settings.loadingLabel', 'Loading...')}</span>
|
|
517
|
+
</div>
|
|
518
|
+
) : activityLogs.length === 0 ? (
|
|
519
|
+
<div className="p-4 rounded-md bg-muted/50 text-center">
|
|
520
|
+
<p className="text-sm text-muted-foreground">
|
|
521
|
+
{t('search.settings.activity.noLogs', 'No recent indexing activity')}
|
|
522
|
+
</p>
|
|
523
|
+
</div>
|
|
524
|
+
) : (
|
|
525
|
+
<div className="space-y-2 max-h-80 overflow-y-auto">
|
|
526
|
+
{activityLogs.map((log) => (
|
|
527
|
+
<div
|
|
528
|
+
key={log.id}
|
|
529
|
+
className={`p-2 rounded-md text-sm ${
|
|
530
|
+
log.level === 'error'
|
|
531
|
+
? 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
|
|
532
|
+
: 'bg-muted/50'
|
|
533
|
+
}`}
|
|
534
|
+
>
|
|
535
|
+
<div className="flex items-start gap-2">
|
|
536
|
+
{log.level === 'error' && (
|
|
537
|
+
<svg className="h-4 w-4 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
538
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
539
|
+
</svg>
|
|
540
|
+
)}
|
|
541
|
+
<div className="flex-1 min-w-0">
|
|
542
|
+
<p className={`text-xs ${log.level === 'error' ? 'text-red-800 dark:text-red-200' : 'text-foreground'}`}>
|
|
543
|
+
{log.message}
|
|
544
|
+
</p>
|
|
545
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
546
|
+
{(() => {
|
|
547
|
+
const d = new Date(log.occurredAt)
|
|
548
|
+
const pad = (n: number) => n.toString().padStart(2, '0')
|
|
549
|
+
return `${pad(d.getDate())}-${pad(d.getMonth() + 1)}-${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
550
|
+
})()}
|
|
551
|
+
{log.entityType && ` · ${log.entityType}`}
|
|
552
|
+
</p>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
))}
|
|
557
|
+
</div>
|
|
558
|
+
)}
|
|
559
|
+
<div className="mt-3">
|
|
560
|
+
<Button
|
|
561
|
+
type="button"
|
|
562
|
+
variant="outline"
|
|
563
|
+
size="sm"
|
|
564
|
+
onClick={fetchActivityLogs}
|
|
565
|
+
disabled={activityLoading}
|
|
566
|
+
>
|
|
567
|
+
{activityLoading ? (
|
|
568
|
+
<>
|
|
569
|
+
<Spinner size="sm" className="mr-2" />
|
|
570
|
+
{t('search.settings.loadingLabel', 'Loading...')}
|
|
571
|
+
</>
|
|
572
|
+
) : (
|
|
573
|
+
t('search.settings.refreshLabel', 'Refresh')
|
|
574
|
+
)}
|
|
575
|
+
</Button>
|
|
576
|
+
</div>
|
|
577
|
+
</TabsContent>
|
|
578
|
+
</Tabs>
|
|
579
|
+
|
|
580
|
+
{/* Reindex Confirmation Dialog */}
|
|
581
|
+
{showReindexDialog && (
|
|
582
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
583
|
+
<div className="mx-4 max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
|
584
|
+
<div className="flex items-start gap-3 mb-4">
|
|
585
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/40">
|
|
586
|
+
<svg className="h-5 w-5 text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
587
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
588
|
+
</svg>
|
|
589
|
+
</div>
|
|
590
|
+
<div>
|
|
591
|
+
<h3 className="text-lg font-semibold">{getDialogContent(showReindexDialog).title}</h3>
|
|
592
|
+
<p className="text-sm text-muted-foreground mt-1">{getDialogContent(showReindexDialog).description}</p>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
<div className="mb-4 p-3 rounded-md bg-amber-50 dark:bg-amber-900/20">
|
|
597
|
+
<div className="flex items-start gap-2">
|
|
598
|
+
<svg className="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
599
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
600
|
+
</svg>
|
|
601
|
+
<p className="text-sm text-amber-800 dark:text-amber-200">{getDialogContent(showReindexDialog).warning}</p>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
|
|
605
|
+
<div className="flex justify-end gap-3">
|
|
606
|
+
<Button type="button" variant="outline" onClick={handleReindexCancel}>
|
|
607
|
+
{t('search.settings.cancelLabel', 'Cancel')}
|
|
608
|
+
</Button>
|
|
609
|
+
<Button
|
|
610
|
+
type="button"
|
|
611
|
+
variant={showReindexDialog === 'reindex' ? 'default' : 'destructive'}
|
|
612
|
+
onClick={handleReindexConfirm}
|
|
613
|
+
>
|
|
614
|
+
{getDialogContent(showReindexDialog).confirmLabel}
|
|
615
|
+
</Button>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
)}
|
|
620
|
+
</div>
|
|
621
|
+
)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export default FulltextSearchSection
|