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.
- package/.claude/launch.json +2 -2
- package/.quackage.json +19 -0
- package/package.json +13 -6
- package/source/services/data-cloner/DataCloner-Command-Sync.js +83 -50
- package/source/services/data-cloner/DataCloner-Command-WebUI.js +27 -10
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +281 -4
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner-Configuration.json +9 -0
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +102 -0
- package/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js +6 -0
- package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +998 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +407 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +126 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +483 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +390 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Schema.js +241 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Session.js +268 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +575 -0
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-ViewData.js +176 -0
- package/source/services/data-cloner/web/data-cloner.js +7952 -0
- package/source/services/data-cloner/web/data-cloner.js.map +1 -0
- package/source/services/data-cloner/web/data-cloner.min.js +2 -0
- package/source/services/data-cloner/web/data-cloner.min.js.map +1 -0
- package/source/services/data-cloner/web/index.html +17 -0
- package/test/DataCloner-Integration_tests.js +1205 -0
- package/test/DataCloner-Puppeteer_tests.js +502 -0
- package/test/integration-report.json +311 -0
- package/test/run-integration-tests.js +501 -0
- package/source/services/data-cloner/data-cloner-web.html +0 -2706
|
@@ -77,7 +77,11 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
77
77
|
SyncStartTime: null,
|
|
78
78
|
SyncEndTime: null,
|
|
79
79
|
SyncEventLog: [],
|
|
80
|
-
SyncReport: null
|
|
80
|
+
SyncReport: null,
|
|
81
|
+
|
|
82
|
+
// Throughput sampling — records per 10-second window
|
|
83
|
+
ThroughputSamples: [],
|
|
84
|
+
ThroughputTimer: null
|
|
81
85
|
});
|
|
82
86
|
|
|
83
87
|
// Create an isolated Pict instance for remote session management
|
|
@@ -156,8 +160,40 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
156
160
|
return fCallback(new Error(`Could not load module "${tmpRegistryEntry.moduleName}": ${pRequireError.message}. Run: npm install ${tmpRegistryEntry.moduleName}`));
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
// Normalize the config to include both old-style (Server, Port, User, etc.)
|
|
164
|
+
// and mysql2-native (host, port, user, etc.) property names so it works
|
|
165
|
+
// with any version of the meadow-connection provider.
|
|
166
|
+
let tmpNormalizedConfig = Object.assign({}, pConfig);
|
|
167
|
+
if (pProviderName === 'MySQL' || pProviderName === 'MSSQL' || pProviderName === 'PostgreSQL')
|
|
168
|
+
{
|
|
169
|
+
if (tmpNormalizedConfig.host && !tmpNormalizedConfig.Server)
|
|
170
|
+
{
|
|
171
|
+
tmpNormalizedConfig.Server = tmpNormalizedConfig.host;
|
|
172
|
+
}
|
|
173
|
+
if (tmpNormalizedConfig.port && !tmpNormalizedConfig.Port)
|
|
174
|
+
{
|
|
175
|
+
tmpNormalizedConfig.Port = tmpNormalizedConfig.port;
|
|
176
|
+
}
|
|
177
|
+
if (tmpNormalizedConfig.user && !tmpNormalizedConfig.User)
|
|
178
|
+
{
|
|
179
|
+
tmpNormalizedConfig.User = tmpNormalizedConfig.user;
|
|
180
|
+
}
|
|
181
|
+
if (tmpNormalizedConfig.password && !tmpNormalizedConfig.Password)
|
|
182
|
+
{
|
|
183
|
+
tmpNormalizedConfig.Password = tmpNormalizedConfig.password;
|
|
184
|
+
}
|
|
185
|
+
if (tmpNormalizedConfig.database && !tmpNormalizedConfig.Database)
|
|
186
|
+
{
|
|
187
|
+
tmpNormalizedConfig.Database = tmpNormalizedConfig.database;
|
|
188
|
+
}
|
|
189
|
+
if (tmpNormalizedConfig.connectionLimit && !tmpNormalizedConfig.ConnectionPoolLimit)
|
|
190
|
+
{
|
|
191
|
+
tmpNormalizedConfig.ConnectionPoolLimit = tmpNormalizedConfig.connectionLimit;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
159
195
|
// Set the provider configuration on fable settings
|
|
160
|
-
this.fable.settings[tmpRegistryEntry.configKey] =
|
|
196
|
+
this.fable.settings[tmpRegistryEntry.configKey] = tmpNormalizedConfig;
|
|
161
197
|
|
|
162
198
|
// Register and instantiate the provider if not already present
|
|
163
199
|
if (!this.fable[tmpRegistryEntry.serviceName])
|
|
@@ -165,6 +201,12 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
165
201
|
this.fable.serviceManager.addServiceType(tmpRegistryEntry.serviceName, tmpModule);
|
|
166
202
|
this.fable.serviceManager.instantiateServiceProvider(tmpRegistryEntry.serviceName);
|
|
167
203
|
}
|
|
204
|
+
else
|
|
205
|
+
{
|
|
206
|
+
// Provider already exists — update its options with the new config
|
|
207
|
+
// so reconnects use the current settings.
|
|
208
|
+
this.fable[tmpRegistryEntry.serviceName].options[tmpRegistryEntry.configKey] = tmpNormalizedConfig;
|
|
209
|
+
}
|
|
168
210
|
|
|
169
211
|
this.fable[tmpRegistryEntry.serviceName].connectAsync(
|
|
170
212
|
(pError) =>
|
|
@@ -322,6 +364,12 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
322
364
|
Synced: tmpP.Synced || 0,
|
|
323
365
|
Skipped: tmpP.Skipped || 0,
|
|
324
366
|
Errors: tmpP.Errors || 0,
|
|
367
|
+
New: tmpP.New || 0,
|
|
368
|
+
Updated: tmpP.Updated || 0,
|
|
369
|
+
Unchanged: tmpP.Unchanged || 0,
|
|
370
|
+
Deleted: tmpP.Deleted || 0,
|
|
371
|
+
ServerTotal: tmpP.ServerTotal || 0,
|
|
372
|
+
LocalCountBefore: tmpP.LocalCountBefore || 0,
|
|
325
373
|
ErrorMessage: tmpP.ErrorMessage || null,
|
|
326
374
|
StartTime: tmpP.StartTime || null,
|
|
327
375
|
EndTime: tmpP.EndTime || null,
|
|
@@ -434,7 +482,8 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
434
482
|
},
|
|
435
483
|
Tables: tmpTables,
|
|
436
484
|
Anomalies: tmpAnomalies,
|
|
437
|
-
EventLog: tmpState.SyncEventLog
|
|
485
|
+
EventLog: tmpState.SyncEventLog,
|
|
486
|
+
ThroughputSamples: tmpState.ThroughputSamples || []
|
|
438
487
|
});
|
|
439
488
|
|
|
440
489
|
tmpState.SyncReport = tmpReport;
|
|
@@ -513,6 +562,75 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
513
562
|
this.fable.log.info(`\n${tmpLines.join('\n')}`);
|
|
514
563
|
}
|
|
515
564
|
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Start the throughput sampler. Every 10 seconds, records a cumulative
|
|
568
|
+
* { t, synced } sample so the UI can render a records-per-10s histogram.
|
|
569
|
+
*/
|
|
570
|
+
startThroughputSampling()
|
|
571
|
+
{
|
|
572
|
+
this.stopThroughputSampling();
|
|
573
|
+
this._cloneState.ThroughputSamples = [];
|
|
574
|
+
this._cloneState.ThroughputSamples.push({ t: Date.now(), synced: 0 });
|
|
575
|
+
|
|
576
|
+
this._cloneState.ThroughputTimer = setInterval(
|
|
577
|
+
() =>
|
|
578
|
+
{
|
|
579
|
+
// Read directly from MeadowSync progress trackers for accurate
|
|
580
|
+
// real-time counts, then update SyncProgress as a side-effect so
|
|
581
|
+
// the next live-status poll also has fresh data.
|
|
582
|
+
let tmpTotalSynced = 0;
|
|
583
|
+
let tmpTableNames = Object.keys(this._cloneState.SyncProgress);
|
|
584
|
+
let tmpSyncEntities = (this.fable.MeadowSync && this.fable.MeadowSync.MeadowSyncEntities) || {};
|
|
585
|
+
|
|
586
|
+
for (let i = 0; i < tmpTableNames.length; i++)
|
|
587
|
+
{
|
|
588
|
+
let tmpName = tmpTableNames[i];
|
|
589
|
+
let tmpProgress = this._cloneState.SyncProgress[tmpName];
|
|
590
|
+
|
|
591
|
+
// If the entity is actively syncing, read the live tracker value
|
|
592
|
+
if (tmpProgress.Status === 'Syncing' || tmpProgress.Status === 'Pending')
|
|
593
|
+
{
|
|
594
|
+
let tmpSyncEntity = tmpSyncEntities[tmpName];
|
|
595
|
+
if (tmpSyncEntity && tmpSyncEntity.operation)
|
|
596
|
+
{
|
|
597
|
+
let tmpTracker = tmpSyncEntity.operation.progressTrackers[`FullSync-${tmpName}`];
|
|
598
|
+
if (tmpTracker)
|
|
599
|
+
{
|
|
600
|
+
tmpProgress.Total = tmpTracker.TotalCount || tmpProgress.Total;
|
|
601
|
+
tmpProgress.Synced = Math.max(tmpTracker.CurrentCount || 0, 0);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
tmpTotalSynced += (tmpProgress.Synced || 0);
|
|
607
|
+
}
|
|
608
|
+
this._cloneState.ThroughputSamples.push({ t: Date.now(), synced: tmpTotalSynced });
|
|
609
|
+
}, 10000);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Stop the throughput sampler and record a final sample.
|
|
614
|
+
*/
|
|
615
|
+
stopThroughputSampling()
|
|
616
|
+
{
|
|
617
|
+
if (this._cloneState.ThroughputTimer)
|
|
618
|
+
{
|
|
619
|
+
clearInterval(this._cloneState.ThroughputTimer);
|
|
620
|
+
this._cloneState.ThroughputTimer = null;
|
|
621
|
+
}
|
|
622
|
+
if (this._cloneState.ThroughputSamples && this._cloneState.ThroughputSamples.length > 0)
|
|
623
|
+
{
|
|
624
|
+
let tmpTotalSynced = 0;
|
|
625
|
+
let tmpTableNames = Object.keys(this._cloneState.SyncProgress);
|
|
626
|
+
for (let i = 0; i < tmpTableNames.length; i++)
|
|
627
|
+
{
|
|
628
|
+
tmpTotalSynced += (this._cloneState.SyncProgress[tmpTableNames[i]].Synced || 0);
|
|
629
|
+
}
|
|
630
|
+
this._cloneState.ThroughputSamples.push({ t: Date.now(), synced: tmpTotalSynced });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
516
634
|
/**
|
|
517
635
|
* Pre-count phase — fetch record counts for all tables in parallel
|
|
518
636
|
* before starting the actual sync. Populates SyncProgress[table].Total
|
|
@@ -565,6 +683,128 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
565
683
|
});
|
|
566
684
|
}
|
|
567
685
|
|
|
686
|
+
/**
|
|
687
|
+
* Warm up the local database connection by running a lightweight test query.
|
|
688
|
+
* This forces the connection pool to recycle any stale/dead connections
|
|
689
|
+
* (e.g. after a burst of CREATE TABLE statements during deploy).
|
|
690
|
+
* Retries up to 3 times with a short delay between attempts.
|
|
691
|
+
*
|
|
692
|
+
* @param {Function} fCallback - Called when a connection is verified
|
|
693
|
+
*/
|
|
694
|
+
warmUpLocalConnection(fCallback)
|
|
695
|
+
{
|
|
696
|
+
let tmpProviderName = this._cloneState.ConnectionProvider;
|
|
697
|
+
let tmpAttempts = 0;
|
|
698
|
+
let tmpMaxAttempts = 5;
|
|
699
|
+
let tmpRetryDelayMs = 2000;
|
|
700
|
+
|
|
701
|
+
let fAttempt = () =>
|
|
702
|
+
{
|
|
703
|
+
tmpAttempts++;
|
|
704
|
+
|
|
705
|
+
if (tmpProviderName === 'SQLite')
|
|
706
|
+
{
|
|
707
|
+
// SQLite doesn't have connection pool issues
|
|
708
|
+
return fCallback();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Look up the provider service via the registry
|
|
712
|
+
let tmpRegistryEntry = _ProviderRegistry[tmpProviderName];
|
|
713
|
+
if (!tmpRegistryEntry)
|
|
714
|
+
{
|
|
715
|
+
this.fable.log.warn(`Data Cloner: Connection warmup — unknown provider "${tmpProviderName}", skipping.`);
|
|
716
|
+
return fCallback();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
let tmpProviderService = this.fable[tmpRegistryEntry.serviceName];
|
|
720
|
+
if (!tmpProviderService)
|
|
721
|
+
{
|
|
722
|
+
this.fable.log.warn(`Data Cloner: Connection warmup — provider service "${tmpRegistryEntry.serviceName}" not available, skipping.`);
|
|
723
|
+
return fCallback();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Log the pool config on the first attempt for diagnostics
|
|
727
|
+
if (tmpAttempts === 1)
|
|
728
|
+
{
|
|
729
|
+
let tmpPoolConfig = tmpProviderService.options && tmpProviderService.options.MySQL;
|
|
730
|
+
if (tmpPoolConfig)
|
|
731
|
+
{
|
|
732
|
+
this.fable.log.info(`Data Cloner: Connection warmup — pool target: ${tmpPoolConfig.host}:${tmpPoolConfig.port} database=${tmpPoolConfig.database} user=${tmpPoolConfig.user}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// If the pool doesn't exist or a previous attempt failed, try to (re)create it
|
|
737
|
+
if (!tmpProviderService.pool)
|
|
738
|
+
{
|
|
739
|
+
this.fable.log.info(`Data Cloner: Connection warmup — no pool exists, calling connect()...`);
|
|
740
|
+
try
|
|
741
|
+
{
|
|
742
|
+
tmpProviderService.connect();
|
|
743
|
+
}
|
|
744
|
+
catch (pConnectError)
|
|
745
|
+
{
|
|
746
|
+
this.fable.log.warn(`Data Cloner: Connection warmup — connect() error: ${pConnectError.message}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (!tmpProviderService.pool)
|
|
751
|
+
{
|
|
752
|
+
this.fable.log.warn(`Data Cloner: Connection warmup — pool still not available after connect(), skipping.`);
|
|
753
|
+
return fCallback();
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Run a lightweight test query to force the pool to recycle stale connections
|
|
757
|
+
this.fable.log.info(`Data Cloner: Connection warmup attempt ${tmpAttempts}/${tmpMaxAttempts}...`);
|
|
758
|
+
tmpProviderService.pool.query('SELECT 1 AS _warmup',
|
|
759
|
+
(pError) =>
|
|
760
|
+
{
|
|
761
|
+
if (pError)
|
|
762
|
+
{
|
|
763
|
+
this.fable.log.warn(`Data Cloner: Connection warmup attempt ${tmpAttempts}/${tmpMaxAttempts} failed: ${pError.code || pError.message}`);
|
|
764
|
+
|
|
765
|
+
// If the pool is persistently failing, destroy and recreate it
|
|
766
|
+
if (tmpAttempts >= 2 && pError.code === 'ECONNREFUSED')
|
|
767
|
+
{
|
|
768
|
+
this.fable.log.info('Data Cloner: Connection warmup — destroying stale pool and recreating...');
|
|
769
|
+
try
|
|
770
|
+
{
|
|
771
|
+
tmpProviderService.pool.end(() => {});
|
|
772
|
+
}
|
|
773
|
+
catch (pEndError) { /* ignore */ }
|
|
774
|
+
tmpProviderService._ConnectionPool = false;
|
|
775
|
+
tmpProviderService.connected = false;
|
|
776
|
+
try
|
|
777
|
+
{
|
|
778
|
+
tmpProviderService.connect();
|
|
779
|
+
this.fable.log.info('Data Cloner: Connection warmup — pool recreated.');
|
|
780
|
+
}
|
|
781
|
+
catch (pReconnectError)
|
|
782
|
+
{
|
|
783
|
+
this.fable.log.warn(`Data Cloner: Connection warmup — reconnect error: ${pReconnectError.message}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (tmpAttempts < tmpMaxAttempts)
|
|
788
|
+
{
|
|
789
|
+
setTimeout(fAttempt, tmpRetryDelayMs);
|
|
790
|
+
}
|
|
791
|
+
else
|
|
792
|
+
{
|
|
793
|
+
this.fable.log.warn('Data Cloner: Connection warmup exhausted retries — proceeding with sync anyway.');
|
|
794
|
+
return fCallback();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
else
|
|
798
|
+
{
|
|
799
|
+
this.fable.log.info(`Data Cloner: Connection warmup succeeded on attempt ${tmpAttempts}.`);
|
|
800
|
+
return fCallback();
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
fAttempt();
|
|
806
|
+
}
|
|
807
|
+
|
|
568
808
|
/**
|
|
569
809
|
* The sync engine — synchronize data for a list of tables sequentially.
|
|
570
810
|
*
|
|
@@ -591,10 +831,12 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
591
831
|
SyncDeletedRecords: this._cloneState.SyncDeletedRecords
|
|
592
832
|
});
|
|
593
833
|
|
|
834
|
+
this.startThroughputSampling();
|
|
594
835
|
let fSyncNextTable = () =>
|
|
595
836
|
{
|
|
596
837
|
if (this._cloneState.SyncStopping || tmpTableIndex >= pTables.length)
|
|
597
838
|
{
|
|
839
|
+
this.stopThroughputSampling();
|
|
598
840
|
this._cloneState.SyncRunning = false;
|
|
599
841
|
this._cloneState.SyncEndTime = new Date().toJSON();
|
|
600
842
|
|
|
@@ -619,6 +861,22 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
619
861
|
}
|
|
620
862
|
|
|
621
863
|
this.fable.log.info('Data Cloner: Sync complete.');
|
|
864
|
+
|
|
865
|
+
// Close the log file stream if one was opened for this run
|
|
866
|
+
if (this._cloneState.SyncLogFileLogger)
|
|
867
|
+
{
|
|
868
|
+
let tmpLogPath = this._cloneState.SyncLogFilePath || '';
|
|
869
|
+
this._cloneState.SyncLogFileLogger.flushBufferToLogFile(() =>
|
|
870
|
+
{
|
|
871
|
+
this._cloneState.SyncLogFileLogger.closeWriter(() =>
|
|
872
|
+
{
|
|
873
|
+
this.fable.log.info(`Data Cloner: Log file closed — ${tmpLogPath}`);
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
this._cloneState.SyncLogFileLogger = null;
|
|
877
|
+
this._cloneState.SyncLogFilePath = null;
|
|
878
|
+
}
|
|
879
|
+
|
|
622
880
|
return;
|
|
623
881
|
}
|
|
624
882
|
|
|
@@ -652,6 +910,18 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
652
910
|
}
|
|
653
911
|
}
|
|
654
912
|
|
|
913
|
+
// Read per-record breakdown from the sync entity
|
|
914
|
+
if (tmpSyncEntity && tmpSyncEntity.syncResults)
|
|
915
|
+
{
|
|
916
|
+
let tmpResults = tmpSyncEntity.syncResults;
|
|
917
|
+
tmpProgress.New = tmpResults.Created || 0;
|
|
918
|
+
tmpProgress.Updated = 0;
|
|
919
|
+
tmpProgress.Unchanged = tmpResults.LocalRecordCount || 0;
|
|
920
|
+
tmpProgress.Deleted = tmpResults.Deleted || 0;
|
|
921
|
+
tmpProgress.ServerTotal = tmpResults.ServerRecordCount || 0;
|
|
922
|
+
tmpProgress.LocalCountBefore = tmpResults.LocalRecordCount || 0;
|
|
923
|
+
}
|
|
924
|
+
|
|
655
925
|
let tmpRESTErrors = this._cloneState.SyncRESTErrors[tmpTableName] || 0;
|
|
656
926
|
tmpProgress.Errors = tmpRESTErrors;
|
|
657
927
|
|
|
@@ -698,7 +968,14 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
698
968
|
this.preCountTables(pTables,
|
|
699
969
|
() =>
|
|
700
970
|
{
|
|
701
|
-
|
|
971
|
+
// Warm up the local database connection before starting sync.
|
|
972
|
+
// After the pre-count phase (which only queries the remote server),
|
|
973
|
+
// local DB pool connections may have gone stale. A test query
|
|
974
|
+
// forces the pool to recycle dead connections before we sync.
|
|
975
|
+
this.warmUpLocalConnection(() =>
|
|
976
|
+
{
|
|
977
|
+
fSyncNextTable();
|
|
978
|
+
});
|
|
702
979
|
});
|
|
703
980
|
}
|
|
704
981
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Name": "Retold Data Cloner",
|
|
3
|
+
"Hash": "DataCloner",
|
|
4
|
+
"MainViewportViewIdentifier": "DataCloner-Layout",
|
|
5
|
+
"MainViewportDestinationAddress": "#DataCloner-Application-Container",
|
|
6
|
+
"MainViewportDefaultDataAddress": "AppData.DataCloner",
|
|
7
|
+
"pict_configuration": { "Product": "DataCloner" },
|
|
8
|
+
"AutoRenderMainViewportViewAfterInitialize": false
|
|
9
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const libPictApplication = require('pict-application');
|
|
2
|
+
|
|
3
|
+
const libProvider = require('./providers/Pict-Provider-DataCloner.js');
|
|
4
|
+
|
|
5
|
+
const libViewLayout = require('./views/PictView-DataCloner-Layout.js');
|
|
6
|
+
const libViewConnection = require('./views/PictView-DataCloner-Connection.js');
|
|
7
|
+
const libViewSession = require('./views/PictView-DataCloner-Session.js');
|
|
8
|
+
const libViewSchema = require('./views/PictView-DataCloner-Schema.js');
|
|
9
|
+
const libViewDeploy = require('./views/PictView-DataCloner-Deploy.js');
|
|
10
|
+
const libViewSync = require('./views/PictView-DataCloner-Sync.js');
|
|
11
|
+
const libViewExport = require('./views/PictView-DataCloner-Export.js');
|
|
12
|
+
const libViewViewData = require('./views/PictView-DataCloner-ViewData.js');
|
|
13
|
+
const libViewHistogram = require('pict-section-histogram');
|
|
14
|
+
|
|
15
|
+
class DataClonerApplication extends libPictApplication
|
|
16
|
+
{
|
|
17
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
18
|
+
{
|
|
19
|
+
super(pFable, pOptions, pServiceHash);
|
|
20
|
+
|
|
21
|
+
// Register provider
|
|
22
|
+
this.pict.addProvider('DataCloner', libProvider.default_configuration, libProvider);
|
|
23
|
+
|
|
24
|
+
// Register views
|
|
25
|
+
this.pict.addView('DataCloner-Layout', libViewLayout.default_configuration, libViewLayout);
|
|
26
|
+
this.pict.addView('DataCloner-Connection', libViewConnection.default_configuration, libViewConnection);
|
|
27
|
+
this.pict.addView('DataCloner-Session', libViewSession.default_configuration, libViewSession);
|
|
28
|
+
this.pict.addView('DataCloner-Schema', libViewSchema.default_configuration, libViewSchema);
|
|
29
|
+
this.pict.addView('DataCloner-Deploy', libViewDeploy.default_configuration, libViewDeploy);
|
|
30
|
+
this.pict.addView('DataCloner-Sync', libViewSync.default_configuration, libViewSync);
|
|
31
|
+
this.pict.addView('DataCloner-Export', libViewExport.default_configuration, libViewExport);
|
|
32
|
+
this.pict.addView('DataCloner-ViewData', libViewViewData.default_configuration, libViewViewData);
|
|
33
|
+
this.pict.addView('DataCloner-StatusHistogram',
|
|
34
|
+
{
|
|
35
|
+
ViewIdentifier: 'DataCloner-StatusHistogram',
|
|
36
|
+
TargetElementAddress: '#DataCloner-Throughput-Histogram',
|
|
37
|
+
DefaultDestinationAddress: '#DataCloner-Throughput-Histogram',
|
|
38
|
+
RenderOnLoad: false,
|
|
39
|
+
Selectable: false,
|
|
40
|
+
Orientation: 'vertical',
|
|
41
|
+
FillContainer: true,
|
|
42
|
+
ShowValues: false,
|
|
43
|
+
ShowLabels: true,
|
|
44
|
+
MaxBarSize: 80,
|
|
45
|
+
BarColor: '#4a90d9',
|
|
46
|
+
Bins: []
|
|
47
|
+
}, libViewHistogram);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onAfterInitializeAsync(fCallback)
|
|
51
|
+
{
|
|
52
|
+
// Centralized state (replaces global variables)
|
|
53
|
+
this.pict.AppData.DataCloner =
|
|
54
|
+
{
|
|
55
|
+
FetchedTables: [],
|
|
56
|
+
DeployedTables: [],
|
|
57
|
+
LastReport: null,
|
|
58
|
+
ServerBusyAtLoad: false,
|
|
59
|
+
SyncPollTimer: null,
|
|
60
|
+
LiveStatusTimer: null,
|
|
61
|
+
StatusDetailExpanded: false,
|
|
62
|
+
StatusDetailTimer: null,
|
|
63
|
+
StatusDetailData: null,
|
|
64
|
+
LastLiveStatus: null,
|
|
65
|
+
PersistFields: [
|
|
66
|
+
'serverURL', 'authMethod', 'authURI', 'checkURI',
|
|
67
|
+
'cookieName', 'cookieValueAddr', 'cookieValueTemplate', 'loginMarker',
|
|
68
|
+
'userName', 'password', 'schemaURL', 'pageSize', 'dateTimePrecisionMS',
|
|
69
|
+
'connProvider', 'sqliteFilePath',
|
|
70
|
+
'mysqlServer', 'mysqlPort', 'mysqlUser', 'mysqlPassword', 'mysqlDatabase', 'mysqlConnectionLimit',
|
|
71
|
+
'mssqlServer', 'mssqlPort', 'mssqlUser', 'mssqlPassword', 'mssqlDatabase', 'mssqlConnectionLimit',
|
|
72
|
+
'postgresqlHost', 'postgresqlPort', 'postgresqlUser', 'postgresqlPassword', 'postgresqlDatabase', 'postgresqlConnectionLimit',
|
|
73
|
+
'solrHost', 'solrPort', 'solrCore', 'solrPath',
|
|
74
|
+
'mongodbHost', 'mongodbPort', 'mongodbUser', 'mongodbPassword', 'mongodbDatabase', 'mongodbConnectionLimit',
|
|
75
|
+
'rocksdbFolder',
|
|
76
|
+
'bibliographFolder'
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Make pict available for inline onclick handlers
|
|
81
|
+
window.pict = this.pict;
|
|
82
|
+
|
|
83
|
+
// Render layout (which chains child view renders via onAfterRender)
|
|
84
|
+
this.pict.views['DataCloner-Layout'].render();
|
|
85
|
+
|
|
86
|
+
// Post-render initialization
|
|
87
|
+
this.pict.providers.DataCloner.initPersistence();
|
|
88
|
+
this.pict.views['DataCloner-Connection'].onProviderChange();
|
|
89
|
+
this.pict.providers.DataCloner.restoreDeployedTables();
|
|
90
|
+
this.pict.providers.DataCloner.startLiveStatusPolling();
|
|
91
|
+
this.pict.providers.DataCloner.initAccordionPreviews();
|
|
92
|
+
this.pict.providers.DataCloner.updateAllPreviews();
|
|
93
|
+
this.pict.views['DataCloner-Layout'].collapseAllSections();
|
|
94
|
+
this.pict.providers.DataCloner.initAutoProcess();
|
|
95
|
+
|
|
96
|
+
return fCallback();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = DataClonerApplication;
|
|
101
|
+
|
|
102
|
+
module.exports.default_configuration = require('./Pict-Application-DataCloner-Configuration.json');
|