retold-data-service 2.0.16 → 2.0.18

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 (28) hide show
  1. package/.claude/launch.json +2 -2
  2. package/.quackage.json +19 -0
  3. package/package.json +13 -6
  4. package/source/services/data-cloner/DataCloner-Command-Sync.js +83 -50
  5. package/source/services/data-cloner/DataCloner-Command-WebUI.js +27 -10
  6. package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +281 -4
  7. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner-Configuration.json +9 -0
  8. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +102 -0
  9. package/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js +6 -0
  10. package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +998 -0
  11. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +407 -0
  12. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +126 -0
  13. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +483 -0
  14. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +390 -0
  15. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Schema.js +241 -0
  16. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Session.js +268 -0
  17. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +575 -0
  18. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-ViewData.js +176 -0
  19. package/source/services/data-cloner/web/data-cloner.js +7952 -0
  20. package/source/services/data-cloner/web/data-cloner.js.map +1 -0
  21. package/source/services/data-cloner/web/data-cloner.min.js +2 -0
  22. package/source/services/data-cloner/web/data-cloner.min.js.map +1 -0
  23. package/source/services/data-cloner/web/index.html +17 -0
  24. package/test/DataCloner-Integration_tests.js +1205 -0
  25. package/test/DataCloner-Puppeteer_tests.js +502 -0
  26. package/test/integration-report.json +311 -0
  27. package/test/run-integration-tests.js +501 -0
  28. package/source/services/data-cloner/data-cloner-web.html +0 -2706
