retold-data-service 2.0.13 → 2.0.14
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/example_applications/data-cloner/data/cloned.sqlite +0 -0
- package/example_applications/data-cloner/data/cloned.sqlite-shm +0 -0
- package/example_applications/data-cloner/data/cloned.sqlite-wal +0 -0
- package/example_applications/data-cloner/data-cloner-web.html +935 -0
- package/example_applications/data-cloner/data-cloner.js +1047 -0
- package/example_applications/data-cloner/package.json +19 -0
- package/package.json +13 -9
- package/source/Retold-Data-Service.js +225 -73
- package/source/services/Retold-Data-Service-ConnectionManager.js +277 -0
- package/source/services/Retold-Data-Service-MeadowEndpoints.js +217 -0
- package/source/services/Retold-Data-Service-ModelManager.js +335 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-CSVCheck.js +85 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-CSVTransform.js +180 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionIntersect.js +153 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionPush.js +190 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionToArray.js +113 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionToCSV.js +211 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-EntityFromTabularFolder.js +244 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-JSONArrayTransform.js +213 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-TSVCheck.js +80 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-TSVTransform.js +166 -0
- package/source/services/meadow-integration/Retold-Data-Service-MeadowIntegration.js +113 -0
- package/source/services/migration-manager/MigrationManager-Command-Connections.js +220 -0
- package/source/services/migration-manager/MigrationManager-Command-DiffMigrate.js +169 -0
- package/source/services/migration-manager/MigrationManager-Command-Schemas.js +532 -0
- package/source/services/migration-manager/MigrationManager-Command-WebUI.js +123 -0
- package/source/services/migration-manager/Retold-Data-Service-MigrationManager.js +357 -0
- package/source/services/stricture/Retold-Data-Service-Stricture.js +303 -0
- package/source/services/stricture/Stricture-Command-Compile.js +39 -0
- package/source/services/stricture/Stricture-Command-Generate-AuthorizationChart.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-DictionaryCSV.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-LaTeX.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-Markdown.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-Meadow.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-ModelGraph.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-MySQL.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-MySQLMigrate.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-Pict.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-TestObjectContainers.js +14 -0
- package/test/RetoldDataService_tests.js +161 -1
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Retold Data Cloner</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 20px; }
|
|
10
|
+
h1 { margin-bottom: 20px; color: #1a1a1a; }
|
|
11
|
+
h2 { margin-bottom: 12px; color: #444; font-size: 1.2em; border-bottom: 2px solid #ddd; padding-bottom: 6px; }
|
|
12
|
+
|
|
13
|
+
.section { background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
14
|
+
|
|
15
|
+
label { display: block; font-weight: 600; margin-bottom: 4px; font-size: 0.9em; }
|
|
16
|
+
input[type="text"], input[type="password"], input[type="number"] {
|
|
17
|
+
width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px;
|
|
18
|
+
font-size: 0.95em; margin-bottom: 10px;
|
|
19
|
+
}
|
|
20
|
+
input[type="text"]:focus, input[type="password"]:focus, input[type="number"]:focus {
|
|
21
|
+
outline: none; border-color: #4a90d9;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
button {
|
|
25
|
+
padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer;
|
|
26
|
+
font-size: 0.9em; font-weight: 600; margin-right: 8px; margin-bottom: 8px;
|
|
27
|
+
}
|
|
28
|
+
button.primary { background: #4a90d9; color: #fff; }
|
|
29
|
+
button.primary:hover { background: #357abd; }
|
|
30
|
+
button.secondary { background: #6c757d; color: #fff; }
|
|
31
|
+
button.secondary:hover { background: #5a6268; }
|
|
32
|
+
button.danger { background: #dc3545; color: #fff; }
|
|
33
|
+
button.danger:hover { background: #c82333; }
|
|
34
|
+
button.success { background: #28a745; color: #fff; }
|
|
35
|
+
button.success:hover { background: #218838; }
|
|
36
|
+
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
37
|
+
|
|
38
|
+
.status { padding: 8px 12px; border-radius: 4px; margin-top: 10px; font-size: 0.9em; }
|
|
39
|
+
.status.ok { background: #d4edda; color: #155724; }
|
|
40
|
+
.status.error { background: #f8d7da; color: #721c24; }
|
|
41
|
+
.status.info { background: #d1ecf1; color: #0c5460; }
|
|
42
|
+
.status.warn { background: #fff3cd; color: #856404; }
|
|
43
|
+
|
|
44
|
+
.table-list { max-height: 300px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 8px; margin: 10px 0; }
|
|
45
|
+
.table-item { padding: 4px 8px; display: flex; align-items: center; }
|
|
46
|
+
.table-item:hover { background: #f0f0f0; }
|
|
47
|
+
.table-item input[type="checkbox"] { margin-right: 8px; width: auto; }
|
|
48
|
+
.table-item label { display: inline; font-weight: normal; margin-bottom: 0; cursor: pointer; }
|
|
49
|
+
|
|
50
|
+
.progress-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
|
51
|
+
.progress-table th, .progress-table td { text-align: left; padding: 6px 12px; border-bottom: 1px solid #eee; font-size: 0.9em; }
|
|
52
|
+
.progress-table th { background: #f8f9fa; font-weight: 600; }
|
|
53
|
+
|
|
54
|
+
.progress-bar-container { width: 120px; height: 16px; background: #e9ecef; border-radius: 8px; overflow: hidden; display: inline-block; vertical-align: middle; }
|
|
55
|
+
.progress-bar-fill { height: 100%; background: #28a745; transition: width 0.3s; }
|
|
56
|
+
|
|
57
|
+
.inline-group { display: flex; gap: 8px; align-items: flex-end; margin-bottom: 10px; }
|
|
58
|
+
.inline-group > div { flex: 1; }
|
|
59
|
+
|
|
60
|
+
a { color: #4a90d9; }
|
|
61
|
+
|
|
62
|
+
select { background: #fff; }
|
|
63
|
+
|
|
64
|
+
.data-table { width: 100%; border-collapse: collapse; font-size: 0.8em; font-family: monospace; }
|
|
65
|
+
.data-table th { background: #f8f9fa; font-weight: 600; text-align: left; padding: 4px 8px; border: 1px solid #ddd; white-space: nowrap; position: sticky; top: 0; }
|
|
66
|
+
.data-table td { padding: 4px 8px; border: 1px solid #eee; white-space: nowrap; max-width: 300px; overflow: hidden; text-overflow: ellipsis; }
|
|
67
|
+
.data-table tr:nth-child(even) { background: #fafafa; }
|
|
68
|
+
.data-table tr:hover { background: #f0f7ff; }
|
|
69
|
+
</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
|
|
73
|
+
<h1>Retold Data Cloner</h1>
|
|
74
|
+
|
|
75
|
+
<!-- ======== Section 1: Connection Management ======== -->
|
|
76
|
+
<div class="section">
|
|
77
|
+
<h2>1. Database Connection</h2>
|
|
78
|
+
<p>Use the <a href="/meadow-migrationmanager/" target="_blank">Migration Manager UI</a> to configure and test database connections. The local SQLite database is pre-configured.</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- ======== Section 2: Remote Session ======== -->
|
|
82
|
+
<div class="section">
|
|
83
|
+
<h2>2. Remote Session</h2>
|
|
84
|
+
|
|
85
|
+
<div class="inline-group">
|
|
86
|
+
<div style="flex:2">
|
|
87
|
+
<label for="serverURL">Remote Server URL</label>
|
|
88
|
+
<input type="text" id="serverURL" placeholder="http://remote-server:8086" value="">
|
|
89
|
+
</div>
|
|
90
|
+
<div style="flex:1">
|
|
91
|
+
<label for="authMethod">Auth Method</label>
|
|
92
|
+
<input type="text" id="authMethod" placeholder="get" value="get">
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<details style="margin-bottom:10px">
|
|
97
|
+
<summary style="cursor:pointer; font-size:0.9em; color:#666">Advanced Session Options</summary>
|
|
98
|
+
<div style="padding:10px 0">
|
|
99
|
+
<label for="authURI">Authentication URI Template (leave blank for default)</label>
|
|
100
|
+
<input type="text" id="authURI" placeholder="/1.0/Authenticate/{~D:Record.UserName~}/{~D:Record.Password~}">
|
|
101
|
+
<label for="checkURI">Check Session URI Template</label>
|
|
102
|
+
<input type="text" id="checkURI" placeholder="/1.0/CheckSession">
|
|
103
|
+
<label for="cookieName">Cookie Name</label>
|
|
104
|
+
<input type="text" id="cookieName" placeholder="SessionID" value="SessionID">
|
|
105
|
+
<label for="cookieValueAddr">Cookie Value Address</label>
|
|
106
|
+
<input type="text" id="cookieValueAddr" placeholder="SessionID" value="SessionID">
|
|
107
|
+
<label for="cookieValueTemplate">Cookie Value Template (overrides Address if set)</label>
|
|
108
|
+
<input type="text" id="cookieValueTemplate" placeholder="{~D:Record.SessionID~}">
|
|
109
|
+
<label for="loginMarker">Login Marker</label>
|
|
110
|
+
<input type="text" id="loginMarker" placeholder="LoggedIn" value="LoggedIn">
|
|
111
|
+
</div>
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
<button class="primary" onclick="configureSession()">Configure Session</button>
|
|
115
|
+
<div id="sessionConfigStatus"></div>
|
|
116
|
+
|
|
117
|
+
<hr style="margin:16px 0; border:none; border-top:1px solid #eee">
|
|
118
|
+
|
|
119
|
+
<div class="inline-group">
|
|
120
|
+
<div>
|
|
121
|
+
<label for="userName">Username</label>
|
|
122
|
+
<input type="text" id="userName" placeholder="username">
|
|
123
|
+
</div>
|
|
124
|
+
<div>
|
|
125
|
+
<label for="password">Password</label>
|
|
126
|
+
<input type="password" id="password" placeholder="password">
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<button class="success" onclick="authenticate()">Authenticate</button>
|
|
131
|
+
<button class="secondary" onclick="checkSession()">Check Session</button>
|
|
132
|
+
<button class="danger" onclick="deauthenticate()">Deauthenticate</button>
|
|
133
|
+
<div id="sessionAuthStatus"></div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- ======== Section 3: Schema ======== -->
|
|
137
|
+
<div class="section">
|
|
138
|
+
<h2>3. Remote Schema</h2>
|
|
139
|
+
|
|
140
|
+
<label for="schemaURL">Schema URL (leave blank for default: /1.0/Retold/Models)</label>
|
|
141
|
+
<input type="text" id="schemaURL" placeholder="http://remote-server:8086/1.0/Retold/Models">
|
|
142
|
+
|
|
143
|
+
<button class="primary" onclick="fetchSchema()">Fetch Schema</button>
|
|
144
|
+
<div id="schemaStatus"></div>
|
|
145
|
+
|
|
146
|
+
<div id="tableSelection" style="display:none">
|
|
147
|
+
<h3 style="margin:12px 0 8px; font-size:1em;">Select Tables</h3>
|
|
148
|
+
<div style="display:flex; gap:8px; align-items:center; margin-bottom:8px">
|
|
149
|
+
<input type="text" id="tableFilter" placeholder="Filter tables..." style="flex:1; margin-bottom:0" oninput="filterTableList()">
|
|
150
|
+
<button class="secondary" onclick="selectAllTables(true)" style="font-size:0.8em">Select All</button>
|
|
151
|
+
<button class="secondary" onclick="selectAllTables(false)" style="font-size:0.8em">Deselect All</button>
|
|
152
|
+
<span id="tableSelectionCount" style="font-size:0.85em; color:#666; white-space:nowrap"></span>
|
|
153
|
+
</div>
|
|
154
|
+
<div id="tableList" class="table-list"></div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<!-- ======== Section 4: Deploy ======== -->
|
|
159
|
+
<div class="section">
|
|
160
|
+
<h2>4. Deploy Schema</h2>
|
|
161
|
+
<p style="font-size:0.9em; color:#666; margin-bottom:10px">Creates the selected tables in the local SQLite database and sets up CRUD endpoints (e.g. GET /1.0/Documents).</p>
|
|
162
|
+
<button class="primary" onclick="deploySchema()">Deploy Selected Tables</button>
|
|
163
|
+
<button class="danger" onclick="resetDatabase()">Reset Database</button>
|
|
164
|
+
<div id="deployStatus"></div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- ======== Section 5: Sync ======== -->
|
|
168
|
+
<!-- NOTE: Section 6 (View Data) is below the Sync section -->
|
|
169
|
+
<div class="section">
|
|
170
|
+
<h2>5. Synchronize Data</h2>
|
|
171
|
+
|
|
172
|
+
<div class="inline-group">
|
|
173
|
+
<div style="flex:0 0 150px">
|
|
174
|
+
<label for="pageSize">Page Size</label>
|
|
175
|
+
<input type="number" id="pageSize" value="100" min="1" max="10000">
|
|
176
|
+
</div>
|
|
177
|
+
<div style="flex:1; display:flex; align-items:flex-end; gap:8px">
|
|
178
|
+
<button class="success" onclick="startSync()">Start Sync</button>
|
|
179
|
+
<button class="danger" onclick="stopSync()">Stop Sync</button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div id="syncStatus"></div>
|
|
184
|
+
<div id="syncProgress"></div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<!-- ======== Section 6: View Data ======== -->
|
|
188
|
+
<div class="section">
|
|
189
|
+
<h2>6. View Data</h2>
|
|
190
|
+
<div class="inline-group">
|
|
191
|
+
<div style="flex:1">
|
|
192
|
+
<label for="viewTable">Table</label>
|
|
193
|
+
<select id="viewTable" style="width:100%; padding:8px 12px; border:1px solid #ccc; border-radius:4px; font-size:0.95em; margin-bottom:10px;">
|
|
194
|
+
<option value="">— deploy tables first —</option>
|
|
195
|
+
</select>
|
|
196
|
+
</div>
|
|
197
|
+
<div style="flex:0 0 120px">
|
|
198
|
+
<label for="viewLimit">Max Rows</label>
|
|
199
|
+
<input type="number" id="viewLimit" value="100" min="1" max="10000">
|
|
200
|
+
</div>
|
|
201
|
+
<div style="flex:0 0 auto; display:flex; align-items:flex-end">
|
|
202
|
+
<button class="primary" onclick="loadTableData()">Load</button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
<div id="viewStatus"></div>
|
|
206
|
+
<div id="viewDataContainer" style="overflow-x:auto; margin-top:10px"></div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<script>
|
|
210
|
+
// ================================================================
|
|
211
|
+
// LocalStorage Persistence
|
|
212
|
+
// ================================================================
|
|
213
|
+
|
|
214
|
+
var _PersistFields = [
|
|
215
|
+
'serverURL', 'authMethod', 'authURI', 'checkURI',
|
|
216
|
+
'cookieName', 'cookieValueAddr', 'cookieValueTemplate', 'loginMarker',
|
|
217
|
+
'userName', 'password', 'schemaURL', 'pageSize'
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
function saveField(pFieldId)
|
|
221
|
+
{
|
|
222
|
+
var el = document.getElementById(pFieldId);
|
|
223
|
+
if (el)
|
|
224
|
+
{
|
|
225
|
+
localStorage.setItem('dataCloner_' + pFieldId, el.value);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function restoreFields()
|
|
230
|
+
{
|
|
231
|
+
for (var i = 0; i < _PersistFields.length; i++)
|
|
232
|
+
{
|
|
233
|
+
var tmpId = _PersistFields[i];
|
|
234
|
+
var tmpSaved = localStorage.getItem('dataCloner_' + tmpId);
|
|
235
|
+
if (tmpSaved !== null)
|
|
236
|
+
{
|
|
237
|
+
var el = document.getElementById(tmpId);
|
|
238
|
+
if (el) el.value = tmpSaved;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Attach change listeners to all persisted fields
|
|
244
|
+
function initPersistence()
|
|
245
|
+
{
|
|
246
|
+
restoreFields();
|
|
247
|
+
for (var i = 0; i < _PersistFields.length; i++)
|
|
248
|
+
{
|
|
249
|
+
(function(pId)
|
|
250
|
+
{
|
|
251
|
+
var el = document.getElementById(pId);
|
|
252
|
+
if (el)
|
|
253
|
+
{
|
|
254
|
+
el.addEventListener('input', function() { saveField(pId); });
|
|
255
|
+
}
|
|
256
|
+
})(_PersistFields[i]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ================================================================
|
|
261
|
+
// API Helper
|
|
262
|
+
// ================================================================
|
|
263
|
+
|
|
264
|
+
function api(method, path, body)
|
|
265
|
+
{
|
|
266
|
+
var opts = { method: method, headers: {} };
|
|
267
|
+
if (body)
|
|
268
|
+
{
|
|
269
|
+
opts.headers['Content-Type'] = 'application/json';
|
|
270
|
+
opts.body = JSON.stringify(body);
|
|
271
|
+
}
|
|
272
|
+
return fetch(path, opts).then(function(r) { return r.json(); });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function setStatus(elementId, message, type)
|
|
276
|
+
{
|
|
277
|
+
var el = document.getElementById(elementId);
|
|
278
|
+
el.className = 'status ' + (type || 'info');
|
|
279
|
+
el.textContent = message;
|
|
280
|
+
el.style.display = 'block';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ================================================================
|
|
284
|
+
// Session Management
|
|
285
|
+
// ================================================================
|
|
286
|
+
|
|
287
|
+
function configureSession()
|
|
288
|
+
{
|
|
289
|
+
var serverURL = document.getElementById('serverURL').value.trim();
|
|
290
|
+
if (!serverURL)
|
|
291
|
+
{
|
|
292
|
+
setStatus('sessionConfigStatus', 'Server URL is required.', 'error');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
var body = { ServerURL: serverURL };
|
|
297
|
+
|
|
298
|
+
var authMethod = document.getElementById('authMethod').value.trim();
|
|
299
|
+
if (authMethod) body.AuthenticationMethod = authMethod;
|
|
300
|
+
|
|
301
|
+
var authURI = document.getElementById('authURI').value.trim();
|
|
302
|
+
if (authURI) body.AuthenticationURITemplate = authURI;
|
|
303
|
+
|
|
304
|
+
var checkURI = document.getElementById('checkURI').value.trim();
|
|
305
|
+
if (checkURI) body.CheckSessionURITemplate = checkURI;
|
|
306
|
+
|
|
307
|
+
var cookieName = document.getElementById('cookieName').value.trim();
|
|
308
|
+
if (cookieName) body.CookieName = cookieName;
|
|
309
|
+
|
|
310
|
+
var cookieValueAddr = document.getElementById('cookieValueAddr').value.trim();
|
|
311
|
+
if (cookieValueAddr) body.CookieValueAddress = cookieValueAddr;
|
|
312
|
+
|
|
313
|
+
var cookieValueTemplate = document.getElementById('cookieValueTemplate').value.trim();
|
|
314
|
+
if (cookieValueTemplate) body.CookieValueTemplate = cookieValueTemplate;
|
|
315
|
+
|
|
316
|
+
var loginMarker = document.getElementById('loginMarker').value.trim();
|
|
317
|
+
if (loginMarker) body.CheckSessionLoginMarker = loginMarker;
|
|
318
|
+
|
|
319
|
+
setStatus('sessionConfigStatus', 'Configuring session...', 'info');
|
|
320
|
+
|
|
321
|
+
api('POST', '/clone/session/configure', body)
|
|
322
|
+
.then(function(data)
|
|
323
|
+
{
|
|
324
|
+
if (data.Success)
|
|
325
|
+
{
|
|
326
|
+
setStatus('sessionConfigStatus', 'Session configured for ' + data.ServerURL + ' (domain: ' + data.DomainMatch + ')', 'ok');
|
|
327
|
+
}
|
|
328
|
+
else
|
|
329
|
+
{
|
|
330
|
+
setStatus('sessionConfigStatus', 'Configuration failed: ' + (data.Error || 'Unknown error'), 'error');
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
.catch(function(err)
|
|
334
|
+
{
|
|
335
|
+
setStatus('sessionConfigStatus', 'Request failed: ' + err.message, 'error');
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function authenticate()
|
|
340
|
+
{
|
|
341
|
+
var userName = document.getElementById('userName').value.trim();
|
|
342
|
+
var password = document.getElementById('password').value.trim();
|
|
343
|
+
|
|
344
|
+
if (!userName || !password)
|
|
345
|
+
{
|
|
346
|
+
setStatus('sessionAuthStatus', 'Username and password are required.', 'error');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
setStatus('sessionAuthStatus', 'Authenticating...', 'info');
|
|
351
|
+
|
|
352
|
+
api('POST', '/clone/session/authenticate', { UserName: userName, Password: password })
|
|
353
|
+
.then(function(data)
|
|
354
|
+
{
|
|
355
|
+
if (data.Success && data.Authenticated)
|
|
356
|
+
{
|
|
357
|
+
setStatus('sessionAuthStatus', 'Authenticated successfully.', 'ok');
|
|
358
|
+
}
|
|
359
|
+
else
|
|
360
|
+
{
|
|
361
|
+
setStatus('sessionAuthStatus', 'Authentication failed: ' + (data.Error || 'Not authenticated'), 'error');
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
.catch(function(err)
|
|
365
|
+
{
|
|
366
|
+
setStatus('sessionAuthStatus', 'Request failed: ' + err.message, 'error');
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function checkSession()
|
|
371
|
+
{
|
|
372
|
+
setStatus('sessionAuthStatus', 'Checking session...', 'info');
|
|
373
|
+
|
|
374
|
+
api('GET', '/clone/session/check')
|
|
375
|
+
.then(function(data)
|
|
376
|
+
{
|
|
377
|
+
if (data.Authenticated)
|
|
378
|
+
{
|
|
379
|
+
setStatus('sessionAuthStatus', 'Session is active. Server: ' + (data.ServerURL || 'N/A'), 'ok');
|
|
380
|
+
}
|
|
381
|
+
else if (data.Configured)
|
|
382
|
+
{
|
|
383
|
+
setStatus('sessionAuthStatus', 'Session configured but not authenticated.', 'warn');
|
|
384
|
+
}
|
|
385
|
+
else
|
|
386
|
+
{
|
|
387
|
+
setStatus('sessionAuthStatus', 'No session configured.', 'warn');
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
.catch(function(err)
|
|
391
|
+
{
|
|
392
|
+
setStatus('sessionAuthStatus', 'Request failed: ' + err.message, 'error');
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function deauthenticate()
|
|
397
|
+
{
|
|
398
|
+
api('POST', '/clone/session/deauthenticate')
|
|
399
|
+
.then(function(data)
|
|
400
|
+
{
|
|
401
|
+
setStatus('sessionAuthStatus', 'Session deauthenticated.', 'info');
|
|
402
|
+
})
|
|
403
|
+
.catch(function(err)
|
|
404
|
+
{
|
|
405
|
+
setStatus('sessionAuthStatus', 'Request failed: ' + err.message, 'error');
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ================================================================
|
|
410
|
+
// Schema Management
|
|
411
|
+
// ================================================================
|
|
412
|
+
|
|
413
|
+
var _FetchedTables = [];
|
|
414
|
+
|
|
415
|
+
function fetchSchema()
|
|
416
|
+
{
|
|
417
|
+
var schemaURL = document.getElementById('schemaURL').value.trim();
|
|
418
|
+
var body = {};
|
|
419
|
+
if (schemaURL) body.SchemaURL = schemaURL;
|
|
420
|
+
|
|
421
|
+
setStatus('schemaStatus', 'Fetching schema...', 'info');
|
|
422
|
+
|
|
423
|
+
api('POST', '/clone/schema/fetch', body)
|
|
424
|
+
.then(function(data)
|
|
425
|
+
{
|
|
426
|
+
if (data.Success)
|
|
427
|
+
{
|
|
428
|
+
_FetchedTables = data.Tables || [];
|
|
429
|
+
setStatus('schemaStatus', 'Fetched ' + data.TableCount + ' tables from ' + data.SchemaURL, 'ok');
|
|
430
|
+
renderTableList();
|
|
431
|
+
}
|
|
432
|
+
else
|
|
433
|
+
{
|
|
434
|
+
setStatus('schemaStatus', 'Fetch failed: ' + (data.Error || 'Unknown error'), 'error');
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
.catch(function(err)
|
|
438
|
+
{
|
|
439
|
+
setStatus('schemaStatus', 'Request failed: ' + err.message, 'error');
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function loadSavedSelections()
|
|
444
|
+
{
|
|
445
|
+
try
|
|
446
|
+
{
|
|
447
|
+
var tmpRaw = localStorage.getItem('dataCloner_selectedTables');
|
|
448
|
+
if (tmpRaw) return JSON.parse(tmpRaw);
|
|
449
|
+
}
|
|
450
|
+
catch (e) { /* ignore */ }
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function saveSelections()
|
|
455
|
+
{
|
|
456
|
+
var tmpSelected = getSelectedTables();
|
|
457
|
+
localStorage.setItem('dataCloner_selectedTables', JSON.stringify(tmpSelected));
|
|
458
|
+
updateSelectionCount();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function updateSelectionCount()
|
|
462
|
+
{
|
|
463
|
+
var tmpCount = getSelectedTables().length;
|
|
464
|
+
var tmpEl = document.getElementById('tableSelectionCount');
|
|
465
|
+
if (tmpEl) tmpEl.textContent = tmpCount + ' / ' + _FetchedTables.length + ' selected';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function renderTableList()
|
|
469
|
+
{
|
|
470
|
+
var container = document.getElementById('tableList');
|
|
471
|
+
container.innerHTML = '';
|
|
472
|
+
|
|
473
|
+
// Load previously saved selections; if none, default to none checked
|
|
474
|
+
var tmpSaved = loadSavedSelections();
|
|
475
|
+
var tmpSavedSet = null;
|
|
476
|
+
if (tmpSaved)
|
|
477
|
+
{
|
|
478
|
+
tmpSavedSet = {};
|
|
479
|
+
for (var s = 0; s < tmpSaved.length; s++) tmpSavedSet[tmpSaved[s]] = true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (var i = 0; i < _FetchedTables.length; i++)
|
|
483
|
+
{
|
|
484
|
+
var name = _FetchedTables[i];
|
|
485
|
+
var div = document.createElement('div');
|
|
486
|
+
div.className = 'table-item';
|
|
487
|
+
div.setAttribute('data-table', name.toLowerCase());
|
|
488
|
+
|
|
489
|
+
var checkbox = document.createElement('input');
|
|
490
|
+
checkbox.type = 'checkbox';
|
|
491
|
+
checkbox.id = 'tbl_' + name;
|
|
492
|
+
checkbox.value = name;
|
|
493
|
+
// If we have saved selections, restore them; otherwise default unchecked
|
|
494
|
+
checkbox.checked = tmpSavedSet ? (tmpSavedSet[name] === true) : false;
|
|
495
|
+
checkbox.addEventListener('change', saveSelections);
|
|
496
|
+
|
|
497
|
+
var label = document.createElement('label');
|
|
498
|
+
label.htmlFor = 'tbl_' + name;
|
|
499
|
+
label.textContent = name;
|
|
500
|
+
|
|
501
|
+
div.appendChild(checkbox);
|
|
502
|
+
div.appendChild(label);
|
|
503
|
+
container.appendChild(div);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
document.getElementById('tableSelection').style.display = _FetchedTables.length > 0 ? 'block' : 'none';
|
|
507
|
+
document.getElementById('tableFilter').value = '';
|
|
508
|
+
updateSelectionCount();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function filterTableList()
|
|
512
|
+
{
|
|
513
|
+
var tmpFilter = document.getElementById('tableFilter').value.toLowerCase().trim();
|
|
514
|
+
var tmpItems = document.getElementById('tableList').children;
|
|
515
|
+
for (var i = 0; i < tmpItems.length; i++)
|
|
516
|
+
{
|
|
517
|
+
var tmpName = tmpItems[i].getAttribute('data-table') || '';
|
|
518
|
+
tmpItems[i].style.display = (!tmpFilter || tmpName.indexOf(tmpFilter) >= 0) ? '' : 'none';
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function selectAllTables(pChecked)
|
|
523
|
+
{
|
|
524
|
+
// Only affect visible (non-filtered) items
|
|
525
|
+
var tmpFilter = document.getElementById('tableFilter').value.toLowerCase().trim();
|
|
526
|
+
for (var i = 0; i < _FetchedTables.length; i++)
|
|
527
|
+
{
|
|
528
|
+
var name = _FetchedTables[i];
|
|
529
|
+
if (tmpFilter && name.toLowerCase().indexOf(tmpFilter) < 0) continue;
|
|
530
|
+
var cb = document.getElementById('tbl_' + name);
|
|
531
|
+
if (cb) cb.checked = pChecked;
|
|
532
|
+
}
|
|
533
|
+
saveSelections();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function getSelectedTables()
|
|
537
|
+
{
|
|
538
|
+
var selected = [];
|
|
539
|
+
for (var i = 0; i < _FetchedTables.length; i++)
|
|
540
|
+
{
|
|
541
|
+
var cb = document.getElementById('tbl_' + _FetchedTables[i]);
|
|
542
|
+
if (cb && cb.checked) selected.push(_FetchedTables[i]);
|
|
543
|
+
}
|
|
544
|
+
return selected;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ================================================================
|
|
548
|
+
// Deploy
|
|
549
|
+
// ================================================================
|
|
550
|
+
|
|
551
|
+
function deploySchema()
|
|
552
|
+
{
|
|
553
|
+
var selectedTables = getSelectedTables();
|
|
554
|
+
|
|
555
|
+
if (selectedTables.length === 0)
|
|
556
|
+
{
|
|
557
|
+
setStatus('deployStatus', 'No tables selected. Fetch a schema and select tables first.', 'error');
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
setStatus('deployStatus', 'Deploying ' + selectedTables.length + ' tables...', 'info');
|
|
562
|
+
|
|
563
|
+
api('POST', '/clone/schema/deploy', { Tables: selectedTables })
|
|
564
|
+
.then(function(data)
|
|
565
|
+
{
|
|
566
|
+
if (data.Success)
|
|
567
|
+
{
|
|
568
|
+
setStatus('deployStatus', data.Message, 'ok');
|
|
569
|
+
_DeployedTables = data.TablesDeployed || selectedTables;
|
|
570
|
+
saveDeployedTables();
|
|
571
|
+
populateViewTableDropdown();
|
|
572
|
+
}
|
|
573
|
+
else
|
|
574
|
+
{
|
|
575
|
+
setStatus('deployStatus', 'Deploy failed: ' + (data.Error || 'Unknown error'), 'error');
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
.catch(function(err)
|
|
579
|
+
{
|
|
580
|
+
setStatus('deployStatus', 'Request failed: ' + err.message, 'error');
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function resetDatabase()
|
|
585
|
+
{
|
|
586
|
+
if (!confirm('This will delete ALL data in the local SQLite database. Continue?'))
|
|
587
|
+
{
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
setStatus('deployStatus', 'Resetting database...', 'info');
|
|
592
|
+
|
|
593
|
+
api('POST', '/clone/reset')
|
|
594
|
+
.then(function(data)
|
|
595
|
+
{
|
|
596
|
+
if (data.Success)
|
|
597
|
+
{
|
|
598
|
+
setStatus('deployStatus', data.Message, 'ok');
|
|
599
|
+
// Clear the sync progress display
|
|
600
|
+
document.getElementById('syncProgress').innerHTML = '';
|
|
601
|
+
}
|
|
602
|
+
else
|
|
603
|
+
{
|
|
604
|
+
setStatus('deployStatus', 'Reset failed: ' + (data.Error || 'Unknown error'), 'error');
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
.catch(function(err)
|
|
608
|
+
{
|
|
609
|
+
setStatus('deployStatus', 'Request failed: ' + err.message, 'error');
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ================================================================
|
|
614
|
+
// Sync
|
|
615
|
+
// ================================================================
|
|
616
|
+
|
|
617
|
+
var _SyncPollTimer = null;
|
|
618
|
+
|
|
619
|
+
function startSync()
|
|
620
|
+
{
|
|
621
|
+
var selectedTables = getSelectedTables();
|
|
622
|
+
var pageSize = parseInt(document.getElementById('pageSize').value, 10) || 100;
|
|
623
|
+
|
|
624
|
+
if (selectedTables.length === 0)
|
|
625
|
+
{
|
|
626
|
+
setStatus('syncStatus', 'No tables selected for sync.', 'error');
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
setStatus('syncStatus', 'Starting sync...', 'info');
|
|
631
|
+
|
|
632
|
+
api('POST', '/clone/sync/start', { Tables: selectedTables, PageSize: pageSize })
|
|
633
|
+
.then(function(data)
|
|
634
|
+
{
|
|
635
|
+
if (data.Success)
|
|
636
|
+
{
|
|
637
|
+
setStatus('syncStatus', 'Sync started for ' + data.Tables.length + ' tables.', 'ok');
|
|
638
|
+
startPolling();
|
|
639
|
+
}
|
|
640
|
+
else
|
|
641
|
+
{
|
|
642
|
+
setStatus('syncStatus', 'Sync start failed: ' + (data.Error || 'Unknown error'), 'error');
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
.catch(function(err)
|
|
646
|
+
{
|
|
647
|
+
setStatus('syncStatus', 'Request failed: ' + err.message, 'error');
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function stopSync()
|
|
652
|
+
{
|
|
653
|
+
api('POST', '/clone/sync/stop')
|
|
654
|
+
.then(function(data)
|
|
655
|
+
{
|
|
656
|
+
setStatus('syncStatus', 'Sync stop requested.', 'warn');
|
|
657
|
+
})
|
|
658
|
+
.catch(function(err)
|
|
659
|
+
{
|
|
660
|
+
setStatus('syncStatus', 'Request failed: ' + err.message, 'error');
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function startPolling()
|
|
665
|
+
{
|
|
666
|
+
if (_SyncPollTimer) clearInterval(_SyncPollTimer);
|
|
667
|
+
_SyncPollTimer = setInterval(pollSyncStatus, 2000);
|
|
668
|
+
pollSyncStatus();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function stopPolling()
|
|
672
|
+
{
|
|
673
|
+
if (_SyncPollTimer)
|
|
674
|
+
{
|
|
675
|
+
clearInterval(_SyncPollTimer);
|
|
676
|
+
_SyncPollTimer = null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function pollSyncStatus()
|
|
681
|
+
{
|
|
682
|
+
api('GET', '/clone/sync/status')
|
|
683
|
+
.then(function(data)
|
|
684
|
+
{
|
|
685
|
+
renderSyncProgress(data);
|
|
686
|
+
|
|
687
|
+
if (!data.Running && !data.Stopping)
|
|
688
|
+
{
|
|
689
|
+
stopPolling();
|
|
690
|
+
if (Object.keys(data.Tables || {}).length > 0)
|
|
691
|
+
{
|
|
692
|
+
// Check if any tables had errors or partial sync
|
|
693
|
+
var tables = data.Tables || {};
|
|
694
|
+
var hasErrors = false;
|
|
695
|
+
var hasPartial = false;
|
|
696
|
+
var names = Object.keys(tables);
|
|
697
|
+
for (var i = 0; i < names.length; i++)
|
|
698
|
+
{
|
|
699
|
+
if (tables[names[i]].Status === 'Error') hasErrors = true;
|
|
700
|
+
if (tables[names[i]].Status === 'Partial') hasPartial = true;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (hasErrors)
|
|
704
|
+
{
|
|
705
|
+
setStatus('syncStatus', 'Sync finished with errors. Check the table below for details.', 'error');
|
|
706
|
+
}
|
|
707
|
+
else if (hasPartial)
|
|
708
|
+
{
|
|
709
|
+
setStatus('syncStatus', 'Sync finished. Some records were skipped (GUID conflicts or permission issues).', 'warn');
|
|
710
|
+
}
|
|
711
|
+
else
|
|
712
|
+
{
|
|
713
|
+
setStatus('syncStatus', 'Sync complete.', 'ok');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
})
|
|
718
|
+
.catch(function(err)
|
|
719
|
+
{
|
|
720
|
+
// Silently ignore poll errors
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function renderSyncProgress(data)
|
|
725
|
+
{
|
|
726
|
+
var container = document.getElementById('syncProgress');
|
|
727
|
+
var tables = data.Tables || {};
|
|
728
|
+
var tableNames = Object.keys(tables);
|
|
729
|
+
|
|
730
|
+
if (tableNames.length === 0)
|
|
731
|
+
{
|
|
732
|
+
container.innerHTML = '';
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
var html = '<table class="progress-table">';
|
|
737
|
+
html += '<tr><th>Table</th><th>Status</th><th>Progress</th><th>Synced</th><th>Details</th></tr>';
|
|
738
|
+
|
|
739
|
+
for (var i = 0; i < tableNames.length; i++)
|
|
740
|
+
{
|
|
741
|
+
var name = tableNames[i];
|
|
742
|
+
var t = tables[name];
|
|
743
|
+
|
|
744
|
+
// Calculate percentage: if total is 0, show 100% (nothing to sync)
|
|
745
|
+
var pct = 0;
|
|
746
|
+
if (t.Total === 0 && (t.Status === 'Complete' || t.Status === 'Error'))
|
|
747
|
+
{
|
|
748
|
+
pct = 100;
|
|
749
|
+
}
|
|
750
|
+
else if (t.Total > 0)
|
|
751
|
+
{
|
|
752
|
+
pct = Math.round((t.Synced / t.Total) * 100);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Color the progress bar based on status
|
|
756
|
+
var barColor = '#28a745'; // green
|
|
757
|
+
if (t.Status === 'Error') barColor = '#dc3545'; // red
|
|
758
|
+
else if (t.Status === 'Partial') barColor = '#ffc107'; // yellow
|
|
759
|
+
else if (t.Status === 'Syncing') barColor = '#4a90d9'; // blue
|
|
760
|
+
|
|
761
|
+
// Status badge
|
|
762
|
+
var statusBadge = t.Status;
|
|
763
|
+
if (t.Status === 'Complete' && t.Total === 0) statusBadge = 'Complete (empty)';
|
|
764
|
+
if (t.Status === 'Partial') statusBadge = 'Partial \u26A0';
|
|
765
|
+
if (t.Status === 'Error') statusBadge = 'Error \u2716';
|
|
766
|
+
|
|
767
|
+
// Details column
|
|
768
|
+
var details = '';
|
|
769
|
+
if (t.ErrorMessage) details = t.ErrorMessage;
|
|
770
|
+
else if (t.Skipped > 0) details = t.Skipped + ' record(s) skipped';
|
|
771
|
+
else if ((t.Errors || 0) > 0) details = t.Errors + ' error(s)';
|
|
772
|
+
else if (t.Status === 'Complete' && t.Total === 0) details = 'No records on server';
|
|
773
|
+
else if (t.Status === 'Complete') details = '\u2714 OK';
|
|
774
|
+
|
|
775
|
+
html += '<tr>';
|
|
776
|
+
html += '<td><strong>' + name + '</strong></td>';
|
|
777
|
+
html += '<td>' + statusBadge + '</td>';
|
|
778
|
+
html += '<td>';
|
|
779
|
+
html += '<div class="progress-bar-container"><div class="progress-bar-fill" style="width:' + pct + '%; background:' + barColor + '"></div></div>';
|
|
780
|
+
html += ' ' + pct + '%';
|
|
781
|
+
html += '</td>';
|
|
782
|
+
html += '<td>' + t.Synced + ' / ' + t.Total + '</td>';
|
|
783
|
+
html += '<td>' + details + '</td>';
|
|
784
|
+
html += '</tr>';
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
html += '</table>';
|
|
788
|
+
container.innerHTML = html;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ================================================================
|
|
792
|
+
// View Data
|
|
793
|
+
// ================================================================
|
|
794
|
+
|
|
795
|
+
var _DeployedTables = [];
|
|
796
|
+
|
|
797
|
+
function populateViewTableDropdown()
|
|
798
|
+
{
|
|
799
|
+
var tmpSelect = document.getElementById('viewTable');
|
|
800
|
+
var tmpCurrentValue = tmpSelect.value;
|
|
801
|
+
|
|
802
|
+
tmpSelect.innerHTML = '';
|
|
803
|
+
|
|
804
|
+
if (_DeployedTables.length === 0)
|
|
805
|
+
{
|
|
806
|
+
var opt = document.createElement('option');
|
|
807
|
+
opt.value = '';
|
|
808
|
+
opt.textContent = '— deploy tables first —';
|
|
809
|
+
tmpSelect.appendChild(opt);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
for (var i = 0; i < _DeployedTables.length; i++)
|
|
814
|
+
{
|
|
815
|
+
var opt = document.createElement('option');
|
|
816
|
+
opt.value = _DeployedTables[i];
|
|
817
|
+
opt.textContent = _DeployedTables[i];
|
|
818
|
+
tmpSelect.appendChild(opt);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Restore previous selection if it exists
|
|
822
|
+
if (tmpCurrentValue)
|
|
823
|
+
{
|
|
824
|
+
tmpSelect.value = tmpCurrentValue;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function loadTableData()
|
|
829
|
+
{
|
|
830
|
+
var tmpTable = document.getElementById('viewTable').value;
|
|
831
|
+
var tmpLimit = parseInt(document.getElementById('viewLimit').value, 10) || 100;
|
|
832
|
+
|
|
833
|
+
if (!tmpTable)
|
|
834
|
+
{
|
|
835
|
+
setStatus('viewStatus', 'Select a table first.', 'error');
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
setStatus('viewStatus', 'Loading ' + tmpTable + '...', 'info');
|
|
840
|
+
document.getElementById('viewDataContainer').innerHTML = '';
|
|
841
|
+
|
|
842
|
+
// Use the standard Meadow CRUD list endpoint: /1.0/{Entity}s/0/{Cap}
|
|
843
|
+
api('GET', '/1.0/' + tmpTable + 's/0/' + tmpLimit)
|
|
844
|
+
.then(function(data)
|
|
845
|
+
{
|
|
846
|
+
if (!Array.isArray(data))
|
|
847
|
+
{
|
|
848
|
+
setStatus('viewStatus', 'Unexpected response (not an array). The table may not be deployed yet.', 'error');
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
setStatus('viewStatus', data.length + ' row(s) returned' + (data.length >= tmpLimit ? ' (limit reached — increase Max Rows to see more)' : '') + '.', 'ok');
|
|
853
|
+
renderDataTable(data);
|
|
854
|
+
})
|
|
855
|
+
.catch(function(err)
|
|
856
|
+
{
|
|
857
|
+
setStatus('viewStatus', 'Request failed: ' + err.message, 'error');
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function renderDataTable(pRows)
|
|
862
|
+
{
|
|
863
|
+
var container = document.getElementById('viewDataContainer');
|
|
864
|
+
|
|
865
|
+
if (!pRows || pRows.length === 0)
|
|
866
|
+
{
|
|
867
|
+
container.innerHTML = '<p style="color:#666; font-size:0.9em; padding:8px">No rows.</p>';
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Collect all column names from the first row
|
|
872
|
+
var tmpColumns = Object.keys(pRows[0]);
|
|
873
|
+
|
|
874
|
+
var html = '<table class="data-table">';
|
|
875
|
+
html += '<thead><tr>';
|
|
876
|
+
for (var c = 0; c < tmpColumns.length; c++)
|
|
877
|
+
{
|
|
878
|
+
html += '<th>' + escapeHtml(tmpColumns[c]) + '</th>';
|
|
879
|
+
}
|
|
880
|
+
html += '</tr></thead>';
|
|
881
|
+
|
|
882
|
+
html += '<tbody>';
|
|
883
|
+
for (var r = 0; r < pRows.length; r++)
|
|
884
|
+
{
|
|
885
|
+
html += '<tr>';
|
|
886
|
+
for (var c = 0; c < tmpColumns.length; c++)
|
|
887
|
+
{
|
|
888
|
+
var val = pRows[r][tmpColumns[c]];
|
|
889
|
+
var display = (val === null || val === undefined) ? '' : String(val);
|
|
890
|
+
html += '<td title="' + escapeHtml(display) + '">' + escapeHtml(display) + '</td>';
|
|
891
|
+
}
|
|
892
|
+
html += '</tr>';
|
|
893
|
+
}
|
|
894
|
+
html += '</tbody></table>';
|
|
895
|
+
|
|
896
|
+
container.innerHTML = html;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function escapeHtml(pStr)
|
|
900
|
+
{
|
|
901
|
+
var div = document.createElement('div');
|
|
902
|
+
div.appendChild(document.createTextNode(pStr));
|
|
903
|
+
return div.innerHTML;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ================================================================
|
|
907
|
+
// Initialize
|
|
908
|
+
// ================================================================
|
|
909
|
+
|
|
910
|
+
// Persist deployed tables across page reload
|
|
911
|
+
function saveDeployedTables()
|
|
912
|
+
{
|
|
913
|
+
localStorage.setItem('dataCloner_deployedTables', JSON.stringify(_DeployedTables));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function restoreDeployedTables()
|
|
917
|
+
{
|
|
918
|
+
try
|
|
919
|
+
{
|
|
920
|
+
var tmpRaw = localStorage.getItem('dataCloner_deployedTables');
|
|
921
|
+
if (tmpRaw)
|
|
922
|
+
{
|
|
923
|
+
_DeployedTables = JSON.parse(tmpRaw);
|
|
924
|
+
populateViewTableDropdown();
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
catch (e) { /* ignore */ }
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
initPersistence();
|
|
931
|
+
restoreDeployedTables();
|
|
932
|
+
</script>
|
|
933
|
+
|
|
934
|
+
</body>
|
|
935
|
+
</html>
|