data_porter 0.1.0

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.
Files changed (159) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/commands/blog-status.md +10 -0
  3. data/.claude/commands/blog.md +109 -0
  4. data/.claude/commands/task-done.md +27 -0
  5. data/.claude/commands/tm/add-dependency.md +58 -0
  6. data/.claude/commands/tm/add-subtask.md +79 -0
  7. data/.claude/commands/tm/add-task.md +81 -0
  8. data/.claude/commands/tm/analyze-complexity.md +124 -0
  9. data/.claude/commands/tm/analyze-project.md +100 -0
  10. data/.claude/commands/tm/auto-implement-tasks.md +100 -0
  11. data/.claude/commands/tm/command-pipeline.md +80 -0
  12. data/.claude/commands/tm/complexity-report.md +120 -0
  13. data/.claude/commands/tm/convert-task-to-subtask.md +74 -0
  14. data/.claude/commands/tm/expand-all-tasks.md +52 -0
  15. data/.claude/commands/tm/expand-task.md +52 -0
  16. data/.claude/commands/tm/fix-dependencies.md +82 -0
  17. data/.claude/commands/tm/help.md +101 -0
  18. data/.claude/commands/tm/init-project-quick.md +49 -0
  19. data/.claude/commands/tm/init-project.md +53 -0
  20. data/.claude/commands/tm/install-taskmaster.md +118 -0
  21. data/.claude/commands/tm/learn.md +106 -0
  22. data/.claude/commands/tm/list-tasks-by-status.md +42 -0
  23. data/.claude/commands/tm/list-tasks-with-subtasks.md +30 -0
  24. data/.claude/commands/tm/list-tasks.md +46 -0
  25. data/.claude/commands/tm/next-task.md +69 -0
  26. data/.claude/commands/tm/parse-prd-with-research.md +51 -0
  27. data/.claude/commands/tm/parse-prd.md +52 -0
  28. data/.claude/commands/tm/project-status.md +67 -0
  29. data/.claude/commands/tm/quick-install-taskmaster.md +23 -0
  30. data/.claude/commands/tm/remove-all-subtasks.md +94 -0
  31. data/.claude/commands/tm/remove-dependency.md +65 -0
  32. data/.claude/commands/tm/remove-subtask.md +87 -0
  33. data/.claude/commands/tm/remove-subtasks.md +89 -0
  34. data/.claude/commands/tm/remove-task.md +110 -0
  35. data/.claude/commands/tm/setup-models.md +52 -0
  36. data/.claude/commands/tm/show-task.md +85 -0
  37. data/.claude/commands/tm/smart-workflow.md +58 -0
  38. data/.claude/commands/tm/sync-readme.md +120 -0
  39. data/.claude/commands/tm/tm-main.md +147 -0
  40. data/.claude/commands/tm/to-cancelled.md +58 -0
  41. data/.claude/commands/tm/to-deferred.md +50 -0
  42. data/.claude/commands/tm/to-done.md +47 -0
  43. data/.claude/commands/tm/to-in-progress.md +39 -0
  44. data/.claude/commands/tm/to-pending.md +35 -0
  45. data/.claude/commands/tm/to-review.md +43 -0
  46. data/.claude/commands/tm/update-single-task.md +122 -0
  47. data/.claude/commands/tm/update-task.md +75 -0
  48. data/.claude/commands/tm/update-tasks-from-id.md +111 -0
  49. data/.claude/commands/tm/validate-dependencies.md +72 -0
  50. data/.claude/commands/tm/view-models.md +52 -0
  51. data/.env.example +12 -0
  52. data/.mcp.json +24 -0
  53. data/.taskmaster/CLAUDE.md +435 -0
  54. data/.taskmaster/config.json +44 -0
  55. data/.taskmaster/docs/prd.txt +2044 -0
  56. data/.taskmaster/state.json +6 -0
  57. data/.taskmaster/tasks/task_001.md +19 -0
  58. data/.taskmaster/tasks/task_002.md +19 -0
  59. data/.taskmaster/tasks/task_003.md +19 -0
  60. data/.taskmaster/tasks/task_004.md +19 -0
  61. data/.taskmaster/tasks/task_005.md +19 -0
  62. data/.taskmaster/tasks/task_006.md +19 -0
  63. data/.taskmaster/tasks/task_007.md +19 -0
  64. data/.taskmaster/tasks/task_008.md +19 -0
  65. data/.taskmaster/tasks/task_009.md +19 -0
  66. data/.taskmaster/tasks/task_010.md +19 -0
  67. data/.taskmaster/tasks/task_011.md +19 -0
  68. data/.taskmaster/tasks/task_012.md +19 -0
  69. data/.taskmaster/tasks/task_013.md +19 -0
  70. data/.taskmaster/tasks/task_014.md +19 -0
  71. data/.taskmaster/tasks/task_015.md +19 -0
  72. data/.taskmaster/tasks/task_016.md +19 -0
  73. data/.taskmaster/tasks/task_017.md +19 -0
  74. data/.taskmaster/tasks/task_018.md +19 -0
  75. data/.taskmaster/tasks/task_019.md +19 -0
  76. data/.taskmaster/tasks/task_020.md +19 -0
  77. data/.taskmaster/tasks/tasks.json +299 -0
  78. data/.taskmaster/templates/example_prd.txt +47 -0
  79. data/.taskmaster/templates/example_prd_rpg.txt +511 -0
  80. data/CHANGELOG.md +29 -0
  81. data/CLAUDE.md +65 -0
  82. data/CODE_OF_CONDUCT.md +10 -0
  83. data/CONTRIBUTING.md +49 -0
  84. data/LICENSE +21 -0
  85. data/README.md +463 -0
  86. data/Rakefile +12 -0
  87. data/app/assets/stylesheets/data_porter/application.css +646 -0
  88. data/app/channels/data_porter/import_channel.rb +10 -0
  89. data/app/controllers/data_porter/imports_controller.rb +68 -0
  90. data/app/javascript/data_porter/progress_controller.js +33 -0
  91. data/app/jobs/data_porter/dry_run_job.rb +12 -0
  92. data/app/jobs/data_porter/import_job.rb +12 -0
  93. data/app/jobs/data_porter/parse_job.rb +12 -0
  94. data/app/models/data_porter/data_import.rb +49 -0
  95. data/app/views/data_porter/imports/index.html.erb +142 -0
  96. data/app/views/data_porter/imports/new.html.erb +88 -0
  97. data/app/views/data_porter/imports/show.html.erb +49 -0
  98. data/config/database.yml +3 -0
  99. data/config/routes.rb +12 -0
  100. data/docs/SPEC.md +2012 -0
  101. data/docs/UI.md +32 -0
  102. data/docs/blog/001-why-build-a-data-import-engine.md +166 -0
  103. data/docs/blog/002-scaffolding-a-rails-engine.md +188 -0
  104. data/docs/blog/003-configuration-dsl.md +222 -0
  105. data/docs/blog/004-store-model-jsonb.md +237 -0
  106. data/docs/blog/005-target-dsl.md +284 -0
  107. data/docs/blog/006-parsing-csv-sources.md +300 -0
  108. data/docs/blog/007-orchestrator.md +247 -0
  109. data/docs/blog/008-actioncable-stimulus.md +376 -0
  110. data/docs/blog/009-phlex-ui-components.md +446 -0
  111. data/docs/blog/010-controllers-routing.md +374 -0
  112. data/docs/blog/011-generators.md +364 -0
  113. data/docs/blog/012-json-api-sources.md +323 -0
  114. data/docs/blog/013-testing-rails-engine.md +618 -0
  115. data/docs/blog/014-dry-run.md +307 -0
  116. data/docs/blog/015-publishing-retro.md +264 -0
  117. data/docs/blog/016-erb-view-templates.md +431 -0
  118. data/docs/blog/017-showcase-final-retro.md +220 -0
  119. data/docs/blog/BACKLOG.md +8 -0
  120. data/docs/blog/SERIES.md +154 -0
  121. data/docs/screenshots/index-with-previewing.jpg +0 -0
  122. data/docs/screenshots/index.jpg +0 -0
  123. data/docs/screenshots/modal-new-import.jpg +0 -0
  124. data/docs/screenshots/preview.jpg +0 -0
  125. data/lib/data_porter/broadcaster.rb +29 -0
  126. data/lib/data_porter/components/base.rb +10 -0
  127. data/lib/data_porter/components/failure_alert.rb +20 -0
  128. data/lib/data_porter/components/preview_table.rb +54 -0
  129. data/lib/data_porter/components/progress_bar.rb +33 -0
  130. data/lib/data_porter/components/results_summary.rb +19 -0
  131. data/lib/data_porter/components/status_badge.rb +16 -0
  132. data/lib/data_porter/components/summary_cards.rb +30 -0
  133. data/lib/data_porter/components.rb +14 -0
  134. data/lib/data_porter/configuration.rb +25 -0
  135. data/lib/data_porter/dsl/api_config.rb +25 -0
  136. data/lib/data_porter/dsl/column.rb +17 -0
  137. data/lib/data_porter/engine.rb +15 -0
  138. data/lib/data_porter/orchestrator.rb +141 -0
  139. data/lib/data_porter/record_validator.rb +32 -0
  140. data/lib/data_porter/registry.rb +33 -0
  141. data/lib/data_porter/sources/api.rb +49 -0
  142. data/lib/data_porter/sources/base.rb +35 -0
  143. data/lib/data_porter/sources/csv.rb +43 -0
  144. data/lib/data_porter/sources/json.rb +45 -0
  145. data/lib/data_porter/sources.rb +20 -0
  146. data/lib/data_porter/store_models/error.rb +13 -0
  147. data/lib/data_porter/store_models/import_record.rb +52 -0
  148. data/lib/data_porter/store_models/report.rb +21 -0
  149. data/lib/data_porter/target.rb +89 -0
  150. data/lib/data_porter/type_validator.rb +46 -0
  151. data/lib/data_porter/version.rb +5 -0
  152. data/lib/data_porter.rb +32 -0
  153. data/lib/generators/data_porter/install/install_generator.rb +33 -0
  154. data/lib/generators/data_porter/install/templates/create_data_porter_imports.rb.erb +21 -0
  155. data/lib/generators/data_porter/install/templates/initializer.rb +30 -0
  156. data/lib/generators/data_porter/target/target_generator.rb +44 -0
  157. data/lib/generators/data_porter/target/templates/target.rb.tt +20 -0
  158. data/sig/data_porter.rbs +4 -0
  159. metadata +274 -0