@@ -0,0 +1,390 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ class DataClonerLayoutView extends libPictView
4
+ {
5
+ constructor(pFable, pOptions, pServiceHash)
6
+ {
7
+ super(pFable, pOptions, pServiceHash);
8
+ }
9
+
10
+ onAfterRender()
11
+ {
12
+ // Render all section views into their containers
13
+ this.pict.views['DataCloner-Connection'].render();
14
+ this.pict.views['DataCloner-Session'].render();
15
+ this.pict.views['DataCloner-Schema'].render();
16
+ this.pict.views['DataCloner-Deploy'].render();
17
+ this.pict.views['DataCloner-Sync'].render();
18
+ this.pict.views['DataCloner-Export'].render();
19
+ this.pict.views['DataCloner-ViewData'].render();
20
+
21
+ this.pict.CSSMap.injectCSS();
22
+ }
23
+
24
+ toggleSection(pSectionId)
25
+ {
26
+ let tmpCard = document.getElementById(pSectionId);
27
+ if (!tmpCard) return;
28
+ tmpCard.classList.toggle('open');
29
+ }
30
+
31
+ expandAllSections()
32
+ {
33
+ let tmpCards = document.querySelectorAll('.accordion-card');
34
+ for (let i = 0; i < tmpCards.length; i++)
35
+ {
36
+ tmpCards[i].classList.add('open');
37
+ }
38
+ }
39
+
40
+ collapseAllSections()
41
+ {
42
+ let tmpCards = document.querySelectorAll('.accordion-card');
43
+ for (let i = 0; i < tmpCards.length; i++)
44
+ {
45
+ tmpCards[i].classList.remove('open');
46
+ }
47
+ }
48
+
49
+ toggleStatusDetail()
50
+ {
51
+ let tmpDetail = document.getElementById('liveStatusDetail');
52
+ let tmpMeta = document.getElementById('liveStatusMeta');
53
+ let tmpMessage = document.getElementById('liveStatusMessage');
54
+ let tmpToggle = document.getElementById('liveStatusToggle');
55
+ let tmpBar = document.getElementById('liveStatusBar');
56
+ if (!tmpDetail) return;
57
+
58
+ let tmpIsExpanded = tmpDetail.style.display !== 'none';
59
+
60
+ if (tmpIsExpanded)
61
+ {
62
+ tmpDetail.style.display = 'none';
63
+ tmpMeta.style.display = '';
64
+ tmpMessage.style.display = '';
65
+ tmpToggle.innerHTML = '&#9660;';
66
+ tmpBar.classList.remove('expanded');
67
+ this.pict.providers.DataCloner.onStatusDetailCollapsed();
68
+ }
69
+ else
70
+ {
71
+ tmpDetail.style.display = '';
72
+ tmpMeta.style.display = 'none';
73
+ tmpMessage.style.display = 'none';
74
+ tmpToggle.innerHTML = '&#9650;';
75
+ tmpBar.classList.add('expanded');
76
+ this.pict.providers.DataCloner.onStatusDetailExpanded();
77
+ }
78
+ }
79
+ }
80
+
81
+ module.exports = DataClonerLayoutView;
82
+
83
+ module.exports.default_configuration =
84
+ {
85
+ ViewIdentifier: 'DataCloner-Layout',
86
+ DefaultRenderable: 'DataCloner-Layout',
87
+ DefaultDestinationAddress: '#DataCloner-Application-Container',
88
+ CSS: /*css*/`
89
+ * { box-sizing: border-box; margin: 0; padding: 0; }
90
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 20px; }
91
+ h1 { margin-bottom: 20px; color: #1a1a1a; }
92
+ h2 { margin-bottom: 12px; color: #444; font-size: 1.2em; border-bottom: 2px solid #ddd; padding-bottom: 6px; }
93
+
94
+ .section { background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
95
+
96
+ /* Accordion layout */
97
+ .accordion-row { display: flex; gap: 0; margin-bottom: 16px; align-items: stretch; }
98
+ .accordion-number {
99
+ flex: 0 0 48px; display: flex; align-items: flex-start; justify-content: center;
100
+ padding-top: 16px; font-size: 1.6em; font-weight: 700; color: #4a90d9;
101
+ user-select: none;
102
+ }
103
+ .accordion-card {
104
+ flex: 1; background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
105
+ overflow: hidden; min-width: 0;
106
+ }
107
+ .accordion-header {
108
+ display: flex; align-items: center; padding: 14px 20px; cursor: pointer;
109
+ user-select: none; gap: 12px; transition: background 0.15s; line-height: 1.4;
110
+ }
111
+ .accordion-header:hover { background: #fafafa; }
112
+ .accordion-title { font-weight: 600; color: #333; font-size: 1.05em; white-space: nowrap; }
113
+ .accordion-preview { flex: 1; font-style: italic; color: #888; font-size: 0.9em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
114
+ .accordion-toggle {
115
+ flex: 0 0 20px; display: flex; align-items: center; justify-content: center;
116
+ border-radius: 4px; transition: background 0.15s, transform 0.25s; font-size: 0.7em; color: #888;
117
+ }
118
+ .accordion-header:hover .accordion-toggle { background: #eee; color: #555; }
119
+ .accordion-card.open .accordion-toggle { transform: rotate(180deg); }
120
+ .accordion-body { padding: 0 20px 20px; display: none; }
121
+ .accordion-card.open .accordion-body { display: block; }
122
+ .accordion-card.open .accordion-header { border-bottom: 1px solid #eee; }
123
+ .accordion-card.open .accordion-preview { display: none; }
124
+
125
+ /* Action controls (go link + auto checkbox) */
126
+ .accordion-actions { display: flex; align-items: baseline; gap: 8px; flex-shrink: 0; }
127
+ .accordion-card.open .accordion-actions { display: none; }
128
+ .accordion-go {
129
+ font-size: 0.82em; color: #4a90d9; cursor: pointer; text-decoration: none;
130
+ font-weight: 500; white-space: nowrap; padding: 2px 6px; border-radius: 3px;
131
+ transition: background 0.15s;
132
+ }
133
+ .accordion-go:hover { background: #e8f0fe; text-decoration: underline; }
134
+ .accordion-auto {
135
+ font-size: 0.82em; color: #999; white-space: nowrap; cursor: pointer;
136
+ }
137
+ .accordion-auto .auto-label { display: none; }
138
+ .accordion-auto:hover .auto-label { display: inline; }
139
+ .accordion-auto input[type="checkbox"] { width: auto; margin: 0; cursor: pointer; vertical-align: middle; position: relative; top: 0px; opacity: 0.75; transition: opacity 0.15s; }
140
+ .accordion-auto:hover input[type="checkbox"] { opacity: 1; }
141
+ .accordion-auto:hover { color: #666; }
142
+
143
+ /* Phase status indicator */
144
+ .accordion-phase {
145
+ flex: 0 0 auto; display: none; align-items: center; justify-content: center;
146
+ font-size: 0.85em; line-height: 1;
147
+ }
148
+ .accordion-phase.visible { display: flex; }
149
+ .accordion-phase-ok { color: #28a745; }
150
+ .accordion-phase-error { color: #dc3545; }
151
+ .accordion-phase-busy { color: #28a745; }
152
+ .accordion-phase-busy .phase-spinner {
153
+ display: inline-block; width: 14px; height: 14px;
154
+ border: 2px solid #28a745; border-top-color: transparent; border-radius: 50%;
155
+ animation: phase-spin 0.8s linear infinite; vertical-align: middle;
156
+ }
157
+ @keyframes phase-spin {
158
+ to { transform: rotate(360deg); }
159
+ }
160
+
161
+ .accordion-controls {
162
+ display: flex; gap: 8px; margin-bottom: 12px; justify-content: flex-end;
163
+ }
164
+ .accordion-controls button {
165
+ padding: 4px 10px; font-size: 0.82em; font-weight: 500; background: none;
166
+ border: 1px solid #ccc; border-radius: 4px; color: #666; cursor: pointer; margin: 0;
167
+ }
168
+ .accordion-controls button:hover { background: #f0f0f0; border-color: #aaa; color: #333; }
169
+
170
+ label { display: block; font-weight: 600; margin-bottom: 4px; font-size: 0.9em; }
171
+ input[type="text"], input[type="password"], input[type="number"] {
172
+ width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px;
173
+ font-size: 0.95em; margin-bottom: 10px;
174
+ }
175
+ input[type="text"]:focus, input[type="password"]:focus, input[type="number"]:focus {
176
+ outline: none; border-color: #4a90d9;
177
+ }
178
+
179
+ button {
180
+ padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;
181
+ font-size: 0.9em; font-weight: 600; margin-right: 8px; margin-bottom: 8px;
182
+ }
183
+ button.primary { background: #4a90d9; color: #fff; }
184
+ button.primary:hover { background: #357abd; }
185
+ button.secondary { background: #6c757d; color: #fff; }
186
+ button.secondary:hover { background: #5a6268; }
187
+ button.danger { background: #dc3545; color: #fff; }
188
+ button.danger:hover { background: #c82333; }
189
+ button.success { background: #28a745; color: #fff; }
190
+ button.success:hover { background: #218838; }
191
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
192
+
193
+ .status { padding: 8px 12px; border-radius: 4px; margin-top: 10px; font-size: 0.9em; }
194
+ .status.ok { background: #d4edda; color: #155724; }
195
+ .status.error { background: #f8d7da; color: #721c24; }
196
+ .status.info { background: #d1ecf1; color: #0c5460; }
197
+ .status.warn { background: #fff3cd; color: #856404; }
198
+
199
+ .inline-group { display: flex; gap: 8px; align-items: flex-end; margin-bottom: 10px; }
200
+ .inline-group > div { flex: 1; }
201
+
202
+ a { color: #4a90d9; }
203
+
204
+ select { background: #fff; width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; font-size: 0.95em; margin-bottom: 10px; }
205
+
206
+ .checkbox-row { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
207
+ .checkbox-row input[type="checkbox"] { width: auto; margin: 0; }
208
+ .checkbox-row label { display: inline; margin: 0; font-weight: normal; cursor: pointer; }
209
+
210
+ /* Live Status Bar */
211
+ .live-status-bar {
212
+ background: #fff; border-radius: 8px; margin-bottom: 16px;
213
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
214
+ position: sticky; top: 0; z-index: 100; border-left: 4px solid #6c757d;
215
+ }
216
+ .live-status-bar.phase-idle { border-left-color: #6c757d; }
217
+ .live-status-bar.phase-disconnected { border-left-color: #dc3545; }
218
+ .live-status-bar.phase-ready { border-left-color: #4a90d9; }
219
+ .live-status-bar.phase-syncing { border-left-color: #28a745; }
220
+ .live-status-bar.phase-stopping { border-left-color: #ffc107; }
221
+ .live-status-bar.phase-complete { border-left-color: #28a745; }
222
+
223
+ .live-status-dot {
224
+ width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0;
225
+ background: #6c757d;
226
+ }
227
+ .live-status-bar.phase-idle .live-status-dot { background: #6c757d; }
228
+ .live-status-bar.phase-disconnected .live-status-dot { background: #dc3545; }
229
+ .live-status-bar.phase-ready .live-status-dot { background: #4a90d9; }
230
+ .live-status-bar.phase-syncing .live-status-dot {
231
+ background: #28a745;
232
+ animation: live-pulse 1.5s ease-in-out infinite;
233
+ }
234
+ .live-status-bar.phase-stopping .live-status-dot {
235
+ background: #ffc107;
236
+ animation: live-pulse 0.8s ease-in-out infinite;
237
+ }
238
+ .live-status-bar.phase-complete .live-status-dot { background: #28a745; }
239
+
240
+ @keyframes live-pulse {
241
+ 0%, 100% { opacity: 1; transform: scale(1); }
242
+ 50% { opacity: 0.4; transform: scale(0.8); }
243
+ }
244
+
245
+ .live-status-message { flex: 1; font-size: 0.92em; color: #333; line-height: 1.4; }
246
+
247
+ .live-status-meta {
248
+ display: flex; gap: 16px; flex-shrink: 0; font-size: 0.82em; color: #666;
249
+ }
250
+ .live-status-meta-item { white-space: nowrap; }
251
+ .live-status-meta-item strong { color: #333; }
252
+
253
+ .live-status-progress-bar {
254
+ height: 3px; background: #e9ecef; border-radius: 2px; overflow: hidden;
255
+ position: absolute; bottom: 0; left: 0; right: 0;
256
+ }
257
+ .live-status-progress-fill {
258
+ height: 100%; background: #28a745; transition: width 1s ease;
259
+ }
260
+ /* Expandable status bar */
261
+ .live-status-header {
262
+ display: flex; align-items: center; gap: 14px; cursor: pointer;
263
+ padding: 14px 20px; user-select: none;
264
+ }
265
+ .live-status-bar.expanded .live-status-header {
266
+ border-bottom: 1px solid #e9ecef; padding-bottom: 10px;
267
+ }
268
+ .live-status-expand-toggle {
269
+ flex: 0 0 20px; display: flex; align-items: center; justify-content: center;
270
+ font-size: 0.7em; color: #888; transition: transform 0.25s;
271
+ }
272
+ .live-status-bar.expanded .live-status-expand-toggle { transform: rotate(180deg); }
273
+
274
+ .live-status-detail {
275
+ padding: 12px 20px 16px; max-height: 60vh; overflow-y: auto;
276
+ }
277
+
278
+ /* Status Detail Sections */
279
+ .status-detail-section { margin-bottom: 14px; }
280
+ .status-detail-section:last-child { margin-bottom: 0; }
281
+ .status-detail-section-title {
282
+ font-size: 0.85em; font-weight: 600; color: #555; text-transform: uppercase;
283
+ letter-spacing: 0.5px; margin-bottom: 8px; padding-bottom: 4px;
284
+ border-bottom: 1px solid #eee;
285
+ }
286
+
287
+ /* Running Operations */
288
+ .running-op-row {
289
+ display: flex; align-items: center; gap: 12px; padding: 6px 0;
290
+ font-size: 0.9em;
291
+ }
292
+ .running-op-name { font-weight: 600; min-width: 180px; }
293
+ .running-op-bar {
294
+ flex: 1; height: 8px; background: #e9ecef; border-radius: 4px; overflow: hidden;
295
+ min-width: 120px;
296
+ }
297
+ .running-op-bar-fill { height: 100%; background: #4a90d9; transition: width 0.5s ease; }
298
+ .running-op-count { font-size: 0.85em; color: #666; white-space: nowrap; }
299
+ .running-op-pending { color: #888; font-size: 0.85em; font-style: italic; padding: 4px 0; }
300
+
301
+ /* Completed Operations */
302
+ .completed-op-row { padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
303
+ .completed-op-row:last-child { border-bottom: none; }
304
+ .completed-op-header {
305
+ display: flex; align-items: center; gap: 10px; font-size: 0.9em; margin-bottom: 4px;
306
+ }
307
+ .completed-op-name { font-weight: 600; }
308
+ .completed-op-stats { color: #666; font-size: 0.85em; }
309
+ .completed-op-checkmark { color: #28a745; }
310
+
311
+ /* Ratio Bar */
312
+ .ratio-bar-container {
313
+ display: flex; height: 10px; border-radius: 5px; overflow: hidden;
314
+ background: #e9ecef; margin: 4px 0;
315
+ }
316
+ .ratio-bar-segment { height: 100%; transition: width 0.5s ease; }
317
+ .ratio-bar-segment.unchanged { background: #6c757d; }
318
+ .ratio-bar-segment.new-records { background: #28a745; }
319
+ .ratio-bar-segment.updated { background: #4a90d9; }
320
+ .ratio-bar-segment.deleted { background: #dc3545; }
321
+ .ratio-bar-legend {
322
+ display: flex; gap: 12px; font-size: 0.75em; color: #666; margin-top: 2px; flex-wrap: wrap;
323
+ }
324
+ .ratio-bar-legend-item { display: flex; align-items: center; gap: 4px; }
325
+ .ratio-bar-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
326
+ .ratio-bar-legend-dot.unchanged-dot { background: #6c757d; }
327
+ .ratio-bar-legend-dot.new-dot { background: #28a745; }
328
+ .ratio-bar-legend-dot.updated-dot { background: #4a90d9; }
329
+ .ratio-bar-legend-dot.deleted-dot { background: #dc3545; }
330
+
331
+ /* Error Operations */
332
+ .error-op-row { padding: 6px 0; border-bottom: 1px solid #f0f0f0; font-size: 0.9em; }
333
+ .error-op-row:last-child { border-bottom: none; }
334
+ .error-op-header { display: flex; align-items: center; gap: 8px; }
335
+ .error-op-name { font-weight: 600; color: #dc3545; }
336
+ .error-op-status { font-size: 0.82em; color: #dc3545; }
337
+ .error-op-message { font-size: 0.82em; color: #888; margin-top: 2px; padding-left: 18px; }
338
+ .error-op-log-entries {
339
+ font-size: 0.78em; color: #888; margin-top: 4px; padding-left: 18px;
340
+ font-family: monospace; max-height: 80px; overflow-y: auto;
341
+ }
342
+ `,
343
+ Templates:
344
+ [
345
+ {
346
+ Hash: 'DataCloner-Layout',
347
+ Template: /*html*/`
348
+ <h1>Retold Data Cloner</h1>
349
+
350
+ <!-- Live Status Bar (Expandable) -->
351
+ <div id="liveStatusBar" class="live-status-bar phase-idle" style="position:relative">
352
+ <div class="live-status-header" onclick="pict.views['DataCloner-Layout'].toggleStatusDetail()">
353
+ <div class="live-status-dot"></div>
354
+ <div class="live-status-message" id="liveStatusMessage">Idle</div>
355
+ <div class="live-status-meta" id="liveStatusMeta"></div>
356
+ <div class="live-status-expand-toggle" id="liveStatusToggle">&#9660;</div>
357
+ </div>
358
+ <div class="live-status-detail" id="liveStatusDetail" style="display:none">
359
+ <div id="DataCloner-Throughput-Histogram"></div>
360
+ <div id="DataCloner-StatusDetail-Container"></div>
361
+ </div>
362
+ <div class="live-status-progress-bar"><div class="live-status-progress-fill" id="liveStatusProgressFill" style="width:0%"></div></div>
363
+ </div>
364
+
365
+ <!-- Expand / Collapse All -->
366
+ <div class="accordion-controls">
367
+ <button onclick="pict.views['DataCloner-Layout'].expandAllSections()">Expand All</button>
368
+ <button onclick="pict.views['DataCloner-Layout'].collapseAllSections()">Collapse All</button>
369
+ </div>
370
+
371
+ <!-- Section containers -->
372
+ <div id="DataCloner-Section-Connection"></div>
373
+ <div id="DataCloner-Section-Session"></div>
374
+ <div id="DataCloner-Section-Schema"></div>
375
+ <div id="DataCloner-Section-Deploy"></div>
376
+ <div id="DataCloner-Section-Sync"></div>
377
+ <div id="DataCloner-Section-Export"></div>
378
+ <div id="DataCloner-Section-ViewData"></div>
379
+ `
380
+ }
381
+ ],
382
+ Renderables:
383
+ [
384
+ {
385
+ RenderableHash: 'DataCloner-Layout',
386
+ TemplateHash: 'DataCloner-Layout',
387
+ DestinationAddress: '#DataCloner-Application-Container'
388
+ }
389
+ ]
390
+ };
@@ -0,0 +1,241 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ class DataClonerSchemaView extends libPictView
4
+ {
5
+ constructor(pFable, pOptions, pServiceHash)
6
+ {
7
+ super(pFable, pOptions, pServiceHash);
8
+ }
9
+
10
+ fetchSchema()
11
+ {
12
+ let tmpSchemaURL = document.getElementById('schemaURL').value.trim();
13
+ let tmpBody = {};
14
+ if (tmpSchemaURL)
15
+ {
16
+ tmpBody.SchemaURL = tmpSchemaURL;
17
+ }
18
+
19
+ this.pict.providers.DataCloner.setSectionPhase(3, 'busy');
20
+ this.pict.providers.DataCloner.setStatus('schemaStatus', 'Fetching schema...', 'info');
21
+
22
+ this.pict.providers.DataCloner.api('POST', '/clone/schema/fetch', tmpBody)
23
+ .then(
24
+ (pData) =>
25
+ {
26
+ if (pData.Success)
27
+ {
28
+ this.pict.AppData.DataCloner.FetchedTables = pData.Tables || [];
29
+ this.pict.providers.DataCloner.setStatus('schemaStatus', 'Fetched ' + pData.TableCount + ' tables from ' + pData.SchemaURL, 'ok');
30
+ this.pict.providers.DataCloner.setSectionPhase(3, 'ok');
31
+ this.renderTableList();
32
+ }
33
+ else
34
+ {
35
+ this.pict.providers.DataCloner.setStatus('schemaStatus', 'Fetch failed: ' + (pData.Error || 'Unknown error'), 'error');
36
+ this.pict.providers.DataCloner.setSectionPhase(3, 'error');
37
+ }
38
+ })
39
+ .catch(
40
+ (pError) =>
41
+ {
42
+ this.pict.providers.DataCloner.setStatus('schemaStatus', 'Request failed: ' + pError.message, 'error');
43
+ this.pict.providers.DataCloner.setSectionPhase(3, 'error');
44
+ });
45
+ }
46
+
47
+ loadSavedSelections()
48
+ {
49
+ try
50
+ {
51
+ let tmpRaw = localStorage.getItem('dataCloner_selectedTables');
52
+ if (tmpRaw)
53
+ {
54
+ return JSON.parse(tmpRaw);
55
+ }
56
+ }
57
+ catch (pError)
58
+ {
59
+ /* ignore */
60
+ }
61
+ return null;
62
+ }
63
+
64
+ saveSelections()
65
+ {
66
+ let tmpSelected = this.getSelectedTables();
67
+ localStorage.setItem('dataCloner_selectedTables', JSON.stringify(tmpSelected));
68
+ this.updateSelectionCount();
69
+ this.pict.providers.DataCloner.updateAllPreviews();
70
+ }
71
+
72
+ updateSelectionCount()
73
+ {
74
+ let tmpFetchedTables = this.pict.AppData.DataCloner.FetchedTables || [];
75
+ let tmpCount = this.getSelectedTables().length;
76
+ let tmpEl = document.getElementById('tableSelectionCount');
77
+ if (tmpEl)
78
+ {
79
+ tmpEl.textContent = tmpCount + ' / ' + tmpFetchedTables.length + ' selected';
80
+ }
81
+ }
82
+
83
+ renderTableList()
84
+ {
85
+ let tmpFetchedTables = this.pict.AppData.DataCloner.FetchedTables || [];
86
+ let tmpContainer = document.getElementById('tableList');
87
+ tmpContainer.innerHTML = '';
88
+
89
+ // Load previously saved selections; if none, default to none checked
90
+ let tmpSaved = this.loadSavedSelections();
91
+ let tmpSavedSet = null;
92
+ if (tmpSaved)
93
+ {
94
+ tmpSavedSet = {};
95
+ for (let i = 0; i < tmpSaved.length; i++)
96
+ {
97
+ tmpSavedSet[tmpSaved[i]] = true;
98
+ }
99
+ }
100
+
101
+ for (let i = 0; i < tmpFetchedTables.length; i++)
102
+ {
103
+ let tmpName = tmpFetchedTables[i];
104
+ let tmpDiv = document.createElement('div');
105
+ tmpDiv.className = 'table-item';
106
+ tmpDiv.setAttribute('data-table', tmpName.toLowerCase());
107
+
108
+ let tmpCheckbox = document.createElement('input');
109
+ tmpCheckbox.type = 'checkbox';
110
+ tmpCheckbox.id = 'tbl_' + tmpName;
111
+ tmpCheckbox.value = tmpName;
112
+ // If we have saved selections, restore them; otherwise default unchecked
113
+ tmpCheckbox.checked = tmpSavedSet ? (tmpSavedSet[tmpName] === true) : false;
114
+ tmpCheckbox.addEventListener('change', () => { this.saveSelections(); });
115
+
116
+ let tmpLabel = document.createElement('label');
117
+ tmpLabel.htmlFor = 'tbl_' + tmpName;
118
+ tmpLabel.textContent = tmpName;
119
+
120
+ tmpDiv.appendChild(tmpCheckbox);
121
+ tmpDiv.appendChild(tmpLabel);
122
+ tmpContainer.appendChild(tmpDiv);
123
+ }
124
+
125
+ document.getElementById('tableSelection').style.display = tmpFetchedTables.length > 0 ? 'block' : 'none';
126
+ document.getElementById('tableFilter').value = '';
127
+ this.updateSelectionCount();
128
+ }
129
+
130
+ filterTableList()
131
+ {
132
+ let tmpFilter = document.getElementById('tableFilter').value.toLowerCase().trim();
133
+ let tmpItems = document.getElementById('tableList').children;
134
+ for (let i = 0; i < tmpItems.length; i++)
135
+ {
136
+ let tmpName = tmpItems[i].getAttribute('data-table') || '';
137
+ tmpItems[i].style.display = (!tmpFilter || tmpName.indexOf(tmpFilter) >= 0) ? '' : 'none';
138
+ }
139
+ }
140
+
141
+ selectAllTables(pChecked)
142
+ {
143
+ let tmpFetchedTables = this.pict.AppData.DataCloner.FetchedTables || [];
144
+ // Only affect visible (non-filtered) items
145
+ let tmpFilter = document.getElementById('tableFilter').value.toLowerCase().trim();
146
+ for (let i = 0; i < tmpFetchedTables.length; i++)
147
+ {
148
+ let tmpName = tmpFetchedTables[i];
149
+ if (tmpFilter && tmpName.toLowerCase().indexOf(tmpFilter) < 0)
150
+ {
151
+ continue;
152
+ }
153
+ let tmpCheckbox = document.getElementById('tbl_' + tmpName);
154
+ if (tmpCheckbox)
155
+ {
156
+ tmpCheckbox.checked = pChecked;
157
+ }
158
+ }
159
+ this.saveSelections();
160
+ }
161
+
162
+ getSelectedTables()
163
+ {
164
+ let tmpFetchedTables = this.pict.AppData.DataCloner.FetchedTables || [];
165
+ let tmpSelected = [];
166
+ for (let i = 0; i < tmpFetchedTables.length; i++)
167
+ {
168
+ let tmpCheckbox = document.getElementById('tbl_' + tmpFetchedTables[i]);
169
+ if (tmpCheckbox && tmpCheckbox.checked)
170
+ {
171
+ tmpSelected.push(tmpFetchedTables[i]);
172
+ }
173
+ }
174
+ return tmpSelected;
175
+ }
176
+ }
177
+
178
+ module.exports = DataClonerSchemaView;
179
+
180
+ module.exports.default_configuration =
181
+ {
182
+ ViewIdentifier: 'DataCloner-Schema',
183
+ DefaultRenderable: 'DataCloner-Schema',
184
+ DefaultDestinationAddress: '#DataCloner-Section-Schema',
185
+ CSS: /*css*/`
186
+ .table-list { max-height: 300px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 8px; margin: 10px 0; }
187
+ .table-item { padding: 4px 8px; display: flex; align-items: center; }
188
+ .table-item:hover { background: #f0f0f0; }
189
+ .table-item input[type="checkbox"] { margin-right: 8px; width: auto; }
190
+ .table-item label { display: inline; font-weight: normal; margin-bottom: 0; cursor: pointer; }
191
+ `,
192
+ Templates:
193
+ [
194
+ {
195
+ Hash: 'DataCloner-Schema',
196
+ Template: /*html*/`
197
+ <div class="accordion-row">
198
+ <div class="accordion-number">3</div>
199
+ <div class="accordion-card" id="section3" data-section="3">
200
+ <div class="accordion-header" onclick="pict.views['DataCloner-Layout'].toggleSection('section3')">
201
+ <div class="accordion-title">Remote Schema</div>
202
+ <span class="accordion-phase" id="phase3"></span>
203
+ <div class="accordion-preview" id="preview3">Fetch and select tables from the remote server</div>
204
+ <div class="accordion-actions">
205
+ <span class="accordion-go" onclick="event.stopPropagation(); pict.views['DataCloner-Schema'].fetchSchema()">go</span>
206
+ <label class="accordion-auto" onclick="event.stopPropagation()"><input type="checkbox" id="auto3"> <span class="auto-label">auto</span></label>
207
+ </div>
208
+ <div class="accordion-toggle">&#9660;</div>
209
+ </div>
210
+ <div class="accordion-body">
211
+ <label for="schemaURL">Schema URL (leave blank for default: /1.0/Retold/Models)</label>
212
+ <input type="text" id="schemaURL" placeholder="http://remote-server:8086/1.0/Retold/Models">
213
+
214
+ <button class="primary" onclick="pict.views['DataCloner-Schema'].fetchSchema()">Fetch Schema</button>
215
+ <div id="schemaStatus"></div>
216
+
217
+ <div id="tableSelection" style="display:none">
218
+ <h3 style="margin:12px 0 8px; font-size:1em;">Select Tables</h3>
219
+ <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px">
220
+ <input type="text" id="tableFilter" placeholder="Filter tables..." style="flex:1; margin-bottom:0" oninput="pict.views['DataCloner-Schema'].filterTableList()">
221
+ <button class="secondary" onclick="pict.views['DataCloner-Schema'].selectAllTables(true)" style="font-size:0.8em">Select All</button>
222
+ <button class="secondary" onclick="pict.views['DataCloner-Schema'].selectAllTables(false)" style="font-size:0.8em">Deselect All</button>
223
+ <span id="tableSelectionCount" style="font-size:0.85em; color:#666; white-space:nowrap"></span>
224
+ </div>
225
+ <div id="tableList" class="table-list"></div>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ `
231
+ }
232
+ ],
233
+ Renderables:
234
+ [
235
+ {
236
+ RenderableHash: 'DataCloner-Schema',
237
+ TemplateHash: 'DataCloner-Schema',
238
+ DestinationAddress: '#DataCloner-Section-Schema'
239
+ }
240
+ ]
241
+ };