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.
- checksums.yaml +7 -0
- data/.claude/commands/blog-status.md +10 -0
- data/.claude/commands/blog.md +109 -0
- data/.claude/commands/task-done.md +27 -0
- data/.claude/commands/tm/add-dependency.md +58 -0
- data/.claude/commands/tm/add-subtask.md +79 -0
- data/.claude/commands/tm/add-task.md +81 -0
- data/.claude/commands/tm/analyze-complexity.md +124 -0
- data/.claude/commands/tm/analyze-project.md +100 -0
- data/.claude/commands/tm/auto-implement-tasks.md +100 -0
- data/.claude/commands/tm/command-pipeline.md +80 -0
- data/.claude/commands/tm/complexity-report.md +120 -0
- data/.claude/commands/tm/convert-task-to-subtask.md +74 -0
- data/.claude/commands/tm/expand-all-tasks.md +52 -0
- data/.claude/commands/tm/expand-task.md +52 -0
- data/.claude/commands/tm/fix-dependencies.md +82 -0
- data/.claude/commands/tm/help.md +101 -0
- data/.claude/commands/tm/init-project-quick.md +49 -0
- data/.claude/commands/tm/init-project.md +53 -0
- data/.claude/commands/tm/install-taskmaster.md +118 -0
- data/.claude/commands/tm/learn.md +106 -0
- data/.claude/commands/tm/list-tasks-by-status.md +42 -0
- data/.claude/commands/tm/list-tasks-with-subtasks.md +30 -0
- data/.claude/commands/tm/list-tasks.md +46 -0
- data/.claude/commands/tm/next-task.md +69 -0
- data/.claude/commands/tm/parse-prd-with-research.md +51 -0
- data/.claude/commands/tm/parse-prd.md +52 -0
- data/.claude/commands/tm/project-status.md +67 -0
- data/.claude/commands/tm/quick-install-taskmaster.md +23 -0
- data/.claude/commands/tm/remove-all-subtasks.md +94 -0
- data/.claude/commands/tm/remove-dependency.md +65 -0
- data/.claude/commands/tm/remove-subtask.md +87 -0
- data/.claude/commands/tm/remove-subtasks.md +89 -0
- data/.claude/commands/tm/remove-task.md +110 -0
- data/.claude/commands/tm/setup-models.md +52 -0
- data/.claude/commands/tm/show-task.md +85 -0
- data/.claude/commands/tm/smart-workflow.md +58 -0
- data/.claude/commands/tm/sync-readme.md +120 -0
- data/.claude/commands/tm/tm-main.md +147 -0
- data/.claude/commands/tm/to-cancelled.md +58 -0
- data/.claude/commands/tm/to-deferred.md +50 -0
- data/.claude/commands/tm/to-done.md +47 -0
- data/.claude/commands/tm/to-in-progress.md +39 -0
- data/.claude/commands/tm/to-pending.md +35 -0
- data/.claude/commands/tm/to-review.md +43 -0
- data/.claude/commands/tm/update-single-task.md +122 -0
- data/.claude/commands/tm/update-task.md +75 -0
- data/.claude/commands/tm/update-tasks-from-id.md +111 -0
- data/.claude/commands/tm/validate-dependencies.md +72 -0
- data/.claude/commands/tm/view-models.md +52 -0
- data/.env.example +12 -0
- data/.mcp.json +24 -0
- data/.taskmaster/CLAUDE.md +435 -0
- data/.taskmaster/config.json +44 -0
- data/.taskmaster/docs/prd.txt +2044 -0
- data/.taskmaster/state.json +6 -0
- data/.taskmaster/tasks/task_001.md +19 -0
- data/.taskmaster/tasks/task_002.md +19 -0
- data/.taskmaster/tasks/task_003.md +19 -0
- data/.taskmaster/tasks/task_004.md +19 -0
- data/.taskmaster/tasks/task_005.md +19 -0
- data/.taskmaster/tasks/task_006.md +19 -0
- data/.taskmaster/tasks/task_007.md +19 -0
- data/.taskmaster/tasks/task_008.md +19 -0
- data/.taskmaster/tasks/task_009.md +19 -0
- data/.taskmaster/tasks/task_010.md +19 -0
- data/.taskmaster/tasks/task_011.md +19 -0
- data/.taskmaster/tasks/task_012.md +19 -0
- data/.taskmaster/tasks/task_013.md +19 -0
- data/.taskmaster/tasks/task_014.md +19 -0
- data/.taskmaster/tasks/task_015.md +19 -0
- data/.taskmaster/tasks/task_016.md +19 -0
- data/.taskmaster/tasks/task_017.md +19 -0
- data/.taskmaster/tasks/task_018.md +19 -0
- data/.taskmaster/tasks/task_019.md +19 -0
- data/.taskmaster/tasks/task_020.md +19 -0
- data/.taskmaster/tasks/tasks.json +299 -0
- data/.taskmaster/templates/example_prd.txt +47 -0
- data/.taskmaster/templates/example_prd_rpg.txt +511 -0
- data/CHANGELOG.md +29 -0
- data/CLAUDE.md +65 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +49 -0
- data/LICENSE +21 -0
- data/README.md +463 -0
- data/Rakefile +12 -0
- data/app/assets/stylesheets/data_porter/application.css +646 -0
- data/app/channels/data_porter/import_channel.rb +10 -0
- data/app/controllers/data_porter/imports_controller.rb +68 -0
- data/app/javascript/data_porter/progress_controller.js +33 -0
- data/app/jobs/data_porter/dry_run_job.rb +12 -0
- data/app/jobs/data_porter/import_job.rb +12 -0
- data/app/jobs/data_porter/parse_job.rb +12 -0
- data/app/models/data_porter/data_import.rb +49 -0
- data/app/views/data_porter/imports/index.html.erb +142 -0
- data/app/views/data_porter/imports/new.html.erb +88 -0
- data/app/views/data_porter/imports/show.html.erb +49 -0
- data/config/database.yml +3 -0
- data/config/routes.rb +12 -0
- data/docs/SPEC.md +2012 -0
- data/docs/UI.md +32 -0
- data/docs/blog/001-why-build-a-data-import-engine.md +166 -0
- data/docs/blog/002-scaffolding-a-rails-engine.md +188 -0
- data/docs/blog/003-configuration-dsl.md +222 -0
- data/docs/blog/004-store-model-jsonb.md +237 -0
- data/docs/blog/005-target-dsl.md +284 -0
- data/docs/blog/006-parsing-csv-sources.md +300 -0
- data/docs/blog/007-orchestrator.md +247 -0
- data/docs/blog/008-actioncable-stimulus.md +376 -0
- data/docs/blog/009-phlex-ui-components.md +446 -0
- data/docs/blog/010-controllers-routing.md +374 -0
- data/docs/blog/011-generators.md +364 -0
- data/docs/blog/012-json-api-sources.md +323 -0
- data/docs/blog/013-testing-rails-engine.md +618 -0
- data/docs/blog/014-dry-run.md +307 -0
- data/docs/blog/015-publishing-retro.md +264 -0
- data/docs/blog/016-erb-view-templates.md +431 -0
- data/docs/blog/017-showcase-final-retro.md +220 -0
- data/docs/blog/BACKLOG.md +8 -0
- data/docs/blog/SERIES.md +154 -0
- data/docs/screenshots/index-with-previewing.jpg +0 -0
- data/docs/screenshots/index.jpg +0 -0
- data/docs/screenshots/modal-new-import.jpg +0 -0
- data/docs/screenshots/preview.jpg +0 -0
- data/lib/data_porter/broadcaster.rb +29 -0
- data/lib/data_porter/components/base.rb +10 -0
- data/lib/data_porter/components/failure_alert.rb +20 -0
- data/lib/data_porter/components/preview_table.rb +54 -0
- data/lib/data_porter/components/progress_bar.rb +33 -0
- data/lib/data_porter/components/results_summary.rb +19 -0
- data/lib/data_porter/components/status_badge.rb +16 -0
- data/lib/data_porter/components/summary_cards.rb +30 -0
- data/lib/data_porter/components.rb +14 -0
- data/lib/data_porter/configuration.rb +25 -0
- data/lib/data_porter/dsl/api_config.rb +25 -0
- data/lib/data_porter/dsl/column.rb +17 -0
- data/lib/data_porter/engine.rb +15 -0
- data/lib/data_porter/orchestrator.rb +141 -0
- data/lib/data_porter/record_validator.rb +32 -0
- data/lib/data_porter/registry.rb +33 -0
- data/lib/data_porter/sources/api.rb +49 -0
- data/lib/data_porter/sources/base.rb +35 -0
- data/lib/data_porter/sources/csv.rb +43 -0
- data/lib/data_porter/sources/json.rb +45 -0
- data/lib/data_porter/sources.rb +20 -0
- data/lib/data_porter/store_models/error.rb +13 -0
- data/lib/data_porter/store_models/import_record.rb +52 -0
- data/lib/data_porter/store_models/report.rb +21 -0
- data/lib/data_porter/target.rb +89 -0
- data/lib/data_porter/type_validator.rb +46 -0
- data/lib/data_porter/version.rb +5 -0
- data/lib/data_porter.rb +32 -0
- data/lib/generators/data_porter/install/install_generator.rb +33 -0
- data/lib/generators/data_porter/install/templates/create_data_porter_imports.rb.erb +21 -0
- data/lib/generators/data_porter/install/templates/initializer.rb +30 -0
- data/lib/generators/data_porter/target/target_generator.rb +44 -0
- data/lib/generators/data_porter/target/templates/target.rb.tt +20 -0
- data/sig/data_porter.rbs +4 -0
- metadata +274 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { createConsumer } from "@rails/actioncable"
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ["bar", "text"]
|
|
6
|
+
static values = { id: Number }
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
this.subscription = createConsumer().subscriptions.create(
|
|
10
|
+
{ channel: "DataPorter::ImportChannel", id: this.idValue },
|
|
11
|
+
{
|
|
12
|
+
received: (data) => {
|
|
13
|
+
if (data.status === "processing") {
|
|
14
|
+
this.updateProgress(data.percentage)
|
|
15
|
+
} else {
|
|
16
|
+
window.location.reload()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
updateProgress(percentage) {
|
|
24
|
+
if (this.hasBarTarget) {
|
|
25
|
+
this.barTarget.style.width = `${percentage}%`
|
|
26
|
+
this.textTarget.textContent = `${percentage}%`
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
disconnect() {
|
|
31
|
+
this.subscription?.unsubscribe()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataPorter
|
|
4
|
+
class DryRunJob < ActiveJob::Base
|
|
5
|
+
queue_as { DataPorter.configuration.queue_name }
|
|
6
|
+
|
|
7
|
+
def perform(import_id)
|
|
8
|
+
data_import = DataImport.find(import_id)
|
|
9
|
+
Orchestrator.new(data_import).dry_run!
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataPorter
|
|
4
|
+
class ImportJob < ActiveJob::Base
|
|
5
|
+
queue_as { DataPorter.configuration.queue_name }
|
|
6
|
+
|
|
7
|
+
def perform(import_id)
|
|
8
|
+
data_import = DataImport.find(import_id)
|
|
9
|
+
Orchestrator.new(data_import).import!
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataPorter
|
|
4
|
+
class ParseJob < ActiveJob::Base
|
|
5
|
+
queue_as { DataPorter.configuration.queue_name }
|
|
6
|
+
|
|
7
|
+
def perform(import_id)
|
|
8
|
+
data_import = DataImport.find(import_id)
|
|
9
|
+
Orchestrator.new(data_import).parse!
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataPorter
|
|
4
|
+
class DataImport < ActiveRecord::Base
|
|
5
|
+
self.table_name = "data_porter_imports"
|
|
6
|
+
|
|
7
|
+
has_one_attached :file
|
|
8
|
+
|
|
9
|
+
belongs_to :user, polymorphic: true, optional: true
|
|
10
|
+
|
|
11
|
+
enum :status, {
|
|
12
|
+
pending: 0,
|
|
13
|
+
parsing: 1,
|
|
14
|
+
previewing: 2,
|
|
15
|
+
importing: 3,
|
|
16
|
+
completed: 4,
|
|
17
|
+
failed: 5,
|
|
18
|
+
dry_running: 6
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
attribute :records, StoreModels::ImportRecord.to_array_type, default: -> { [] }
|
|
22
|
+
attribute :report, StoreModels::Report.to_type, default: -> { StoreModels::Report.new }
|
|
23
|
+
|
|
24
|
+
attribute :config, :json, default: -> { {} }
|
|
25
|
+
|
|
26
|
+
validates :target_key, presence: true
|
|
27
|
+
validates :source_type, presence: true, inclusion: { in: %w[csv json api] }
|
|
28
|
+
|
|
29
|
+
def target_class
|
|
30
|
+
Registry.find(target_key)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def source_class
|
|
34
|
+
Sources.resolve(source_type)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def previewable?
|
|
38
|
+
previewing? && records.any?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def importable_records
|
|
42
|
+
records.select(&:importable?)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def records_summary
|
|
46
|
+
records.group_by(&:status).transform_values(&:count)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<%= stylesheet_link_tag "data_porter/application" %>
|
|
2
|
+
|
|
3
|
+
<div class="data-porter">
|
|
4
|
+
<div class="dp-header">
|
|
5
|
+
<h1 class="dp-title">Imports</h1>
|
|
6
|
+
<button type="button" class="dp-btn dp-btn--primary" onclick="document.getElementById('dp-modal').classList.add('dp-modal--open')">
|
|
7
|
+
New Import
|
|
8
|
+
</button>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<% if @imports.any? %>
|
|
12
|
+
<table class="dp-table">
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<th>ID</th>
|
|
16
|
+
<th>Target</th>
|
|
17
|
+
<th>Source</th>
|
|
18
|
+
<th>Status</th>
|
|
19
|
+
<th>Created</th>
|
|
20
|
+
<th></th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
<% @imports.each do |import| %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td><%= import.id %></td>
|
|
27
|
+
<td><%= import.target_key %></td>
|
|
28
|
+
<td><%= import.source_type %></td>
|
|
29
|
+
<td><%= raw DataPorter::Components::StatusBadge.new(status: import.status).call %></td>
|
|
30
|
+
<td><%= import.created_at&.strftime("%Y-%m-%d %H:%M") %></td>
|
|
31
|
+
<td><%= link_to "View", import_path(import), class: "dp-link" %></td>
|
|
32
|
+
</tr>
|
|
33
|
+
<% end %>
|
|
34
|
+
</tbody>
|
|
35
|
+
</table>
|
|
36
|
+
<% else %>
|
|
37
|
+
<div class="dp-empty-state">
|
|
38
|
+
<div class="dp-empty-state__icon">📦</div>
|
|
39
|
+
<p class="dp-empty-state__text">No imports yet</p>
|
|
40
|
+
<button type="button" class="dp-btn dp-btn--primary" onclick="document.getElementById('dp-modal').classList.add('dp-modal--open')">
|
|
41
|
+
Create your first import
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div id="dp-modal" class="dp-modal">
|
|
48
|
+
<div class="dp-modal__backdrop" onclick="document.getElementById('dp-modal').classList.remove('dp-modal--open')"></div>
|
|
49
|
+
<div class="dp-modal__content">
|
|
50
|
+
<div class="dp-modal__header">
|
|
51
|
+
<h2 class="dp-modal__title">New Import</h2>
|
|
52
|
+
<button type="button" class="dp-modal__close" onclick="document.getElementById('dp-modal').classList.remove('dp-modal--open')">×</button>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<%= form_with model: DataPorter::DataImport.new, url: imports_path, class: "dp-modal__body", multipart: true do |f| %>
|
|
56
|
+
<div class="dp-field">
|
|
57
|
+
<%= f.label :target_key, "Target", class: "dp-label" %>
|
|
58
|
+
<%= f.select :target_key,
|
|
59
|
+
@targets.map { |t| [t[:label], t[:key]] },
|
|
60
|
+
{ prompt: "Select a target..." },
|
|
61
|
+
class: "dp-select" %>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="dp-field">
|
|
65
|
+
<%= f.label :source_type, "Source Type", class: "dp-label" %>
|
|
66
|
+
<%= f.select :source_type,
|
|
67
|
+
DataPorter.configuration.enabled_sources.map { |s| [s.to_s.upcase, s] },
|
|
68
|
+
{ prompt: "Select source type..." },
|
|
69
|
+
id: "dp-source-select",
|
|
70
|
+
class: "dp-select" %>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div id="dp-file-field" class="dp-field">
|
|
74
|
+
<%= f.label :file, "File", class: "dp-label" %>
|
|
75
|
+
<label class="dp-dropzone" id="dp-dropzone">
|
|
76
|
+
<input type="file" name="data_import[file]" id="dp-file-input" class="dp-dropzone__input" />
|
|
77
|
+
<div class="dp-dropzone__content">
|
|
78
|
+
<div class="dp-dropzone__icon">📄</div>
|
|
79
|
+
<span class="dp-dropzone__text">Drop your file here or <strong>browse</strong></span>
|
|
80
|
+
<span class="dp-dropzone__hint">CSV or JSON files accepted</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="dp-dropzone__selected" id="dp-file-name" style="display: none;"></div>
|
|
83
|
+
</label>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="dp-modal__footer">
|
|
87
|
+
<%= f.submit "Start Import", class: "dp-btn dp-btn--primary" %>
|
|
88
|
+
<button type="button" class="dp-btn dp-btn--secondary" onclick="document.getElementById('dp-modal').classList.remove('dp-modal--open')">Cancel</button>
|
|
89
|
+
</div>
|
|
90
|
+
<% end %>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<script>
|
|
95
|
+
(function() {
|
|
96
|
+
var sourceSelect = document.getElementById("dp-source-select");
|
|
97
|
+
var fileField = document.getElementById("dp-file-field");
|
|
98
|
+
var fileInput = document.getElementById("dp-file-input");
|
|
99
|
+
var dropzone = document.getElementById("dp-dropzone");
|
|
100
|
+
var fileName = document.getElementById("dp-file-name");
|
|
101
|
+
|
|
102
|
+
if (sourceSelect) {
|
|
103
|
+
sourceSelect.addEventListener("change", function() {
|
|
104
|
+
fileField.style.display = this.value === "api" ? "none" : "";
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (fileInput) {
|
|
109
|
+
fileInput.addEventListener("change", function() {
|
|
110
|
+
if (this.files.length > 0) {
|
|
111
|
+
fileName.textContent = this.files[0].name;
|
|
112
|
+
fileName.style.display = "";
|
|
113
|
+
dropzone.classList.add("dp-dropzone--has-file");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (dropzone) {
|
|
119
|
+
dropzone.addEventListener("dragover", function(e) {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
this.classList.add("dp-dropzone--dragover");
|
|
122
|
+
});
|
|
123
|
+
dropzone.addEventListener("dragleave", function() {
|
|
124
|
+
this.classList.remove("dp-dropzone--dragover");
|
|
125
|
+
});
|
|
126
|
+
dropzone.addEventListener("drop", function(e) {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
this.classList.remove("dp-dropzone--dragover");
|
|
129
|
+
if (e.dataTransfer.files.length > 0) {
|
|
130
|
+
fileInput.files = e.dataTransfer.files;
|
|
131
|
+
fileInput.dispatchEvent(new Event("change"));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
document.addEventListener("keydown", function(e) {
|
|
137
|
+
if (e.key === "Escape") {
|
|
138
|
+
document.getElementById("dp-modal").classList.remove("dp-modal--open");
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
})();
|
|
142
|
+
</script>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<%= stylesheet_link_tag "data_porter/application" %>
|
|
2
|
+
|
|
3
|
+
<div class="data-porter">
|
|
4
|
+
<div class="dp-header">
|
|
5
|
+
<h1 class="dp-title">New Import</h1>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<%= form_with model: @import, url: imports_path, class: "dp-form", multipart: true do |f| %>
|
|
9
|
+
<div class="dp-field">
|
|
10
|
+
<%= f.label :target_key, "Target", class: "dp-label" %>
|
|
11
|
+
<%= f.select :target_key,
|
|
12
|
+
@targets.map { |t| [t[:label], t[:key]] },
|
|
13
|
+
{ prompt: "Select a target..." },
|
|
14
|
+
class: "dp-select" %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="dp-field">
|
|
18
|
+
<%= f.label :source_type, "Source Type", class: "dp-label" %>
|
|
19
|
+
<%= f.select :source_type,
|
|
20
|
+
DataPorter.configuration.enabled_sources.map { |s| [s.to_s.upcase, s] },
|
|
21
|
+
{ prompt: "Select source type..." },
|
|
22
|
+
id: "dp-source-select-new",
|
|
23
|
+
class: "dp-select" %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div id="dp-file-field-new" class="dp-field">
|
|
27
|
+
<%= f.label :file, "File", class: "dp-label" %>
|
|
28
|
+
<label class="dp-dropzone" id="dp-dropzone-new">
|
|
29
|
+
<input type="file" name="data_import[file]" id="dp-file-input-new" class="dp-dropzone__input" />
|
|
30
|
+
<div class="dp-dropzone__content">
|
|
31
|
+
<div class="dp-dropzone__icon">📄</div>
|
|
32
|
+
<span class="dp-dropzone__text">Drop your file here or <strong>browse</strong></span>
|
|
33
|
+
<span class="dp-dropzone__hint">CSV or JSON files accepted</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="dp-dropzone__selected" id="dp-file-name-new" style="display: none;"></div>
|
|
36
|
+
</label>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="dp-actions">
|
|
40
|
+
<%= f.submit "Start Import", class: "dp-btn dp-btn--primary" %>
|
|
41
|
+
<%= link_to "Cancel", imports_path, class: "dp-btn dp-btn--secondary" %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<script>
|
|
47
|
+
(function() {
|
|
48
|
+
var sourceSelect = document.getElementById("dp-source-select-new");
|
|
49
|
+
var fileField = document.getElementById("dp-file-field-new");
|
|
50
|
+
var fileInput = document.getElementById("dp-file-input-new");
|
|
51
|
+
var dropzone = document.getElementById("dp-dropzone-new");
|
|
52
|
+
var fileName = document.getElementById("dp-file-name-new");
|
|
53
|
+
|
|
54
|
+
if (sourceSelect) {
|
|
55
|
+
sourceSelect.addEventListener("change", function() {
|
|
56
|
+
fileField.style.display = this.value === "api" ? "none" : "";
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (fileInput) {
|
|
61
|
+
fileInput.addEventListener("change", function() {
|
|
62
|
+
if (this.files.length > 0) {
|
|
63
|
+
fileName.textContent = this.files[0].name;
|
|
64
|
+
fileName.style.display = "";
|
|
65
|
+
dropzone.classList.add("dp-dropzone--has-file");
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (dropzone) {
|
|
71
|
+
dropzone.addEventListener("dragover", function(e) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
this.classList.add("dp-dropzone--dragover");
|
|
74
|
+
});
|
|
75
|
+
dropzone.addEventListener("dragleave", function() {
|
|
76
|
+
this.classList.remove("dp-dropzone--dragover");
|
|
77
|
+
});
|
|
78
|
+
dropzone.addEventListener("drop", function(e) {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
this.classList.remove("dp-dropzone--dragover");
|
|
81
|
+
if (e.dataTransfer.files.length > 0) {
|
|
82
|
+
fileInput.files = e.dataTransfer.files;
|
|
83
|
+
fileInput.dispatchEvent(new Event("change"));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<%= stylesheet_link_tag "data_porter/application" %>
|
|
2
|
+
|
|
3
|
+
<div class="data-porter">
|
|
4
|
+
<div class="dp-header">
|
|
5
|
+
<h1 class="dp-title">
|
|
6
|
+
<%= @target._label %> Import #<%= @import.id %>
|
|
7
|
+
</h1>
|
|
8
|
+
<%= raw DataPorter::Components::StatusBadge.new(status: @import.status).call %>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<% if @import.parsing? || @import.importing? || @import.dry_running? %>
|
|
12
|
+
<%= raw DataPorter::Components::ProgressBar.new(import_id: @import.id).call %>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<% if @import.previewing? %>
|
|
16
|
+
<%= raw DataPorter::Components::SummaryCards.new(report: @import.report).call %>
|
|
17
|
+
<%= raw DataPorter::Components::PreviewTable.new(
|
|
18
|
+
columns: @target._columns,
|
|
19
|
+
records: @records
|
|
20
|
+
).call %>
|
|
21
|
+
|
|
22
|
+
<div class="dp-actions">
|
|
23
|
+
<%= button_to "Confirm", confirm_import_path(@import),
|
|
24
|
+
method: :post, class: "dp-btn dp-btn--primary" %>
|
|
25
|
+
<% if @target._dry_run_enabled %>
|
|
26
|
+
<%= button_to "Dry Run", dry_run_import_path(@import),
|
|
27
|
+
method: :post, class: "dp-btn dp-btn--secondary" %>
|
|
28
|
+
<% end %>
|
|
29
|
+
<%= button_to "Cancel", cancel_import_path(@import),
|
|
30
|
+
method: :post, class: "dp-btn dp-btn--danger" %>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<% if @import.completed? %>
|
|
35
|
+
<%= raw DataPorter::Components::ResultsSummary.new(report: @import.report).call %>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
<% if @import.failed? %>
|
|
39
|
+
<%= raw DataPorter::Components::FailureAlert.new(report: @import.report).call %>
|
|
40
|
+
<div class="dp-actions">
|
|
41
|
+
<%= button_to "Retry", parse_import_path(@import),
|
|
42
|
+
method: :post, class: "dp-btn dp-btn--primary" %>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<div class="dp-nav">
|
|
47
|
+
<%= link_to "Back to imports", imports_path, class: "dp-link" %>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
data/config/database.yml
ADDED