mangleframes 0.3.2__tar.gz → 0.3.3__tar.gz
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.
- {mangleframes-0.3.2 → mangleframes-0.3.3}/PKG-INFO +1 -1
- {mangleframes-0.3.2 → mangleframes-0.3.3}/pyproject.toml +1 -1
- {mangleframes-0.3.2 → mangleframes-0.3.3}/python/mangleframes/__init__.py +1 -1
- mangleframes-0.3.3/viewer/frontend/src/components/analysis/JoinAnalyzer.tsx +442 -0
- mangleframes-0.3.3/viewer/frontend/src/components/analysis/__tests__/JoinAnalyzer.test.tsx +307 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/lib/api.ts +91 -0
- mangleframes-0.3.3/viewer/src/history_handlers.rs +513 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/join_handlers.rs +268 -44
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/reconcile_handlers.rs +86 -98
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/sql_builder.rs +211 -42
- mangleframes-0.3.2/viewer/frontend/src/components/analysis/JoinAnalyzer.tsx +0 -165
- mangleframes-0.3.2/viewer/src/history_handlers.rs +0 -341
- {mangleframes-0.3.2 → mangleframes-0.3.3}/Cargo.lock +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/Cargo.toml +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/python/mangleframes/alerts.py +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/python/mangleframes/launcher.py +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/python/mangleframes/session.py +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/Cargo.toml +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/build.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/proto/spark/connect/base.proto +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/proto/spark/connect/catalog.proto +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/proto/spark/connect/commands.proto +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/proto/spark/connect/common.proto +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/proto/spark/connect/expressions.proto +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/proto/spark/connect/ml.proto +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/proto/spark/connect/ml_common.proto +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/proto/spark/connect/relations.proto +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/proto/spark/connect/types.proto +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/src/client.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/src/error.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/src/lib.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/spark-connect/src/proto/spark.connect.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/Cargo.toml +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/index.html +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/package-lock.json +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/package.json +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/postcss.config.js +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/App.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/analysis/Reconciliation.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/analysis/SQLEditor.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/data/ColumnDropdown.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/data/ColumnStats.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/data/DataGrid.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/data/SchemaView.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/erd/ERDBuilder.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/erd/ERDCanvas.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/erd/ERDConfigModal.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/erd/ERDTableList.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/erd/ERDToolbar.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/erd/ERDValidationPanel.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/erd/TableNode.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/erd/__tests__/ERDDragDrop.test.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/erd/index.ts +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/layout/ContextPanel.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/layout/Layout.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/layout/MainContent.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/layout/Sidebar.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/layout/StatusBar.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/layout/TabBar.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/layout/TopBar.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/quality/AlertBuilder.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/components/quality/QualityDashboard.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/index.css +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/lib/erdValidation.ts +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/main.tsx +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/stores/dataStore.ts +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/stores/erdStore.ts +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/stores/uiStore.ts +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/src/test/setup.ts +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/tailwind.config.js +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/tsconfig.json +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/tsconfig.node.json +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/frontend/vite.config.ts +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/alert_handlers.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/arrow_reader.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/dashboard.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/export.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/handlers.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/history_analysis.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/main.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/perf.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/spark_client.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/stats.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/test_helpers.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/web_server.rs +0 -0
- {mangleframes-0.3.2 → mangleframes-0.3.3}/viewer/src/websocket.rs +0 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useMutation } from '@tanstack/react-query'
|
|
3
|
+
import { useDataStore } from '@/stores/dataStore'
|
|
4
|
+
import { Play, Loader2, Calendar } from 'lucide-react'
|
|
5
|
+
import { api, CoverageResult } from '@/lib/api'
|
|
6
|
+
|
|
7
|
+
interface JoinStatistics {
|
|
8
|
+
left_total: number
|
|
9
|
+
right_total: number
|
|
10
|
+
matched_left: number
|
|
11
|
+
matched_right: number
|
|
12
|
+
match_rate_left: number
|
|
13
|
+
match_rate_right: number
|
|
14
|
+
cardinality: string
|
|
15
|
+
left_null_keys: number
|
|
16
|
+
right_null_keys: number
|
|
17
|
+
left_duplicate_keys: number
|
|
18
|
+
right_duplicate_keys: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface UnmatchedData {
|
|
22
|
+
rows: Record<string, unknown>[]
|
|
23
|
+
total: number
|
|
24
|
+
columns_limited: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface JoinResult {
|
|
28
|
+
statistics: JoinStatistics
|
|
29
|
+
left_unmatched: UnmatchedData
|
|
30
|
+
right_unmatched: UnmatchedData
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function JoinAnalyzer() {
|
|
34
|
+
const frames = useDataStore((s) => s.frames)
|
|
35
|
+
const [leftTable, setLeftTable] = useState('')
|
|
36
|
+
const [rightTable, setRightTable] = useState('')
|
|
37
|
+
const [leftKey, setLeftKey] = useState('')
|
|
38
|
+
const [rightKey, setRightKey] = useState('')
|
|
39
|
+
const [result, setResult] = useState<JoinResult | null>(null)
|
|
40
|
+
const [bucketSize, setBucketSize] = useState<'day' | 'week' | 'month'>('month')
|
|
41
|
+
const [coverageResult, setCoverageResult] = useState<CoverageResult | null>(null)
|
|
42
|
+
|
|
43
|
+
const mutation = useMutation({
|
|
44
|
+
mutationFn: async () => {
|
|
45
|
+
const response = await fetch('/api/join/analyze', {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
left_table: leftTable,
|
|
50
|
+
right_table: rightTable,
|
|
51
|
+
left_keys: [leftKey],
|
|
52
|
+
right_keys: [rightKey],
|
|
53
|
+
}),
|
|
54
|
+
})
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const error = await response.json()
|
|
57
|
+
throw new Error(error.error || 'Analysis failed')
|
|
58
|
+
}
|
|
59
|
+
return response.json()
|
|
60
|
+
},
|
|
61
|
+
onSuccess: (data) => setResult(data),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const coverageMutation = useMutation({
|
|
65
|
+
mutationFn: async () => {
|
|
66
|
+
console.log('[JoinAnalyzer] Analyzing date coverage...')
|
|
67
|
+
return api.analyzeHistory({
|
|
68
|
+
frames: [
|
|
69
|
+
{ frame: leftTable, columns: [leftKey] },
|
|
70
|
+
{ frame: rightTable, columns: [rightKey] },
|
|
71
|
+
],
|
|
72
|
+
join_pairs: [{
|
|
73
|
+
source_frame: leftTable,
|
|
74
|
+
target_frame: rightTable,
|
|
75
|
+
source_keys: [leftKey],
|
|
76
|
+
target_keys: [rightKey],
|
|
77
|
+
}],
|
|
78
|
+
bucket_size: bucketSize,
|
|
79
|
+
})
|
|
80
|
+
},
|
|
81
|
+
onSuccess: (data) => {
|
|
82
|
+
console.log('[JoinAnalyzer] Coverage analysis complete')
|
|
83
|
+
setCoverageResult(data)
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const handleAnalyze = () => {
|
|
88
|
+
if (leftTable && rightTable && leftKey && rightKey) {
|
|
89
|
+
mutation.mutate()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="p-3">
|
|
95
|
+
<h3 className="text-sm font-semibold text-mf-text mb-4">Join Analysis</h3>
|
|
96
|
+
|
|
97
|
+
<div className="space-y-3">
|
|
98
|
+
<div>
|
|
99
|
+
<label className="block text-xs text-mf-muted mb-1">Left Table</label>
|
|
100
|
+
<select
|
|
101
|
+
value={leftTable}
|
|
102
|
+
onChange={(e) => setLeftTable(e.target.value)}
|
|
103
|
+
className="w-full px-2 py-1 text-xs bg-mf-bg border border-mf-border rounded text-mf-text"
|
|
104
|
+
>
|
|
105
|
+
<option value="">Select table...</option>
|
|
106
|
+
{frames.map((f) => (
|
|
107
|
+
<option key={f} value={f}>{f}</option>
|
|
108
|
+
))}
|
|
109
|
+
</select>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div>
|
|
113
|
+
<label className="block text-xs text-mf-muted mb-1">Left Key</label>
|
|
114
|
+
<input
|
|
115
|
+
type="text"
|
|
116
|
+
value={leftKey}
|
|
117
|
+
onChange={(e) => setLeftKey(e.target.value)}
|
|
118
|
+
placeholder="Column name"
|
|
119
|
+
className="w-full px-2 py-1 text-xs bg-mf-bg border border-mf-border rounded text-mf-text"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div>
|
|
124
|
+
<label className="block text-xs text-mf-muted mb-1">Right Table</label>
|
|
125
|
+
<select
|
|
126
|
+
value={rightTable}
|
|
127
|
+
onChange={(e) => setRightTable(e.target.value)}
|
|
128
|
+
className="w-full px-2 py-1 text-xs bg-mf-bg border border-mf-border rounded text-mf-text"
|
|
129
|
+
>
|
|
130
|
+
<option value="">Select table...</option>
|
|
131
|
+
{frames.map((f) => (
|
|
132
|
+
<option key={f} value={f}>{f}</option>
|
|
133
|
+
))}
|
|
134
|
+
</select>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div>
|
|
138
|
+
<label className="block text-xs text-mf-muted mb-1">Right Key</label>
|
|
139
|
+
<input
|
|
140
|
+
type="text"
|
|
141
|
+
value={rightKey}
|
|
142
|
+
onChange={(e) => setRightKey(e.target.value)}
|
|
143
|
+
placeholder="Column name"
|
|
144
|
+
className="w-full px-2 py-1 text-xs bg-mf-bg border border-mf-border rounded text-mf-text"
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<button
|
|
149
|
+
onClick={handleAnalyze}
|
|
150
|
+
disabled={mutation.isPending || !leftTable || !rightTable || !leftKey || !rightKey}
|
|
151
|
+
className="w-full flex items-center justify-center gap-2 px-3 py-2 text-xs bg-mf-accent hover:bg-mf-accent/80 text-white rounded disabled:opacity-50"
|
|
152
|
+
>
|
|
153
|
+
{mutation.isPending ? (
|
|
154
|
+
<Loader2 size={12} className="animate-spin" />
|
|
155
|
+
) : (
|
|
156
|
+
<Play size={12} />
|
|
157
|
+
)}
|
|
158
|
+
Analyze Join
|
|
159
|
+
</button>
|
|
160
|
+
|
|
161
|
+
{mutation.isError && (
|
|
162
|
+
<div className="p-2 text-xs text-red-400 bg-red-500/10 rounded">
|
|
163
|
+
{mutation.error instanceof Error ? mutation.error.message : 'Analysis failed'}
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{result && (
|
|
168
|
+
<div className="mt-4 p-3 bg-mf-panel rounded border border-mf-border">
|
|
169
|
+
<div className="text-xs font-medium text-mf-text mb-3">Results</div>
|
|
170
|
+
<div className="space-y-2 text-xs">
|
|
171
|
+
<div className="flex justify-between">
|
|
172
|
+
<span className="text-mf-muted">Left rows:</span>
|
|
173
|
+
<span className="text-mf-text">{result.statistics.left_total.toLocaleString()}</span>
|
|
174
|
+
</div>
|
|
175
|
+
<div className="flex justify-between">
|
|
176
|
+
<span className="text-mf-muted">Right rows:</span>
|
|
177
|
+
<span className="text-mf-text">{result.statistics.right_total.toLocaleString()}</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex justify-between">
|
|
180
|
+
<span className="text-mf-muted">Matched (left):</span>
|
|
181
|
+
<span className="text-green-400">{result.statistics.matched_left.toLocaleString()}</span>
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flex justify-between">
|
|
184
|
+
<span className="text-mf-muted">Matched (right):</span>
|
|
185
|
+
<span className="text-green-400">{result.statistics.matched_right.toLocaleString()}</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="flex justify-between">
|
|
188
|
+
<span className="text-mf-muted">Left only:</span>
|
|
189
|
+
<span className="text-yellow-400">{result.left_unmatched.total.toLocaleString()}</span>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="flex justify-between">
|
|
192
|
+
<span className="text-mf-muted">Right only:</span>
|
|
193
|
+
<span className="text-yellow-400">{result.right_unmatched.total.toLocaleString()}</span>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="border-t border-mf-border pt-2 mt-2">
|
|
196
|
+
<div className="flex justify-between">
|
|
197
|
+
<span className="text-mf-muted">Cardinality:</span>
|
|
198
|
+
<span className="text-mf-accent font-mono">{result.statistics.cardinality}</span>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="flex justify-between">
|
|
201
|
+
<span className="text-mf-muted">Left match rate:</span>
|
|
202
|
+
<span className="text-mf-accent">{(result.statistics.match_rate_left * 100).toFixed(1)}%</span>
|
|
203
|
+
</div>
|
|
204
|
+
<div className="flex justify-between">
|
|
205
|
+
<span className="text-mf-muted">Right match rate:</span>
|
|
206
|
+
<span className="text-mf-accent">{(result.statistics.match_rate_right * 100).toFixed(1)}%</span>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
{(result.statistics.left_null_keys > 0 || result.statistics.right_null_keys > 0 ||
|
|
210
|
+
result.statistics.left_duplicate_keys > 0 || result.statistics.right_duplicate_keys > 0) && (
|
|
211
|
+
<div className="border-t border-mf-border pt-2 mt-2">
|
|
212
|
+
<div className="text-xs font-medium text-mf-muted mb-1">Key Quality</div>
|
|
213
|
+
{result.statistics.left_null_keys > 0 && (
|
|
214
|
+
<div className="flex justify-between">
|
|
215
|
+
<span className="text-mf-muted">Left null keys:</span>
|
|
216
|
+
<span className="text-red-400">{result.statistics.left_null_keys.toLocaleString()}</span>
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
{result.statistics.right_null_keys > 0 && (
|
|
220
|
+
<div className="flex justify-between">
|
|
221
|
+
<span className="text-mf-muted">Right null keys:</span>
|
|
222
|
+
<span className="text-red-400">{result.statistics.right_null_keys.toLocaleString()}</span>
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
{result.statistics.left_duplicate_keys > 0 && (
|
|
226
|
+
<div className="flex justify-between">
|
|
227
|
+
<span className="text-mf-muted">Left duplicate keys:</span>
|
|
228
|
+
<span className="text-orange-400">{result.statistics.left_duplicate_keys.toLocaleString()}</span>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
{result.statistics.right_duplicate_keys > 0 && (
|
|
232
|
+
<div className="flex justify-between">
|
|
233
|
+
<span className="text-mf-muted">Right duplicate keys:</span>
|
|
234
|
+
<span className="text-orange-400">{result.statistics.right_duplicate_keys.toLocaleString()}</span>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{/* Date Coverage Analysis */}
|
|
244
|
+
<div className="mt-4 pt-4 border-t border-mf-border">
|
|
245
|
+
<h4 className="text-xs font-medium text-mf-text mb-3 flex items-center gap-2">
|
|
246
|
+
<Calendar size={12} />
|
|
247
|
+
Date Coverage
|
|
248
|
+
</h4>
|
|
249
|
+
|
|
250
|
+
<div className="flex gap-1 mb-3">
|
|
251
|
+
{(['day', 'week', 'month'] as const).map((size) => (
|
|
252
|
+
<button
|
|
253
|
+
key={size}
|
|
254
|
+
onClick={() => setBucketSize(size)}
|
|
255
|
+
className={`px-2 py-1 text-xs rounded ${
|
|
256
|
+
bucketSize === size
|
|
257
|
+
? 'bg-mf-accent text-white'
|
|
258
|
+
: 'bg-mf-panel text-mf-muted hover:text-mf-text'
|
|
259
|
+
}`}
|
|
260
|
+
>
|
|
261
|
+
{size.charAt(0).toUpperCase() + size.slice(1)}
|
|
262
|
+
</button>
|
|
263
|
+
))}
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<button
|
|
267
|
+
onClick={() => coverageMutation.mutate()}
|
|
268
|
+
disabled={coverageMutation.isPending || !leftTable || !rightTable || !leftKey || !rightKey}
|
|
269
|
+
className="w-full flex items-center justify-center gap-2 px-3 py-2 text-xs bg-mf-panel hover:bg-mf-border text-mf-text rounded disabled:opacity-50"
|
|
270
|
+
>
|
|
271
|
+
{coverageMutation.isPending ? (
|
|
272
|
+
<Loader2 size={12} className="animate-spin" />
|
|
273
|
+
) : (
|
|
274
|
+
<Calendar size={12} />
|
|
275
|
+
)}
|
|
276
|
+
Analyze Coverage
|
|
277
|
+
</button>
|
|
278
|
+
|
|
279
|
+
{coverageMutation.isError && (
|
|
280
|
+
<div className="mt-2 p-2 text-xs text-red-400 bg-red-500/10 rounded">
|
|
281
|
+
{coverageMutation.error instanceof Error ? coverageMutation.error.message : 'Coverage analysis failed'}
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{coverageResult && (
|
|
286
|
+
<CoverageResults coverage={coverageResult} />
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function CoverageResults({ coverage }: { coverage: CoverageResult }) {
|
|
295
|
+
return (
|
|
296
|
+
<div className="mt-3 space-y-3">
|
|
297
|
+
{/* Overlap Zone Banner */}
|
|
298
|
+
{coverage.overlap_zone && (
|
|
299
|
+
<div className={`p-3 rounded text-xs ${
|
|
300
|
+
coverage.overlap_zone.valid
|
|
301
|
+
? 'bg-green-500/10 border border-green-500/50 text-green-400'
|
|
302
|
+
: 'bg-red-500/10 border border-red-500/50 text-red-400'
|
|
303
|
+
}`}>
|
|
304
|
+
{coverage.overlap_zone.valid
|
|
305
|
+
? `Overlap: ${coverage.overlap_zone.start} → ${coverage.overlap_zone.end} (${coverage.overlap_zone.span})`
|
|
306
|
+
: 'No date overlap - INNER join would return 0 rows'}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{/* Temporal Range Table */}
|
|
311
|
+
{coverage.temporal_ranges.length > 0 && (
|
|
312
|
+
<div className="p-2 bg-mf-panel rounded">
|
|
313
|
+
<div className="text-xs font-medium text-mf-text mb-2">Date Ranges</div>
|
|
314
|
+
<table className="w-full text-xs">
|
|
315
|
+
<thead>
|
|
316
|
+
<tr className="text-mf-muted text-left">
|
|
317
|
+
<th className="pb-1">Frame</th>
|
|
318
|
+
<th className="pb-1">Min</th>
|
|
319
|
+
<th className="pb-1">Max</th>
|
|
320
|
+
<th className="pb-1 text-right">Rows</th>
|
|
321
|
+
<th className="pb-1 text-right">Nulls</th>
|
|
322
|
+
<th className="pb-1 text-right">Gaps</th>
|
|
323
|
+
</tr>
|
|
324
|
+
</thead>
|
|
325
|
+
<tbody className="text-mf-text">
|
|
326
|
+
{coverage.temporal_ranges.map(r => (
|
|
327
|
+
<tr key={`${r.frame}-${r.column}`}>
|
|
328
|
+
<td className="py-0.5 truncate max-w-[80px]" title={r.frame}>
|
|
329
|
+
{r.frame.split('.').pop()}
|
|
330
|
+
</td>
|
|
331
|
+
<td className="py-0.5">{r.min_date || '-'}</td>
|
|
332
|
+
<td className="py-0.5">{r.max_date || '-'}</td>
|
|
333
|
+
<td className="py-0.5 text-right">{r.total_rows.toLocaleString()}</td>
|
|
334
|
+
<td className={`py-0.5 text-right ${r.null_dates > 0 ? 'text-yellow-400' : ''}`}>
|
|
335
|
+
{r.null_dates}
|
|
336
|
+
</td>
|
|
337
|
+
<td className={`py-0.5 text-right ${r.internal_gaps.length > 0 ? 'text-yellow-400' : ''}`}>
|
|
338
|
+
{r.internal_gaps.length}
|
|
339
|
+
</td>
|
|
340
|
+
</tr>
|
|
341
|
+
))}
|
|
342
|
+
</tbody>
|
|
343
|
+
</table>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
{coverage.temporal_ranges.length === 0 && (
|
|
348
|
+
<div className="p-2 text-xs text-mf-muted bg-mf-panel rounded">
|
|
349
|
+
No date columns detected
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
{/* Coverage Timeline */}
|
|
354
|
+
{coverage.timeline.length > 0 && (
|
|
355
|
+
<div className="p-2 bg-mf-panel rounded">
|
|
356
|
+
<div className="text-xs font-medium text-mf-text mb-2">Timeline</div>
|
|
357
|
+
<div className="space-y-1">
|
|
358
|
+
{coverage.frames.map(frame => (
|
|
359
|
+
<div key={frame} className="flex items-center gap-2">
|
|
360
|
+
<span className="text-xs text-mf-muted w-20 truncate" title={frame}>
|
|
361
|
+
{frame.split('.').pop()}
|
|
362
|
+
</span>
|
|
363
|
+
<div className="flex flex-1 gap-px">
|
|
364
|
+
{coverage.timeline.map(bucket => (
|
|
365
|
+
<div
|
|
366
|
+
key={bucket.bucket}
|
|
367
|
+
className={`h-3 flex-1 rounded-sm ${
|
|
368
|
+
bucket.frame_counts[frame] > 0 ? 'bg-mf-accent' : 'bg-mf-border'
|
|
369
|
+
}`}
|
|
370
|
+
title={`${bucket.bucket}: ${bucket.frame_counts[frame] || 0} rows`}
|
|
371
|
+
/>
|
|
372
|
+
))}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
))}
|
|
376
|
+
</div>
|
|
377
|
+
<div className="flex justify-between text-[10px] text-mf-muted mt-1">
|
|
378
|
+
<span>{coverage.timeline[0]?.bucket}</span>
|
|
379
|
+
<span>{coverage.timeline[coverage.timeline.length - 1]?.bucket}</span>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
|
|
384
|
+
{/* Data Loss Section */}
|
|
385
|
+
{coverage.data_loss.filter(d => d.total_lost > 0).length > 0 && (
|
|
386
|
+
<div className="p-2 bg-mf-panel rounded">
|
|
387
|
+
<div className="text-xs font-medium text-mf-text mb-2">Data Loss (INNER Join)</div>
|
|
388
|
+
<div className="space-y-1">
|
|
389
|
+
{coverage.data_loss.filter(d => d.total_lost > 0).map(loss => (
|
|
390
|
+
<div key={loss.frame} className="p-2 bg-red-500/10 rounded text-xs">
|
|
391
|
+
<span className="text-red-400">
|
|
392
|
+
{loss.frame.split('.').pop()}: {loss.total_lost.toLocaleString()} rows ({loss.pct_lost.toFixed(1)}%)
|
|
393
|
+
</span>
|
|
394
|
+
</div>
|
|
395
|
+
))}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
{/* Join Predictions */}
|
|
401
|
+
{coverage.predictions.length > 0 && (
|
|
402
|
+
<div className="p-2 bg-mf-panel rounded">
|
|
403
|
+
<div className="text-xs font-medium text-mf-text mb-2">Join Predictions</div>
|
|
404
|
+
<div className="space-y-1">
|
|
405
|
+
{coverage.predictions.map(pred => (
|
|
406
|
+
<div key={pred.join_type} className="flex justify-between text-xs">
|
|
407
|
+
<span className="text-mf-muted">{pred.join_type}</span>
|
|
408
|
+
<span className="text-mf-text">
|
|
409
|
+
{pred.estimated_rows.toLocaleString()} rows ({pred.coverage_pct.toFixed(1)}%)
|
|
410
|
+
</span>
|
|
411
|
+
</div>
|
|
412
|
+
))}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
416
|
+
|
|
417
|
+
{/* Pairwise Overlaps */}
|
|
418
|
+
{coverage.pairwise_overlaps.length > 0 && (
|
|
419
|
+
<div className="p-2 bg-mf-panel rounded">
|
|
420
|
+
<div className="text-xs font-medium text-mf-text mb-2">Key Overlap</div>
|
|
421
|
+
<div className="space-y-2">
|
|
422
|
+
{coverage.pairwise_overlaps.map(o => (
|
|
423
|
+
<div key={`${o.frame1}-${o.frame2}`} className="text-xs">
|
|
424
|
+
<div className="flex justify-between">
|
|
425
|
+
<span className="text-mf-muted">
|
|
426
|
+
{o.frame1.split('.').pop()} ↔ {o.frame2.split('.').pop()}
|
|
427
|
+
</span>
|
|
428
|
+
<span className="text-mf-accent">{o.overlap_pct.toFixed(1)}% overlap</span>
|
|
429
|
+
</div>
|
|
430
|
+
<div className="flex gap-3 text-mf-muted mt-0.5">
|
|
431
|
+
<span>Common: {o.both.toLocaleString()}</span>
|
|
432
|
+
<span>Left only: {o.left_only.toLocaleString()}</span>
|
|
433
|
+
<span>Right only: {o.right_only.toLocaleString()}</span>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
))}
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
)
|
|
442
|
+
}
|