@@ -0,0 +1,646 @@
1
+ :root {
2
+ --dp-primary: #4f46e5;
3
+ --dp-primary-hover: #4338ca;
4
+ --dp-primary-light: #eef2ff;
5
+ --dp-success: #059669;
6
+ --dp-success-light: #ecfdf5;
7
+ --dp-success-border: #a7f3d0;
8
+ --dp-warning: #d97706;
9
+ --dp-warning-light: #fffbeb;
10
+ --dp-warning-border: #fde68a;
11
+ --dp-danger: #dc2626;
12
+ --dp-danger-hover: #b91c1c;
13
+ --dp-danger-light: #fef2f2;
14
+ --dp-danger-border: #fecaca;
15
+ --dp-info: #2563eb;
16
+ --dp-info-light: #eff6ff;
17
+ --dp-info-border: #bfdbfe;
18
+ --dp-purple: #7c3aed;
19
+ --dp-purple-light: #f5f3ff;
20
+ --dp-purple-border: #ddd6fe;
21
+ --dp-gray-50: #f9fafb;
22
+ --dp-gray-100: #f3f4f6;
23
+ --dp-gray-200: #e5e7eb;
24
+ --dp-gray-300: #d1d5db;
25
+ --dp-gray-400: #9ca3af;
26
+ --dp-gray-500: #6b7280;
27
+ --dp-gray-600: #4b5563;
28
+ --dp-gray-700: #374151;
29
+ --dp-gray-800: #1f2937;
30
+ --dp-gray-900: #111827;
31
+ --dp-radius-sm: 0.375rem;
32
+ --dp-radius-md: 0.5rem;
33
+ --dp-radius-lg: 0.75rem;
34
+ --dp-radius-xl: 1rem;
35
+ --dp-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
36
+ --dp-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
37
+ --dp-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
38
+ }
39
+
40
+ .data-porter {
41
+ font-family: "Inter", system-ui, -apple-system, sans-serif;
42
+ color: var(--dp-gray-800);
43
+ max-width: 1100px;
44
+ margin: 2rem auto;
45
+ padding: 0 1.5rem;
46
+ line-height: 1.6;
47
+ -webkit-font-smoothing: antialiased;
48
+ }
49
+
50
+ .dp-title {
51
+ font-size: 1.75rem;
52
+ font-weight: 700;
53
+ margin: 0;
54
+ letter-spacing: -0.025em;
55
+ color: var(--dp-gray-900);
56
+ }
57
+
58
+ .dp-header {
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: space-between;
62
+ gap: 1rem;
63
+ margin-bottom: 2rem;
64
+ padding-bottom: 1.5rem;
65
+ border-bottom: 1px solid var(--dp-gray-200);
66
+ }
67
+
68
+ .dp-table {
69
+ width: 100%;
70
+ border-collapse: separate;
71
+ border-spacing: 0;
72
+ margin-bottom: 1.5rem;
73
+ background: white;
74
+ border: 1px solid var(--dp-gray-200);
75
+ border-radius: var(--dp-radius-lg);
76
+ overflow: hidden;
77
+ box-shadow: var(--dp-shadow-sm);
78
+ }
79
+
80
+ .dp-table th,
81
+ .dp-table td {
82
+ padding: 0.75rem 1rem;
83
+ text-align: left;
84
+ }
85
+
86
+ .dp-table th {
87
+ font-weight: 600;
88
+ font-size: 0.75rem;
89
+ text-transform: uppercase;
90
+ letter-spacing: 0.05em;
91
+ color: var(--dp-gray-500);
92
+ background: var(--dp-gray-50);
93
+ border-bottom: 1px solid var(--dp-gray-200);
94
+ }
95
+
96
+ .dp-table td {
97
+ font-size: 0.875rem;
98
+ color: var(--dp-gray-700);
99
+ border-bottom: 1px solid var(--dp-gray-100);
100
+ }
101
+
102
+ .dp-table tbody tr:last-child td {
103
+ border-bottom: none;
104
+ }
105
+
106
+ .dp-table tbody tr {
107
+ transition: background-color 0.15s ease;
108
+ }
109
+
110
+ .dp-table tbody tr:hover {
111
+ background: var(--dp-gray-50);
112
+ }
113
+
114
+ .dp-badge {
115
+ display: inline-flex;
116
+ align-items: center;
117
+ padding: 0.25rem 0.75rem;
118
+ border-radius: 9999px;
119
+ font-size: 0.7rem;
120
+ font-weight: 600;
121
+ text-transform: uppercase;
122
+ letter-spacing: 0.05em;
123
+ border: 1px solid transparent;
124
+ }
125
+
126
+ .dp-badge--pending {
127
+ background: var(--dp-gray-100);
128
+ color: var(--dp-gray-600);
129
+ border-color: var(--dp-gray-300);
130
+ }
131
+
132
+ .dp-badge--parsing {
133
+ background: var(--dp-info-light);
134
+ color: var(--dp-info);
135
+ border-color: var(--dp-info-border);
136
+ }
137
+
138
+ .dp-badge--importing {
139
+ background: var(--dp-info-light);
140
+ color: var(--dp-info);
141
+ border-color: var(--dp-info-border);
142
+ animation: dp-pulse 2s ease-in-out infinite;
143
+ }
144
+
145
+ .dp-badge--previewing {
146
+ background: var(--dp-warning-light);
147
+ color: var(--dp-warning);
148
+ border-color: var(--dp-warning-border);
149
+ }
150
+
151
+ .dp-badge--completed {
152
+ background: var(--dp-success-light);
153
+ color: var(--dp-success);
154
+ border-color: var(--dp-success-border);
155
+ }
156
+
157
+ .dp-badge--failed {
158
+ background: var(--dp-danger-light);
159
+ color: var(--dp-danger);
160
+ border-color: var(--dp-danger-border);
161
+ }
162
+
163
+ .dp-badge--dry_running {
164
+ background: var(--dp-purple-light);
165
+ color: var(--dp-purple);
166
+ border-color: var(--dp-purple-border);
167
+ animation: dp-pulse 2s ease-in-out infinite;
168
+ }
169
+
170
+ @keyframes dp-pulse {
171
+ 0%, 100% { opacity: 1; }
172
+ 50% { opacity: 0.6; }
173
+ }
174
+
175
+ .dp-summary-cards {
176
+ display: grid;
177
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
178
+ gap: 1rem;
179
+ margin-bottom: 2rem;
180
+ }
181
+
182
+ .dp-card {
183
+ padding: 1.25rem;
184
+ border-radius: var(--dp-radius-lg);
185
+ border: 1px solid var(--dp-gray-200);
186
+ text-align: center;
187
+ background: white;
188
+ box-shadow: var(--dp-shadow-sm);
189
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
190
+ }
191
+
192
+ .dp-card:hover {
193
+ transform: translateY(-1px);
194
+ box-shadow: var(--dp-shadow-md);
195
+ }
196
+
197
+ .dp-card strong {
198
+ display: block;
199
+ font-size: 2rem;
200
+ font-weight: 700;
201
+ margin-bottom: 0.25rem;
202
+ letter-spacing: -0.025em;
203
+ }
204
+
205
+ .dp-card span {
206
+ font-size: 0.8rem;
207
+ font-weight: 500;
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.05em;
210
+ }
211
+
212
+ .dp-card--complete {
213
+ border-color: var(--dp-success-border);
214
+ background: var(--dp-success-light);
215
+ }
216
+
217
+ .dp-card--complete strong { color: var(--dp-success); }
218
+
219
+ .dp-card--partial {
220
+ border-color: var(--dp-warning-border);
221
+ background: var(--dp-warning-light);
222
+ }
223
+
224
+ .dp-card--partial strong { color: var(--dp-warning); }
225
+
226
+ .dp-card--missing {
227
+ border-color: var(--dp-danger-border);
228
+ background: var(--dp-danger-light);
229
+ }
230
+
231
+ .dp-card--missing strong { color: var(--dp-danger); }
232
+
233
+ .dp-card--duplicate {
234
+ border-color: var(--dp-gray-300);
235
+ background: var(--dp-gray-50);
236
+ }
237
+
238
+ .dp-card--duplicate strong { color: var(--dp-gray-500); }
239
+
240
+ .dp-preview-table {
241
+ overflow-x: auto;
242
+ margin-bottom: 2rem;
243
+ border-radius: var(--dp-radius-lg);
244
+ border: 1px solid var(--dp-gray-200);
245
+ box-shadow: var(--dp-shadow-sm);
246
+ }
247
+
248
+ .dp-preview-table .dp-table {
249
+ margin-bottom: 0;
250
+ border: none;
251
+ border-radius: 0;
252
+ box-shadow: none;
253
+ }
254
+
255
+ .dp-row--complete { background: var(--dp-success-light); }
256
+ .dp-row--partial { background: var(--dp-warning-light); }
257
+ .dp-row--missing { background: var(--dp-danger-light); }
258
+
259
+ .dp-errors {
260
+ color: var(--dp-danger);
261
+ font-size: 0.8rem;
262
+ font-weight: 500;
263
+ }
264
+
265
+ .dp-progress {
266
+ margin: 2rem 0;
267
+ background: var(--dp-gray-200);
268
+ border-radius: var(--dp-radius-xl);
269
+ overflow: hidden;
270
+ height: 1.75rem;
271
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.08);
272
+ }
273
+
274
+ .dp-progress-bar {
275
+ height: 100%;
276
+ background: linear-gradient(135deg, var(--dp-primary), #818cf8);
277
+ border-radius: var(--dp-radius-xl);
278
+ transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
279
+ display: flex;
280
+ align-items: center;
281
+ justify-content: center;
282
+ color: white;
283
+ font-size: 0.75rem;
284
+ font-weight: 700;
285
+ min-width: 2.5rem;
286
+ position: relative;
287
+ overflow: hidden;
288
+ }
289
+
290
+ .dp-progress-bar::after {
291
+ content: "";
292
+ position: absolute;
293
+ inset: 0;
294
+ background: linear-gradient(
295
+ 90deg,
296
+ transparent,
297
+ rgba(255, 255, 255, 0.2),
298
+ transparent
299
+ );
300
+ animation: dp-shimmer 1.5s infinite;
301
+ }
302
+
303
+ @keyframes dp-shimmer {
304
+ 0% { transform: translateX(-100%); }
305
+ 100% { transform: translateX(100%); }
306
+ }
307
+
308
+ .dp-results {
309
+ padding: 1.5rem;
310
+ background: var(--dp-success-light);
311
+ border: 1px solid var(--dp-success-border);
312
+ border-radius: var(--dp-radius-lg);
313
+ margin-bottom: 2rem;
314
+ box-shadow: var(--dp-shadow-sm);
315
+ }
316
+
317
+ .dp-results p {
318
+ margin: 0.35rem 0;
319
+ font-size: 0.9375rem;
320
+ }
321
+
322
+ .dp-alert {
323
+ padding: 1.25rem 1.5rem;
324
+ border-radius: var(--dp-radius-lg);
325
+ margin-bottom: 2rem;
326
+ box-shadow: var(--dp-shadow-sm);
327
+ }
328
+
329
+ .dp-alert--danger {
330
+ background: var(--dp-danger-light);
331
+ border: 1px solid var(--dp-danger-border);
332
+ color: var(--dp-danger);
333
+ }
334
+
335
+ .dp-alert p {
336
+ margin: 0.35rem 0;
337
+ font-size: 0.9375rem;
338
+ }
339
+
340
+ .dp-form {
341
+ max-width: 520px;
342
+ background: white;
343
+ padding: 2rem;
344
+ border: 1px solid var(--dp-gray-200);
345
+ border-radius: var(--dp-radius-lg);
346
+ box-shadow: var(--dp-shadow-md);
347
+ }
348
+
349
+ .dp-field {
350
+ margin-bottom: 1.5rem;
351
+ }
352
+
353
+ .dp-label {
354
+ display: block;
355
+ font-weight: 600;
356
+ margin-bottom: 0.5rem;
357
+ font-size: 0.875rem;
358
+ color: var(--dp-gray-700);
359
+ }
360
+
361
+ .dp-select,
362
+ .dp-file-input {
363
+ display: block;
364
+ width: 100%;
365
+ padding: 0.625rem 0.875rem;
366
+ border: 1px solid var(--dp-gray-300);
367
+ border-radius: var(--dp-radius-md);
368
+ font-size: 0.9375rem;
369
+ background: white;
370
+ color: var(--dp-gray-800);
371
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
372
+ appearance: none;
373
+ }
374
+
375
+ .dp-select {
376
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
377
+ background-position: right 0.5rem center;
378
+ background-repeat: no-repeat;
379
+ background-size: 1.5em 1.5em;
380
+ padding-right: 2.5rem;
381
+ }
382
+
383
+ .dp-select:focus,
384
+ .dp-file-input:focus {
385
+ outline: none;
386
+ border-color: var(--dp-primary);
387
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
388
+ }
389
+
390
+ .dp-actions {
391
+ display: flex;
392
+ gap: 0.75rem;
393
+ margin: 2rem 0;
394
+ }
395
+
396
+ .dp-btn {
397
+ display: inline-flex;
398
+ align-items: center;
399
+ justify-content: center;
400
+ padding: 0.625rem 1.25rem;
401
+ border-radius: var(--dp-radius-md);
402
+ font-size: 0.875rem;
403
+ font-weight: 600;
404
+ text-decoration: none;
405
+ border: 1px solid transparent;
406
+ cursor: pointer;
407
+ transition: all 0.15s ease;
408
+ line-height: 1.25;
409
+ }
410
+
411
+ .dp-btn:active {
412
+ transform: scale(0.98);
413
+ }
414
+
415
+ .dp-btn--primary {
416
+ background: var(--dp-primary);
417
+ color: white;
418
+ box-shadow: var(--dp-shadow-sm);
419
+ }
420
+
421
+ .dp-btn--primary:hover {
422
+ background: var(--dp-primary-hover);
423
+ box-shadow: var(--dp-shadow-md);
424
+ }
425
+
426
+ .dp-btn--secondary {
427
+ background: white;
428
+ color: var(--dp-gray-700);
429
+ border-color: var(--dp-gray-300);
430
+ box-shadow: var(--dp-shadow-sm);
431
+ }
432
+
433
+ .dp-btn--secondary:hover {
434
+ background: var(--dp-gray-50);
435
+ border-color: var(--dp-gray-400);
436
+ }
437
+
438
+ .dp-btn--danger {
439
+ background: var(--dp-danger);
440
+ color: white;
441
+ box-shadow: var(--dp-shadow-sm);
442
+ }
443
+
444
+ .dp-btn--danger:hover {
445
+ background: var(--dp-danger-hover);
446
+ box-shadow: var(--dp-shadow-md);
447
+ }
448
+
449
+ .dp-link {
450
+ color: var(--dp-primary);
451
+ text-decoration: none;
452
+ font-weight: 500;
453
+ transition: color 0.15s ease;
454
+ }
455
+
456
+ .dp-link:hover {
457
+ color: var(--dp-primary-hover);
458
+ text-decoration: underline;
459
+ }
460
+
461
+ .dp-nav {
462
+ margin-top: 2rem;
463
+ padding-top: 1.5rem;
464
+ border-top: 1px solid var(--dp-gray-200);
465
+ }
466
+
467
+ .dp-empty {
468
+ text-align: center;
469
+ padding: 3rem 1rem;
470
+ color: var(--dp-gray-400);
471
+ font-size: 0.9375rem;
472
+ }
473
+
474
+ .dp-empty-state {
475
+ text-align: center;
476
+ padding: 4rem 2rem;
477
+ background: white;
478
+ border: 2px dashed var(--dp-gray-300);
479
+ border-radius: var(--dp-radius-xl);
480
+ }
481
+
482
+ .dp-empty-state__icon {
483
+ font-size: 3rem;
484
+ margin-bottom: 1rem;
485
+ opacity: 0.6;
486
+ }
487
+
488
+ .dp-empty-state__text {
489
+ font-size: 1.125rem;
490
+ color: var(--dp-gray-500);
491
+ margin: 0 0 1.5rem;
492
+ }
493
+
494
+ .dp-modal {
495
+ display: none;
496
+ position: fixed;
497
+ inset: 0;
498
+ z-index: 9999;
499
+ align-items: center;
500
+ justify-content: center;
501
+ }
502
+
503
+ .dp-modal--open {
504
+ display: flex;
505
+ }
506
+
507
+ .dp-modal__backdrop {
508
+ position: fixed;
509
+ inset: 0;
510
+ background: rgba(0, 0, 0, 0.4);
511
+ backdrop-filter: blur(4px);
512
+ }
513
+
514
+ .dp-modal__content {
515
+ position: relative;
516
+ background: white;
517
+ border-radius: var(--dp-radius-xl);
518
+ box-shadow: var(--dp-shadow-lg), 0 25px 50px -12px rgba(0, 0, 0, 0.15);
519
+ width: 100%;
520
+ max-width: 520px;
521
+ max-height: 90vh;
522
+ overflow-y: auto;
523
+ animation: dp-modal-in 0.2s ease-out;
524
+ }
525
+
526
+ @keyframes dp-modal-in {
527
+ from {
528
+ opacity: 0;
529
+ transform: scale(0.95) translateY(10px);
530
+ }
531
+ to {
532
+ opacity: 1;
533
+ transform: scale(1) translateY(0);
534
+ }
535
+ }
536
+
537
+ .dp-modal__header {
538
+ display: flex;
539
+ align-items: center;
540
+ justify-content: space-between;
541
+ padding: 1.5rem 1.5rem 0;
542
+ }
543
+
544
+ .dp-modal__title {
545
+ font-size: 1.25rem;
546
+ font-weight: 700;
547
+ margin: 0;
548
+ color: var(--dp-gray-900);
549
+ }
550
+
551
+ .dp-modal__close {
552
+ background: none;
553
+ border: none;
554
+ font-size: 1.5rem;
555
+ color: var(--dp-gray-400);
556
+ cursor: pointer;
557
+ padding: 0.25rem;
558
+ line-height: 1;
559
+ border-radius: var(--dp-radius-sm);
560
+ transition: color 0.15s ease, background 0.15s ease;
561
+ }
562
+
563
+ .dp-modal__close:hover {
564
+ color: var(--dp-gray-700);
565
+ background: var(--dp-gray-100);
566
+ }
567
+
568
+ .dp-modal__body {
569
+ padding: 1.5rem;
570
+ }
571
+
572
+ .dp-modal__footer {
573
+ display: flex;
574
+ gap: 0.75rem;
575
+ justify-content: flex-end;
576
+ padding-top: 0.5rem;
577
+ border-top: 1px solid var(--dp-gray-100);
578
+ margin-top: 0.5rem;
579
+ padding-top: 1.5rem;
580
+ }
581
+
582
+ .dp-dropzone {
583
+ display: flex;
584
+ flex-direction: column;
585
+ align-items: center;
586
+ justify-content: center;
587
+ padding: 2rem 1.5rem;
588
+ border: 2px dashed var(--dp-gray-300);
589
+ border-radius: var(--dp-radius-lg);
590
+ cursor: pointer;
591
+ transition: border-color 0.15s ease, background 0.15s ease;
592
+ text-align: center;
593
+ }
594
+
595
+ .dp-dropzone:hover,
596
+ .dp-dropzone--dragover {
597
+ border-color: var(--dp-primary);
598
+ background: var(--dp-primary-light);
599
+ }
600
+
601
+ .dp-dropzone--has-file {
602
+ border-color: var(--dp-success);
603
+ background: var(--dp-success-light);
604
+ border-style: solid;
605
+ }
606
+
607
+ .dp-dropzone__input {
608
+ position: absolute;
609
+ width: 1px;
610
+ height: 1px;
611
+ overflow: hidden;
612
+ clip: rect(0, 0, 0, 0);
613
+ }
614
+
615
+ .dp-dropzone__content {
616
+ display: flex;
617
+ flex-direction: column;
618
+ align-items: center;
619
+ gap: 0.5rem;
620
+ }
621
+
622
+ .dp-dropzone__icon {
623
+ font-size: 2rem;
624
+ opacity: 0.5;
625
+ }
626
+
627
+ .dp-dropzone__text {
628
+ font-size: 0.9375rem;
629
+ color: var(--dp-gray-600);
630
+ }
631
+
632
+ .dp-dropzone__text strong {
633
+ color: var(--dp-primary);
634
+ }
635
+
636
+ .dp-dropzone__hint {
637
+ font-size: 0.8rem;
638
+ color: var(--dp-gray-400);
639
+ }
640
+
641
+ .dp-dropzone__selected {
642
+ font-size: 0.875rem;
643
+ font-weight: 600;
644
+ color: var(--dp-success);
645
+ margin-top: 0.5rem;
646
+ }
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataPorter
4
+ class ImportChannel < ActionCable::Channel::Base
5
+ def subscribed
6
+ prefix = DataPorter.configuration.cable_channel_prefix
7
+ stream_from "#{prefix}/imports/#{params[:id]}"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataPorter
4
+ class ImportsController < DataPorter.configuration.parent_controller.constantize
5
+ before_action :set_import, only: %i[show parse confirm cancel dry_run]
6
+
7
+ def index
8
+ @imports = DataPorter::DataImport.order(created_at: :desc)
9
+ @targets = DataPorter::Registry.available
10
+ end
11
+
12
+ def new
13
+ @import = DataPorter::DataImport.new
14
+ @targets = DataPorter::Registry.available
15
+ end
16
+
17
+ def create
18
+ @import = DataPorter::DataImport.new(import_params)
19
+ @import.user = current_user if respond_to?(:current_user, true)
20
+ @import.status = :pending
21
+
22
+ if @import.save
23
+ DataPorter::ParseJob.perform_later(@import.id)
24
+ redirect_to import_path(@import)
25
+ else
26
+ @targets = DataPorter::Registry.available
27
+ render :new
28
+ end
29
+ end
30
+
31
+ def show
32
+ @target = @import.target_class
33
+ @records = @import.records
34
+ @grouped = @records.group_by(&:status)
35
+ end
36
+
37
+ def parse
38
+ @import.update!(status: :pending)
39
+ DataPorter::ParseJob.perform_later(@import.id)
40
+ redirect_to import_path(@import)
41
+ end
42
+
43
+ def confirm
44
+ DataPorter::ImportJob.perform_later(@import.id)
45
+ redirect_to import_path(@import)
46
+ end
47
+
48
+ def cancel
49
+ @import.update!(status: :failed)
50
+ redirect_to imports_path
51
+ end
52
+
53
+ def dry_run
54
+ DataPorter::DryRunJob.perform_later(@import.id)
55
+ redirect_to import_path(@import)
56
+ end
57
+
58
+ private
59
+
60
+ def set_import
61
+ @import = DataPorter::DataImport.find(params[:id])
62
+ end
63
+
64
+ def import_params
65
+ params.require(:data_import).permit(:target_key, :source_type, :file, config: {})
66
+ end
67
+ end
68
+ end