data_porter 0.4.0 → 0.9.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 +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +12 -6
- data/app/assets/javascripts/data_porter/import_form_controller.js +126 -0
- data/app/assets/javascripts/data_porter/progress_controller.js +42 -0
- data/app/assets/stylesheets/data_porter/alerts.css +62 -4
- data/app/assets/stylesheets/data_porter/layout.css +19 -0
- data/app/assets/stylesheets/data_porter/progress.css +37 -12
- data/app/assets/stylesheets/data_porter/table.css +51 -0
- data/app/controllers/data_porter/concerns/import_validation.rb +47 -0
- data/app/controllers/data_porter/concerns/mapping_management.rb +43 -0
- data/app/controllers/data_porter/concerns/record_pagination.rb +19 -0
- data/app/controllers/data_porter/imports_controller.rb +28 -40
- data/app/models/data_porter/data_import.rb +5 -0
- data/app/views/data_porter/imports/index.html.erb +67 -95
- data/app/views/data_porter/imports/new.html.erb +71 -1
- data/app/views/data_porter/imports/show.html.erb +46 -7
- data/app/views/layouts/data_porter/application.html.erb +17 -140
- data/config/routes.rb +2 -1
- data/docs/CONFIGURATION.md +28 -6
- data/docs/ROADMAP.md +28 -0
- data/docs/TARGETS.md +54 -3
- data/lib/data_porter/components/preview/results_summary.rb +43 -4
- data/lib/data_porter/components/progress/bar.rb +18 -6
- data/lib/data_porter/components/shared/pagination.rb +53 -0
- data/lib/data_porter/components.rb +1 -0
- data/lib/data_porter/configuration.rb +3 -1
- data/lib/data_porter/dsl/param.rb +32 -0
- data/lib/data_porter/engine.rb +4 -0
- data/lib/data_porter/orchestrator/dry_runner.rb +30 -0
- data/lib/data_porter/orchestrator/importer.rb +41 -0
- data/lib/data_porter/orchestrator/record_builder.rb +38 -0
- data/lib/data_porter/orchestrator.rb +26 -80
- data/lib/data_porter/registry.rb +26 -1
- data/lib/data_porter/target.rb +17 -1
- data/lib/data_porter/version.rb +1 -1
- data/lib/generators/data_porter/install/templates/initializer.rb +4 -0
- data/lib/generators/data_porter/target/target_generator.rb +5 -0
- data/lib/generators/data_porter/target/templates/target.rb.tt +1 -1
- data/lib/tasks/data_porter.rake +9 -0
- metadata +15 -4
- data/app/javascript/data_porter/progress_controller.js +0 -33
- /data/app/{javascript → assets/javascripts}/data_porter/mapping_controller.js +0 -0
- /data/app/{javascript → assets/javascripts}/data_porter/template_form_controller.js +0 -0
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module DataPorter
|
|
4
4
|
class ImportsController < DataPorter.configuration.parent_controller.constantize
|
|
5
|
+
include Concerns::ImportValidation
|
|
6
|
+
include Concerns::MappingManagement
|
|
7
|
+
include Concerns::RecordPagination
|
|
8
|
+
|
|
5
9
|
layout "data_porter/application"
|
|
6
10
|
|
|
7
|
-
before_action :set_import, only: %i[show parse confirm cancel dry_run update_mapping]
|
|
11
|
+
before_action :set_import, only: %i[show parse confirm cancel dry_run update_mapping status destroy]
|
|
8
12
|
before_action :load_targets, only: %i[index new create]
|
|
9
13
|
|
|
10
14
|
def index
|
|
@@ -18,7 +22,7 @@ module DataPorter
|
|
|
18
22
|
def create
|
|
19
23
|
build_import
|
|
20
24
|
|
|
21
|
-
if valid_file_presence? && @import.save
|
|
25
|
+
if valid_source_for_target? && valid_file_presence? && valid_import_params? && @import.save
|
|
22
26
|
enqueue_after_create
|
|
23
27
|
redirect_to import_path(@import)
|
|
24
28
|
else
|
|
@@ -30,6 +34,7 @@ module DataPorter
|
|
|
30
34
|
@target = @import.target_class
|
|
31
35
|
@records = @import.records
|
|
32
36
|
@grouped = @records.group_by(&:status)
|
|
37
|
+
paginate_records
|
|
33
38
|
load_mapping_data if @import.mapping?
|
|
34
39
|
end
|
|
35
40
|
|
|
@@ -47,6 +52,7 @@ module DataPorter
|
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
def confirm
|
|
55
|
+
@import.update!(status: :pending)
|
|
50
56
|
DataPorter::ImportJob.perform_later(@import.id)
|
|
51
57
|
redirect_to import_path(@import)
|
|
52
58
|
end
|
|
@@ -57,10 +63,22 @@ module DataPorter
|
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
def dry_run
|
|
66
|
+
@import.update!(status: :pending)
|
|
60
67
|
DataPorter::DryRunJob.perform_later(@import.id)
|
|
61
68
|
redirect_to import_path(@import)
|
|
62
69
|
end
|
|
63
70
|
|
|
71
|
+
def status
|
|
72
|
+
progress = @import.config["progress"] || {}
|
|
73
|
+
render json: { status: @import.status, progress: progress }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def destroy
|
|
77
|
+
@import.file.purge if @import.file.attached?
|
|
78
|
+
@import.destroy!
|
|
79
|
+
redirect_to imports_path
|
|
80
|
+
end
|
|
81
|
+
|
|
64
82
|
private
|
|
65
83
|
|
|
66
84
|
def set_import
|
|
@@ -78,15 +96,17 @@ module DataPorter
|
|
|
78
96
|
end
|
|
79
97
|
|
|
80
98
|
def import_params
|
|
81
|
-
params.require(:data_import).permit(:target_key, :source_type, :file, config: {})
|
|
99
|
+
permitted = params.require(:data_import).permit(:target_key, :source_type, :file, config: {})
|
|
100
|
+
merge_import_params(permitted)
|
|
82
101
|
end
|
|
83
102
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
return
|
|
103
|
+
def merge_import_params(permitted)
|
|
104
|
+
nested = params.dig(:data_import, :config, :import_params)
|
|
105
|
+
return permitted unless nested
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
config = permitted[:config] || {}
|
|
108
|
+
config["import_params"] = nested.permit!.to_h
|
|
109
|
+
permitted.merge(config: config)
|
|
90
110
|
end
|
|
91
111
|
|
|
92
112
|
def enqueue_after_create
|
|
@@ -96,37 +116,5 @@ module DataPorter
|
|
|
96
116
|
DataPorter::ParseJob.perform_later(@import.id)
|
|
97
117
|
end
|
|
98
118
|
end
|
|
99
|
-
|
|
100
|
-
def load_mapping_data
|
|
101
|
-
target = @import.target_class
|
|
102
|
-
columns = target._columns || []
|
|
103
|
-
@file_headers = @import.config["file_headers"] || []
|
|
104
|
-
@target_columns = columns.map { |c| [c.label, c.name.to_s, c.required] }
|
|
105
|
-
@default_mapping = (target._csv_mappings || {}).transform_values(&:to_s)
|
|
106
|
-
@templates = load_templates
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def load_templates
|
|
110
|
-
return [] unless defined?(DataPorter::MappingTemplate)
|
|
111
|
-
|
|
112
|
-
DataPorter::MappingTemplate.for_target(@import.target_key)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def save_column_mapping
|
|
116
|
-
mapping = params.require(:column_mapping).permit!.to_h
|
|
117
|
-
merged = (@import.config || {}).merge("column_mapping" => mapping)
|
|
118
|
-
@import.update!(config: merged, status: :pending)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def save_template_if_requested
|
|
122
|
-
return unless params[:save_template] == "1"
|
|
123
|
-
return unless defined?(DataPorter::MappingTemplate)
|
|
124
|
-
|
|
125
|
-
mapping = params.require(:column_mapping).permit!.to_h
|
|
126
|
-
DataPorter::MappingTemplate.find_or_initialize_by(
|
|
127
|
-
target_key: @import.target_key,
|
|
128
|
-
name: params[:template_name].presence || "Default"
|
|
129
|
-
).update!(mapping: mapping)
|
|
130
|
-
end
|
|
131
119
|
end
|
|
132
120
|
end
|
|
@@ -25,6 +25,11 @@ module DataPorter
|
|
|
25
25
|
|
|
26
26
|
attribute :config, :json, default: -> { {} }
|
|
27
27
|
|
|
28
|
+
scope :purgeable, lambda {
|
|
29
|
+
where(status: %i[completed failed])
|
|
30
|
+
.where(created_at: ...DataPorter.configuration.purge_after.ago)
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
validates :target_key, presence: true
|
|
29
34
|
validates :source_type, presence: true, inclusion: { in: %w[csv json api xlsx] }
|
|
30
35
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
<div class="data-porter"
|
|
1
|
+
<div class="data-porter" data-controller="data-porter--import-form"
|
|
2
|
+
data-data-porter--import-form-sources-value="<%= @targets.map { |t| [t[:key], t[:sources]] }.to_h.to_json %>"
|
|
3
|
+
data-data-porter--import-form-params-value="<%= @targets.map { |t| [t[:key], t[:params]] }.to_h.to_json %>"
|
|
4
|
+
data-action="keydown@document->data-porter--import-form#closeModal">
|
|
2
5
|
<div class="dp-header">
|
|
3
6
|
<h1 class="dp-title">Imports</h1>
|
|
4
7
|
<div class="dp-header__actions">
|
|
5
8
|
<%= link_to "Mapping Templates", mapping_templates_path, class: "dp-btn dp-btn--secondary" %>
|
|
6
|
-
<button type="button" class="dp-btn dp-btn--primary"
|
|
9
|
+
<button type="button" class="dp-btn dp-btn--primary" data-action="data-porter--import-form#openModal">
|
|
7
10
|
New Import
|
|
8
11
|
</button>
|
|
9
12
|
</div>
|
|
@@ -29,7 +32,14 @@
|
|
|
29
32
|
<td><%= import.source_type %></td>
|
|
30
33
|
<td><%= raw DataPorter::Components::Shared::StatusBadge.new(status: import.status).call %></td>
|
|
31
34
|
<td><%= import.created_at&.strftime("%Y-%m-%d %H:%M") %></td>
|
|
32
|
-
<td
|
|
35
|
+
<td class="dp-table__actions">
|
|
36
|
+
<%= link_to "View", import_path(import), class: "dp-btn dp-btn--sm dp-btn--secondary" %>
|
|
37
|
+
<% if import.completed? || import.failed? %>
|
|
38
|
+
<%= button_to "Delete", import_path(import),
|
|
39
|
+
method: :delete, class: "dp-btn dp-btn--sm dp-btn--danger",
|
|
40
|
+
data: { turbo_confirm: "Delete this import?" } %>
|
|
41
|
+
<% end %>
|
|
42
|
+
</td>
|
|
33
43
|
</tr>
|
|
34
44
|
<% end %>
|
|
35
45
|
</tbody>
|
|
@@ -38,106 +48,68 @@
|
|
|
38
48
|
<div class="dp-empty-state">
|
|
39
49
|
<div class="dp-empty-state__icon">📦</div>
|
|
40
50
|
<p class="dp-empty-state__text">No imports yet</p>
|
|
41
|
-
<button type="button" class="dp-btn dp-btn--primary"
|
|
51
|
+
<button type="button" class="dp-btn dp-btn--primary" data-action="data-porter--import-form#openModal">
|
|
42
52
|
Create your first import
|
|
43
53
|
</button>
|
|
44
54
|
</div>
|
|
45
55
|
<% end %>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<div id="dp-modal" class="dp-modal">
|
|
49
|
-
<div class="dp-modal__backdrop" onclick="document.getElementById('dp-modal').classList.remove('dp-modal--open')"></div>
|
|
50
|
-
<div class="dp-modal__content">
|
|
51
|
-
<div class="dp-modal__header">
|
|
52
|
-
<h2 class="dp-modal__title">New Import</h2>
|
|
53
|
-
<button type="button" class="dp-modal__close" onclick="document.getElementById('dp-modal').classList.remove('dp-modal--open')">×</button>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<%= form_with model: DataPorter::DataImport.new, url: imports_path, class: "dp-modal__body", multipart: true, data: { turbo: false } do |f| %>
|
|
57
|
-
<div class="dp-field">
|
|
58
|
-
<%= f.label :target_key, "Target", class: "dp-label" %>
|
|
59
|
-
<%= f.select :target_key,
|
|
60
|
-
@targets.map { |t| [t[:label], t[:key]] },
|
|
61
|
-
{ prompt: "Select a target..." },
|
|
62
|
-
class: "dp-select" %>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div class="dp-field">
|
|
66
|
-
<%= f.label :source_type, "Source Type", class: "dp-label" %>
|
|
67
|
-
<%= f.select :source_type,
|
|
68
|
-
DataPorter.configuration.enabled_sources.map { |s| [s.to_s.upcase, s] },
|
|
69
|
-
{ prompt: "Select source type..." },
|
|
70
|
-
id: "dp-source-select",
|
|
71
|
-
class: "dp-select" %>
|
|
72
|
-
</div>
|
|
73
56
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<span class="dp-dropzone__text">Drop your file here or <strong>browse</strong></span>
|
|
81
|
-
<span class="dp-dropzone__hint">CSV, JSON, or XLSX files accepted</span>
|
|
82
|
-
</div>
|
|
83
|
-
<div class="dp-dropzone__selected" id="dp-file-name" style="display: none;"></div>
|
|
84
|
-
</label>
|
|
57
|
+
<div id="dp-modal" class="dp-modal" data-data-porter--import-form-target="modal">
|
|
58
|
+
<div class="dp-modal__backdrop" data-action="click->data-porter--import-form#closeModalClick"></div>
|
|
59
|
+
<div class="dp-modal__content">
|
|
60
|
+
<div class="dp-modal__header">
|
|
61
|
+
<h2 class="dp-modal__title">New Import</h2>
|
|
62
|
+
<button type="button" class="dp-modal__close" data-action="data-porter--import-form#closeModalClick">×</button>
|
|
85
63
|
</div>
|
|
86
64
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
var fileInput = document.getElementById("dp-file-input");
|
|
100
|
-
var dropzone = document.getElementById("dp-dropzone");
|
|
101
|
-
var fileName = document.getElementById("dp-file-name");
|
|
65
|
+
<%= form_with model: DataPorter::DataImport.new, url: imports_path, class: "dp-modal__body", multipart: true, data: { turbo: false } do |f| %>
|
|
66
|
+
<div class="dp-field">
|
|
67
|
+
<%= f.label :target_key, "Target", class: "dp-label" %>
|
|
68
|
+
<%= f.select :target_key,
|
|
69
|
+
@targets.map { |t| [t[:label], t[:key]] },
|
|
70
|
+
{ prompt: "Select a target..." },
|
|
71
|
+
class: "dp-select",
|
|
72
|
+
data: {
|
|
73
|
+
data_porter__import_form_target: "targetSelect",
|
|
74
|
+
action: "data-porter--import-form#filterSources"
|
|
75
|
+
} %>
|
|
76
|
+
</div>
|
|
102
77
|
|
|
103
|
-
|
|
104
|
-
sourceSelect.addEventListener("change", function() {
|
|
105
|
-
fileField.style.display = this.value === "api" ? "none" : "";
|
|
106
|
-
});
|
|
107
|
-
}
|
|
78
|
+
<div data-data-porter--import-form-target="paramsContainer"></div>
|
|
108
79
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
80
|
+
<div class="dp-field">
|
|
81
|
+
<%= f.label :source_type, "Source Type", class: "dp-label" %>
|
|
82
|
+
<%= f.select :source_type,
|
|
83
|
+
DataPorter.configuration.enabled_sources.map { |s| [s.to_s.upcase, s] },
|
|
84
|
+
{ prompt: "Select source type..." },
|
|
85
|
+
class: "dp-select",
|
|
86
|
+
data: {
|
|
87
|
+
data_porter__import_form_target: "sourceSelect",
|
|
88
|
+
action: "data-porter--import-form#toggleFileField"
|
|
89
|
+
} %>
|
|
90
|
+
</div>
|
|
118
91
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
135
|
-
}
|
|
92
|
+
<div class="dp-field" data-data-porter--import-form-target="fileField">
|
|
93
|
+
<%= f.label :file, "File", class: "dp-label" %>
|
|
94
|
+
<label class="dp-dropzone" data-data-porter--import-form-target="dropzone"
|
|
95
|
+
data-action="dragover->data-porter--import-form#dragover dragleave->data-porter--import-form#dragleave drop->data-porter--import-form#drop">
|
|
96
|
+
<input type="file" name="data_import[file]" class="dp-dropzone__input"
|
|
97
|
+
data-data-porter--import-form-target="fileInput"
|
|
98
|
+
data-action="data-porter--import-form#handleFile" />
|
|
99
|
+
<div class="dp-dropzone__content">
|
|
100
|
+
<div class="dp-dropzone__icon">📄</div>
|
|
101
|
+
<span class="dp-dropzone__text">Drop your file here or <strong>browse</strong></span>
|
|
102
|
+
<span class="dp-dropzone__hint">CSV, JSON, or XLSX files accepted</span>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="dp-dropzone__selected" data-data-porter--import-form-target="fileName" style="display: none;"></div>
|
|
105
|
+
</label>
|
|
106
|
+
</div>
|
|
136
107
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</
|
|
108
|
+
<div class="dp-modal__footer">
|
|
109
|
+
<%= f.submit "Start Import", class: "dp-btn dp-btn--primary" %>
|
|
110
|
+
<button type="button" class="dp-btn dp-btn--secondary" data-action="data-porter--import-form#closeModalClick">Cancel</button>
|
|
111
|
+
</div>
|
|
112
|
+
<% end %>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
@@ -17,9 +17,16 @@
|
|
|
17
17
|
<%= f.select :target_key,
|
|
18
18
|
@targets.map { |t| [t[:label], t[:key]] },
|
|
19
19
|
{ prompt: "Select a target..." },
|
|
20
|
-
|
|
20
|
+
id: "dp-target-select-new",
|
|
21
|
+
class: "dp-select",
|
|
22
|
+
data: {
|
|
23
|
+
sources: @targets.map { |t| [t[:key], t[:sources]] }.to_h.to_json,
|
|
24
|
+
params: @targets.map { |t| [t[:key], t[:params]] }.to_h.to_json
|
|
25
|
+
} %>
|
|
21
26
|
</div>
|
|
22
27
|
|
|
28
|
+
<div id="dp-params-container" class="dp-params-container"></div>
|
|
29
|
+
|
|
23
30
|
<div class="dp-field">
|
|
24
31
|
<%= f.label :source_type, "Source Type", class: "dp-label" %>
|
|
25
32
|
<%= f.select :source_type,
|
|
@@ -51,11 +58,74 @@
|
|
|
51
58
|
|
|
52
59
|
<script>
|
|
53
60
|
(function() {
|
|
61
|
+
var targetSelect = document.getElementById("dp-target-select-new");
|
|
54
62
|
var sourceSelect = document.getElementById("dp-source-select-new");
|
|
55
63
|
var fileField = document.getElementById("dp-file-field-new");
|
|
56
64
|
var fileInput = document.getElementById("dp-file-input-new");
|
|
57
65
|
var dropzone = document.getElementById("dp-dropzone-new");
|
|
58
66
|
var fileName = document.getElementById("dp-file-name-new");
|
|
67
|
+
var paramsContainer = document.getElementById("dp-params-container");
|
|
68
|
+
var paramsData = JSON.parse((targetSelect && targetSelect.dataset.params) || "{}");
|
|
69
|
+
|
|
70
|
+
function renderParams() {
|
|
71
|
+
if (!paramsContainer || !targetSelect) return;
|
|
72
|
+
paramsContainer.innerHTML = "";
|
|
73
|
+
var defs = paramsData[targetSelect.value] || [];
|
|
74
|
+
defs.forEach(function(p) {
|
|
75
|
+
var div = document.createElement("div");
|
|
76
|
+
div.className = "dp-field";
|
|
77
|
+
var label = document.createElement("label");
|
|
78
|
+
label.className = "dp-label";
|
|
79
|
+
label.textContent = p.label + (p.required ? " *" : "");
|
|
80
|
+
div.appendChild(label);
|
|
81
|
+
var input;
|
|
82
|
+
if (p.type === "select" && p.collection) {
|
|
83
|
+
input = document.createElement("select");
|
|
84
|
+
input.className = "dp-select";
|
|
85
|
+
var blank = document.createElement("option");
|
|
86
|
+
blank.value = "";
|
|
87
|
+
blank.textContent = "Select...";
|
|
88
|
+
input.appendChild(blank);
|
|
89
|
+
p.collection.forEach(function(opt) {
|
|
90
|
+
var o = document.createElement("option");
|
|
91
|
+
o.textContent = opt[0];
|
|
92
|
+
o.value = opt[1];
|
|
93
|
+
if (p["default"] && String(opt[1]) === String(p["default"])) o.selected = true;
|
|
94
|
+
input.appendChild(o);
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
input = document.createElement("input");
|
|
98
|
+
input.className = "dp-input";
|
|
99
|
+
input.type = p.type === "number" ? "number" : (p.type === "hidden" ? "hidden" : "text");
|
|
100
|
+
if (p["default"]) input.value = p["default"];
|
|
101
|
+
}
|
|
102
|
+
input.name = "data_import[config][import_params][" + p.name + "]";
|
|
103
|
+
if (p.required) input.required = true;
|
|
104
|
+
div.appendChild(input);
|
|
105
|
+
paramsContainer.appendChild(div);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function filterSources() {
|
|
110
|
+
if (!targetSelect || !sourceSelect) return;
|
|
111
|
+
var sourcesMap = JSON.parse(targetSelect.dataset.sources || "{}");
|
|
112
|
+
var allowed = sourcesMap[targetSelect.value];
|
|
113
|
+
var options = sourceSelect.options;
|
|
114
|
+
for (var i = 1; i < options.length; i++) {
|
|
115
|
+
options[i].style.display = allowed && allowed.indexOf(options[i].value) === -1 ? "none" : "";
|
|
116
|
+
}
|
|
117
|
+
if (allowed && sourceSelect.selectedIndex > 0 && allowed.indexOf(sourceSelect.value) === -1) {
|
|
118
|
+
sourceSelect.selectedIndex = 0;
|
|
119
|
+
fileField.style.display = "";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (targetSelect) {
|
|
124
|
+
targetSelect.addEventListener("change", function() {
|
|
125
|
+
filterSources();
|
|
126
|
+
renderParams();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
59
129
|
|
|
60
130
|
if (sourceSelect) {
|
|
61
131
|
sourceSelect.addEventListener("change", function() {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
</dl>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
|
-
<% if @import.parsing? || @import.importing? || @import.dry_running? || @import.extracting_headers? %>
|
|
32
|
-
<%= raw DataPorter::Components::Progress::Bar.new(import_id: @import.id).call %>
|
|
31
|
+
<% if @import.pending? || @import.parsing? || @import.importing? || @import.dry_running? || @import.extracting_headers? %>
|
|
32
|
+
<%= raw DataPorter::Components::Progress::Bar.new(import_id: @import.id, status_url: status_import_path(@import)).call %>
|
|
33
33
|
<% end %>
|
|
34
34
|
|
|
35
35
|
<% if @import.mapping? %>
|
|
@@ -45,18 +45,25 @@
|
|
|
45
45
|
<% end %>
|
|
46
46
|
|
|
47
47
|
<% if @import.previewing? %>
|
|
48
|
+
<div id="records">
|
|
48
49
|
<%= raw DataPorter::Components::Preview::SummaryCards.new(report: @import.report).call %>
|
|
49
50
|
<%= raw DataPorter::Components::Preview::Table.new(
|
|
50
51
|
columns: @target._columns,
|
|
51
52
|
records: @records
|
|
52
53
|
).call %>
|
|
54
|
+
</div>
|
|
55
|
+
<%= raw DataPorter::Components::Shared::Pagination.new(
|
|
56
|
+
page: @page, total_pages: @total_pages, base_url: import_path(@import)
|
|
57
|
+
).call %>
|
|
53
58
|
|
|
54
59
|
<div class="dp-actions">
|
|
55
|
-
<%= button_to
|
|
56
|
-
|
|
60
|
+
<%= button_to confirm_import_path(@import), method: :post, class: "dp-btn dp-btn--primary", data: { dp_submit: true } do %>
|
|
61
|
+
Confirm Import
|
|
62
|
+
<% end %>
|
|
57
63
|
<% if @target._dry_run_enabled %>
|
|
58
|
-
<%= button_to
|
|
59
|
-
|
|
64
|
+
<%= button_to dry_run_import_path(@import), method: :post, class: "dp-btn dp-btn--secondary", data: { dp_submit: true } do %>
|
|
65
|
+
Dry Run
|
|
66
|
+
<% end %>
|
|
60
67
|
<% end %>
|
|
61
68
|
<%= button_to "Cancel", cancel_import_path(@import),
|
|
62
69
|
method: :post, class: "dp-btn dp-btn--danger" %>
|
|
@@ -64,7 +71,25 @@
|
|
|
64
71
|
<% end %>
|
|
65
72
|
|
|
66
73
|
<% if @import.completed? %>
|
|
67
|
-
|
|
74
|
+
<% duration = @import.updated_at && @import.created_at ? distance_of_time_in_words(@import.created_at, @import.updated_at) : nil %>
|
|
75
|
+
<%= raw DataPorter::Components::Preview::ResultsSummary.new(report: @import.report, duration: duration).call %>
|
|
76
|
+
<% if @records.any? %>
|
|
77
|
+
<div id="records">
|
|
78
|
+
<%= raw DataPorter::Components::Preview::Table.new(
|
|
79
|
+
columns: @target._columns,
|
|
80
|
+
records: @records
|
|
81
|
+
).call %>
|
|
82
|
+
</div>
|
|
83
|
+
<%= raw DataPorter::Components::Shared::Pagination.new(
|
|
84
|
+
page: @page, total_pages: @total_pages, base_url: import_path(@import)
|
|
85
|
+
).call %>
|
|
86
|
+
<% end %>
|
|
87
|
+
<div class="dp-actions">
|
|
88
|
+
<%= link_to "Back to imports", imports_path, class: "dp-btn dp-btn--primary" %>
|
|
89
|
+
<%= button_to "Delete", import_path(@import),
|
|
90
|
+
method: :delete, class: "dp-btn dp-btn--danger",
|
|
91
|
+
data: { turbo_confirm: "Delete this import?" } %>
|
|
92
|
+
</div>
|
|
68
93
|
<% end %>
|
|
69
94
|
|
|
70
95
|
<% if @import.failed? %>
|
|
@@ -72,6 +97,20 @@
|
|
|
72
97
|
<div class="dp-actions">
|
|
73
98
|
<%= button_to "Retry", parse_import_path(@import),
|
|
74
99
|
method: :post, class: "dp-btn dp-btn--primary" %>
|
|
100
|
+
<%= button_to "Delete", import_path(@import),
|
|
101
|
+
method: :delete, class: "dp-btn dp-btn--danger",
|
|
102
|
+
data: { turbo_confirm: "Delete this import?" } %>
|
|
75
103
|
</div>
|
|
76
104
|
<% end %>
|
|
77
105
|
</div>
|
|
106
|
+
|
|
107
|
+
<script>
|
|
108
|
+
(function() {
|
|
109
|
+
document.querySelectorAll("[data-dp-submit]").forEach(function(btn) {
|
|
110
|
+
btn.closest("form").addEventListener("submit", function() {
|
|
111
|
+
btn.disabled = true;
|
|
112
|
+
btn.innerHTML = '<span class="dp-spinner"></span>Processing...';
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
})();
|
|
116
|
+
</script>
|