retold-facto 0.0.4

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 (92) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.dockerignore +8 -0
  3. package/.quackage.json +19 -0
  4. package/Dockerfile +26 -0
  5. package/bin/retold-facto.js +909 -0
  6. package/examples/facto-government-data.sqlite +0 -0
  7. package/examples/government-data-catalog.json +137 -0
  8. package/examples/government-data-loader.js +1432 -0
  9. package/package.json +91 -0
  10. package/scripts/facto-download.js +425 -0
  11. package/source/Retold-Facto.js +1042 -0
  12. package/source/services/Retold-Facto-BeaconProvider.js +511 -0
  13. package/source/services/Retold-Facto-CatalogManager.js +1252 -0
  14. package/source/services/Retold-Facto-DataLakeService.js +1642 -0
  15. package/source/services/Retold-Facto-DatasetManager.js +417 -0
  16. package/source/services/Retold-Facto-IngestEngine.js +1315 -0
  17. package/source/services/Retold-Facto-ProjectionEngine.js +3960 -0
  18. package/source/services/Retold-Facto-RecordManager.js +360 -0
  19. package/source/services/Retold-Facto-SchemaManager.js +1110 -0
  20. package/source/services/Retold-Facto-SourceFolderScanner.js +2243 -0
  21. package/source/services/Retold-Facto-SourceManager.js +730 -0
  22. package/source/services/Retold-Facto-StoreConnectionManager.js +441 -0
  23. package/source/services/Retold-Facto-ThroughputMonitor.js +478 -0
  24. package/source/services/web-app/codemirror-entry.js +7 -0
  25. package/source/services/web-app/pict-app/Pict-Application-Facto-Configuration.json +9 -0
  26. package/source/services/web-app/pict-app/Pict-Application-Facto.js +70 -0
  27. package/source/services/web-app/pict-app/Pict-Facto-Bundle.js +11 -0
  28. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto-UI.js +66 -0
  29. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto.js +69 -0
  30. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Catalog.js +93 -0
  31. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Connections.js +42 -0
  32. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Datasets.js +605 -0
  33. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Projections.js +188 -0
  34. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Scanner.js +80 -0
  35. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Schema.js +116 -0
  36. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Sources.js +104 -0
  37. package/source/services/web-app/pict-app/views/PictView-Facto-Catalog.js +526 -0
  38. package/source/services/web-app/pict-app/views/PictView-Facto-Datasets.js +173 -0
  39. package/source/services/web-app/pict-app/views/PictView-Facto-Ingest.js +259 -0
  40. package/source/services/web-app/pict-app/views/PictView-Facto-Layout.js +191 -0
  41. package/source/services/web-app/pict-app/views/PictView-Facto-Projections.js +231 -0
  42. package/source/services/web-app/pict-app/views/PictView-Facto-Records.js +326 -0
  43. package/source/services/web-app/pict-app/views/PictView-Facto-Scanner.js +624 -0
  44. package/source/services/web-app/pict-app/views/PictView-Facto-Sources.js +201 -0
  45. package/source/services/web-app/pict-app/views/PictView-Facto-Throughput.js +456 -0
  46. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full-Configuration.json +14 -0
  47. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full.js +391 -0
  48. package/source/services/web-app/pict-app-full/providers/PictRouter-Facto-Configuration.json +56 -0
  49. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-BottomBar.js +68 -0
  50. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Connections.js +340 -0
  51. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboard.js +149 -0
  52. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboards.js +819 -0
  53. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Datasets.js +178 -0
  54. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-IngestJobs.js +99 -0
  55. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Layout.js +62 -0
  56. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-MappingEditor.js +158 -0
  57. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-ProjectionDetail.js +1120 -0
  58. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Projections.js +172 -0
  59. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-QueryPanel.js +119 -0
  60. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-RecordViewer.js +663 -0
  61. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Records.js +648 -0
  62. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Scanner.js +1017 -0
  63. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDetail.js +1404 -0
  64. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDocEditor.js +1036 -0
  65. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaEditor.js +636 -0
  66. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaResearch.js +357 -0
  67. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceDetail.js +822 -0
  68. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceEditor.js +1036 -0
  69. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceResearch.js +487 -0
  70. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Sources.js +165 -0
  71. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Throughput.js +439 -0
  72. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-TopBar.js +335 -0
  73. package/source/services/web-app/pict-app-full/views/projections/Facto-Projections-Constants.js +71 -0
  74. package/source/services/web-app/web/chart.min.js +20 -0
  75. package/source/services/web-app/web/codemirror-bundle.js +30099 -0
  76. package/source/services/web-app/web/css/facto-themes.css +467 -0
  77. package/source/services/web-app/web/css/facto.css +502 -0
  78. package/source/services/web-app/web/index.html +28 -0
  79. package/source/services/web-app/web/retold-facto.js +12138 -0
  80. package/source/services/web-app/web/retold-facto.js.map +1 -0
  81. package/source/services/web-app/web/retold-facto.min.js +2 -0
  82. package/source/services/web-app/web/retold-facto.min.js.map +1 -0
  83. package/source/services/web-app/web/simple/index.html +17 -0
  84. package/test/Facto_Browser_Integration_tests.js +798 -0
  85. package/test/RetoldFacto_tests.js +4117 -0
  86. package/test/fixtures/weather-readings.csv +17 -0
  87. package/test/fixtures/weather-stations.csv +9 -0
  88. package/test/model/MeadowModel-Extended.json +8497 -0
  89. package/test/model/MeadowModel-PICT.json +1 -0
  90. package/test/model/MeadowModel.json +1355 -0
  91. package/test/model/ddl/Facto.ddl +225 -0
  92. package/test/model/fable-configuration.json +14 -0
@@ -0,0 +1,487 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _ViewConfiguration =
4
+ {
5
+ ViewIdentifier: "Facto-Full-SourceResearch",
6
+
7
+ DefaultRenderable: "Facto-Full-SourceResearch-Content",
8
+ DefaultDestinationAddress: "#Facto-Full-Content-Container",
9
+
10
+ AutoRender: false,
11
+
12
+ CSS: /*css*/`
13
+ .facto-research-search {
14
+ display: flex;
15
+ gap: 0.75em;
16
+ margin-bottom: 1.25em;
17
+ }
18
+ .facto-research-search input {
19
+ flex: 1;
20
+ margin-bottom: 0;
21
+ }
22
+ .facto-research-detail {
23
+ margin-top: 1.25em;
24
+ padding-top: 1.25em;
25
+ border-top: 1px solid var(--facto-border-subtle);
26
+ }
27
+ .facto-research-import textarea {
28
+ width: 100%;
29
+ font-family: 'SF Mono', Consolas, monospace;
30
+ font-size: 0.85em;
31
+ padding: 0.75em;
32
+ background: var(--facto-bg-input);
33
+ color: var(--facto-text);
34
+ border: 1px solid var(--facto-border);
35
+ border-radius: 6px;
36
+ margin-bottom: 0.5em;
37
+ }
38
+ .facto-research-add-form {
39
+ background: var(--facto-bg-card);
40
+ border: 1px solid var(--facto-border);
41
+ border-radius: 8px;
42
+ padding: 1.25em;
43
+ margin-bottom: 1.25em;
44
+ }
45
+ .facto-research-add-form .facto-form-grid {
46
+ display: grid;
47
+ grid-template-columns: 1fr 1fr;
48
+ gap: 0.75em;
49
+ }
50
+ .facto-research-add-form .facto-form-grid .facto-form-full {
51
+ grid-column: 1 / -1;
52
+ }
53
+ .facto-research-add-form label {
54
+ display: block;
55
+ font-size: 0.8em;
56
+ font-weight: 600;
57
+ margin-bottom: 0.25em;
58
+ color: var(--facto-text-muted);
59
+ }
60
+ .facto-research-add-form input,
61
+ .facto-research-add-form textarea,
62
+ .facto-research-add-form select {
63
+ width: 100%;
64
+ margin-bottom: 0;
65
+ }
66
+ .facto-research-add-form textarea {
67
+ font-size: 0.9em;
68
+ }
69
+ .facto-research-add-form .facto-form-actions {
70
+ margin-top: 1em;
71
+ display: flex;
72
+ gap: 0.5em;
73
+ }
74
+ .facto-research-header-row {
75
+ display: flex;
76
+ justify-content: space-between;
77
+ align-items: flex-start;
78
+ margin-bottom: 0;
79
+ }
80
+ `,
81
+
82
+ Templates:
83
+ [
84
+ {
85
+ Hash: "Facto-Full-SourceResearch-Template",
86
+ Template: /*html*/`
87
+ <div class="facto-content">
88
+ <div class="facto-content-header facto-research-header-row">
89
+ <div>
90
+ <h1>Source Research</h1>
91
+ <p>Research and catalog potential data sources before provisioning them as runtime Sources and Datasets.</p>
92
+ </div>
93
+ <button class="facto-btn facto-btn-success" onclick="{~P~}.views['Facto-Full-SourceResearch'].toggleAddForm()">+ Add Entry</button>
94
+ </div>
95
+
96
+ <div id="Facto-Full-Research-AddForm" style="display:none;">
97
+ <div class="facto-research-add-form">
98
+ <div class="facto-form-grid">
99
+ <div>
100
+ <label>Name</label>
101
+ <input type="text" id="Facto-Research-Add-Name" placeholder="Source name">
102
+ </div>
103
+ <div>
104
+ <label>Agency</label>
105
+ <input type="text" id="Facto-Research-Add-Agency" placeholder="Agency or organization">
106
+ </div>
107
+ <div>
108
+ <label>Type</label>
109
+ <input type="text" id="Facto-Research-Add-Type" placeholder="e.g. API, CSV, Database">
110
+ </div>
111
+ <div>
112
+ <label>Category</label>
113
+ <input type="text" id="Facto-Research-Add-Category" placeholder="e.g. Census, Financial, Health">
114
+ </div>
115
+ <div>
116
+ <label>URL</label>
117
+ <input type="text" id="Facto-Research-Add-URL" placeholder="https://...">
118
+ </div>
119
+ <div>
120
+ <label>Protocol</label>
121
+ <input type="text" id="Facto-Research-Add-Protocol" placeholder="e.g. REST, FTP, SFTP">
122
+ </div>
123
+ <div>
124
+ <label>Region</label>
125
+ <input type="text" id="Facto-Research-Add-Region" placeholder="e.g. US, EU, Global">
126
+ </div>
127
+ <div>
128
+ <label>Update Frequency</label>
129
+ <input type="text" id="Facto-Research-Add-UpdateFrequency" placeholder="e.g. Daily, Weekly, Annual">
130
+ </div>
131
+ <div class="facto-form-full">
132
+ <label>Description</label>
133
+ <textarea id="Facto-Research-Add-Description" rows="2" placeholder="Brief description of this source"></textarea>
134
+ </div>
135
+ <div class="facto-form-full">
136
+ <label>Notes</label>
137
+ <textarea id="Facto-Research-Add-Notes" rows="2" placeholder="Additional notes"></textarea>
138
+ </div>
139
+ </div>
140
+ <div class="facto-form-actions">
141
+ <button class="facto-btn facto-btn-primary" onclick="{~P~}.views['Facto-Full-SourceResearch'].createEntry()">Save Entry</button>
142
+ <button class="facto-btn facto-btn-secondary" onclick="{~P~}.views['Facto-Full-SourceResearch'].toggleAddForm()">Cancel</button>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ <div class="facto-research-search">
148
+ <input type="text" id="Facto-Full-Research-Search" placeholder="Search catalog by name, agency, category, or description...">
149
+ <button class="facto-btn facto-btn-primary" onclick="{~P~}.views['Facto-Full-SourceResearch'].searchCatalog()">Search</button>
150
+ </div>
151
+
152
+ <div id="Facto-Full-Research-List"></div>
153
+ <div id="Facto-Full-Research-Detail" class="facto-research-detail" style="display:none;"></div>
154
+
155
+ <div class="facto-section" style="margin-top:2em;">
156
+ <div class="facto-section-title">Import / Export</div>
157
+ <div class="facto-research-import">
158
+ <textarea id="Facto-Full-Research-ImportJSON" rows="4" placeholder="Paste JSON array of catalog entries here..."></textarea>
159
+ <button class="facto-btn facto-btn-primary" onclick="{~P~}.views['Facto-Full-SourceResearch'].importCatalog()">Import JSON</button>
160
+ <button class="facto-btn facto-btn-secondary" onclick="{~P~}.views['Facto-Full-SourceResearch'].exportCatalog()">Export Catalog</button>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ `
165
+ }
166
+ ],
167
+
168
+ Renderables:
169
+ [
170
+ {
171
+ RenderableHash: "Facto-Full-SourceResearch-Content",
172
+ TemplateHash: "Facto-Full-SourceResearch-Template",
173
+ DestinationAddress: "#Facto-Full-Content-Container",
174
+ RenderMethod: "replace"
175
+ }
176
+ ]
177
+ };
178
+
179
+ class FactoFullSourceResearchView extends libPictView
180
+ {
181
+ constructor(pFable, pOptions, pServiceHash)
182
+ {
183
+ super(pFable, pOptions, pServiceHash);
184
+ }
185
+
186
+ onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
187
+ {
188
+ this._SourceLinks = {};
189
+
190
+ Promise.all([
191
+ this.pict.providers.Facto.loadCatalogEntries(),
192
+ this.pict.providers.Facto.loadCatalogSourceLinks()
193
+ ]).then(
194
+ (pResults) =>
195
+ {
196
+ let tmpLinksResponse = pResults[1];
197
+ if (tmpLinksResponse && tmpLinksResponse.Links)
198
+ {
199
+ this._SourceLinks = tmpLinksResponse.Links;
200
+ }
201
+ this.refreshList();
202
+ }).catch(
203
+ (pError) =>
204
+ {
205
+ this.pict.views['Pict-Section-Modal'].toast('Error loading catalog: ' + pError.message, {type: 'error'});
206
+ });
207
+
208
+ return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
209
+ }
210
+
211
+
212
+
213
+ refreshList()
214
+ {
215
+ let tmpContainer = document.getElementById('Facto-Full-Research-List');
216
+ if (!tmpContainer) return;
217
+
218
+ let tmpEntries = this.pict.AppData.Facto.CatalogEntries;
219
+ if (!tmpEntries || tmpEntries.length === 0)
220
+ {
221
+ tmpContainer.innerHTML = '<div class="facto-empty">No catalog entries yet. Import a catalog or add sources manually.</div>';
222
+ return;
223
+ }
224
+
225
+ let tmpHtml = '<table><thead><tr><th>ID</th><th>Agency</th><th>Name</th><th>Type</th><th>Category</th><th>Region</th><th>Verified</th><th>Actions</th></tr></thead><tbody>';
226
+ for (let i = 0; i < tmpEntries.length; i++)
227
+ {
228
+ let tmpEntry = tmpEntries[i];
229
+ let tmpVerified = tmpEntry.Verified ? '<span class="facto-badge facto-badge-success">Yes</span>' : '<span class="facto-badge facto-badge-muted">No</span>';
230
+ tmpHtml += '<tr>';
231
+ tmpHtml += '<td>' + (tmpEntry.IDSourceCatalogEntry || '') + '</td>';
232
+ tmpHtml += '<td>' + (tmpEntry.Agency || '') + '</td>';
233
+ tmpHtml += '<td>' + (tmpEntry.Name || '') + '</td>';
234
+ tmpHtml += '<td><span class="facto-badge facto-badge-primary">' + (tmpEntry.Type || '') + '</span></td>';
235
+ tmpHtml += '<td>' + (tmpEntry.Category || '') + '</td>';
236
+ tmpHtml += '<td>' + (tmpEntry.Region || '') + '</td>';
237
+ tmpHtml += '<td>' + tmpVerified + '</td>';
238
+ tmpHtml += '<td>';
239
+ let tmpLinkedSource = this._SourceLinks && this._SourceLinks[tmpEntry.IDSourceCatalogEntry];
240
+ if (tmpLinkedSource)
241
+ {
242
+ tmpHtml += '<button class="facto-btn facto-btn-secondary facto-btn-small" onclick="pict.PictApplication.navigateTo(\'/Source/' + tmpLinkedSource + '\')">View Source &rarr;</button> ';
243
+ }
244
+ tmpHtml += '<button class="facto-btn facto-btn-primary facto-btn-small" onclick="pict.views[\'Facto-Full-SourceResearch\'].viewEntry(' + tmpEntry.IDSourceCatalogEntry + ')">Datasets</button> ';
245
+ tmpHtml += '<button class="facto-btn facto-btn-danger facto-btn-small" onclick="pict.views[\'Facto-Full-SourceResearch\'].deleteEntry(' + tmpEntry.IDSourceCatalogEntry + ')">Delete</button>';
246
+ tmpHtml += '</td>';
247
+ tmpHtml += '</tr>';
248
+ }
249
+ tmpHtml += '</tbody></table>';
250
+ tmpContainer.innerHTML = tmpHtml;
251
+ }
252
+
253
+ toggleAddForm()
254
+ {
255
+ let tmpForm = document.getElementById('Facto-Full-Research-AddForm');
256
+ if (!tmpForm) return;
257
+ tmpForm.style.display = (tmpForm.style.display === 'none') ? 'block' : 'none';
258
+ }
259
+
260
+ createEntry()
261
+ {
262
+ let tmpData =
263
+ {
264
+ Name: this.pict.providers.FactoUI.getVal('Facto-Research-Add-Name'),
265
+ Agency: this.pict.providers.FactoUI.getVal('Facto-Research-Add-Agency'),
266
+ Type: this.pict.providers.FactoUI.getVal('Facto-Research-Add-Type'),
267
+ Category: this.pict.providers.FactoUI.getVal('Facto-Research-Add-Category'),
268
+ URL: this.pict.providers.FactoUI.getVal('Facto-Research-Add-URL'),
269
+ Protocol: this.pict.providers.FactoUI.getVal('Facto-Research-Add-Protocol'),
270
+ Region: this.pict.providers.FactoUI.getVal('Facto-Research-Add-Region'),
271
+ UpdateFrequency: this.pict.providers.FactoUI.getVal('Facto-Research-Add-UpdateFrequency'),
272
+ Description: this.pict.providers.FactoUI.getVal('Facto-Research-Add-Description'),
273
+ Notes: this.pict.providers.FactoUI.getVal('Facto-Research-Add-Notes')
274
+ };
275
+
276
+ if (!tmpData.Name)
277
+ {
278
+ this.pict.views['Pict-Section-Modal'].toast('Name is required', {type: 'error'});
279
+ return;
280
+ }
281
+
282
+ this.pict.providers.Facto.createCatalogEntry(tmpData).then(
283
+ (pResponse) =>
284
+ {
285
+ if (pResponse && pResponse.Error)
286
+ {
287
+ this.pict.views['Pict-Section-Modal'].toast('Error: ' + pResponse.Error, {type: 'error'});
288
+ return;
289
+ }
290
+
291
+ this.pict.views['Pict-Section-Modal'].toast('Catalog entry created', {type: 'success'});
292
+
293
+ // Clear the form fields
294
+ let tmpFields = ['Name', 'Agency', 'Type', 'Category', 'URL', 'Protocol', 'Region', 'UpdateFrequency', 'Description', 'Notes'];
295
+ for (let i = 0; i < tmpFields.length; i++)
296
+ {
297
+ let tmpEl = document.getElementById('Facto-Research-Add-' + tmpFields[i]);
298
+ if (tmpEl) tmpEl.value = '';
299
+ }
300
+
301
+ // Hide the form and refresh the list
302
+ let tmpForm = document.getElementById('Facto-Full-Research-AddForm');
303
+ if (tmpForm) tmpForm.style.display = 'none';
304
+
305
+ return this.pict.providers.Facto.loadCatalogEntries();
306
+ }).then(
307
+ () =>
308
+ {
309
+ this.refreshList();
310
+ });
311
+ }
312
+
313
+ searchCatalog()
314
+ {
315
+ let tmpQuery = this.pict.providers.FactoUI.getVal('Facto-Full-Research-Search');
316
+ if (!tmpQuery)
317
+ {
318
+ this.pict.providers.Facto.loadCatalogEntries().then(
319
+ () => { this.refreshList(); });
320
+ return;
321
+ }
322
+
323
+ this.pict.providers.Facto.searchCatalog(tmpQuery).then(
324
+ (pResponse) =>
325
+ {
326
+ this.pict.AppData.Facto.CatalogEntries = (pResponse && pResponse.Entries) ? pResponse.Entries : [];
327
+ this.refreshList();
328
+ });
329
+ }
330
+
331
+ async deleteEntry(pIDEntry)
332
+ {
333
+ let tmpConfirmed = await this.pict.views['Pict-Section-Modal'].confirm('Remove this catalog entry?', { title: 'Remove Entry', confirmLabel: 'Remove', dangerous: true });
334
+ if (!tmpConfirmed) return;
335
+ this.pict.providers.Facto.deleteCatalogEntry(pIDEntry).then(
336
+ () => { return this.pict.providers.Facto.loadCatalogEntries(); }).then(
337
+ () => { this.refreshList(); this.pict.views['Pict-Section-Modal'].toast('Entry removed', {type: 'success'}); });
338
+ }
339
+
340
+ viewEntry(pIDEntry)
341
+ {
342
+ let tmpDetail = document.getElementById('Facto-Full-Research-Detail');
343
+ if (!tmpDetail) return;
344
+ tmpDetail.style.display = 'block';
345
+
346
+ this.pict.providers.Facto.loadCatalogEntryDatasets(pIDEntry).then(
347
+ (pResponse) =>
348
+ {
349
+ let tmpDatasets = (pResponse && pResponse.Datasets) ? pResponse.Datasets : [];
350
+ let tmpHtml = '<h3>Dataset Definitions for Entry #' + pIDEntry + '</h3>';
351
+
352
+ if (tmpDatasets.length === 0)
353
+ {
354
+ tmpHtml += '<div class="facto-empty">No dataset definitions yet.</div>';
355
+ }
356
+ else
357
+ {
358
+ tmpHtml += '<table><thead><tr><th>ID</th><th>Name</th><th>Format</th><th>Endpoint URL</th><th>Policy</th><th>Status</th><th>Actions</th></tr></thead><tbody>';
359
+ for (let i = 0; i < tmpDatasets.length; i++)
360
+ {
361
+ let tmpDS = tmpDatasets[i];
362
+ let tmpStatus = tmpDS.Provisioned
363
+ ? '<span class="facto-badge facto-badge-success">Provisioned</span>'
364
+ : '<span class="facto-badge facto-badge-muted">Not provisioned</span>';
365
+ let tmpAction = '';
366
+ if (tmpDS.Provisioned)
367
+ {
368
+ tmpAction = '<button class="facto-btn facto-btn-primary facto-btn-small" onclick="pict.views[\'Facto-Full-SourceResearch\'].fetchDataset(' + tmpDS.IDCatalogDatasetDefinition + ', ' + pIDEntry + ')">Fetch</button>';
369
+ if (tmpDS.IDSource)
370
+ {
371
+ tmpAction += ' <button class="facto-btn facto-btn-secondary facto-btn-small" onclick="pict.PictApplication.navigateTo(\'/Source/' + tmpDS.IDSource + '\')">View Source &rarr;</button>';
372
+ }
373
+ }
374
+ else
375
+ {
376
+ tmpAction = '<button class="facto-btn facto-btn-success facto-btn-small" onclick="pict.views[\'Facto-Full-SourceResearch\'].provisionDataset(' + tmpDS.IDCatalogDatasetDefinition + ', ' + pIDEntry + ')">Provision</button>';
377
+ }
378
+ tmpHtml += '<tr>';
379
+ tmpHtml += '<td>' + (tmpDS.IDCatalogDatasetDefinition || '') + '</td>';
380
+ tmpHtml += '<td>' + (tmpDS.Name || '') + '</td>';
381
+ tmpHtml += '<td><span class="facto-badge facto-badge-primary">' + (tmpDS.Format || '') + '</span></td>';
382
+ tmpHtml += '<td style="max-width:250px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">' + (tmpDS.EndpointURL || '') + '</td>';
383
+ tmpHtml += '<td>' + (tmpDS.VersionPolicy || 'Append') + '</td>';
384
+ tmpHtml += '<td>' + tmpStatus + '</td>';
385
+ tmpHtml += '<td>' + tmpAction + '</td>';
386
+ tmpHtml += '</tr>';
387
+ }
388
+ tmpHtml += '</tbody></table>';
389
+ }
390
+
391
+ tmpHtml += '<div style="margin-top:1em;"><button class="facto-btn facto-btn-secondary" onclick="document.getElementById(\'Facto-Full-Research-Detail\').style.display=\'none\'">Close</button></div>';
392
+ tmpDetail.innerHTML = tmpHtml;
393
+ });
394
+ }
395
+
396
+ provisionDataset(pIDCatalogDataset, pIDEntry)
397
+ {
398
+ this.pict.views['Pict-Section-Modal'].toast('Provisioning...', {type: 'info'});
399
+ this.pict.providers.Facto.provisionCatalogDataset(pIDCatalogDataset).then(
400
+ (pResponse) =>
401
+ {
402
+ if (pResponse && pResponse.Success)
403
+ {
404
+ let tmpStatusEl = document.getElementById('Facto-Full-Research-Status');
405
+ if (tmpStatusEl)
406
+ {
407
+ tmpStatusEl.className = 'facto-status facto-status-ok';
408
+ tmpStatusEl.innerHTML = 'Provisioned! Source: ' + (pResponse.Source.Hash || pResponse.Source.Name) + ' (#' + pResponse.Source.IDSource + '), Dataset: ' + (pResponse.Dataset.Hash || pResponse.Dataset.Name) + ' (#' + pResponse.Dataset.IDDataset + ') &mdash; <a href="#/Source/' + pResponse.Source.IDSource + '" style="color:var(--facto-brand);text-decoration:underline;cursor:pointer;">View Source \u2192</a>';
409
+ tmpStatusEl.style.display = 'block';
410
+ }
411
+ this.viewEntry(pIDEntry);
412
+ }
413
+ else
414
+ {
415
+ this.pict.views['Pict-Section-Modal'].toast('Error: ' + ((pResponse && pResponse.Error) || 'Unknown'), {type: 'error'});
416
+ }
417
+ });
418
+ }
419
+
420
+ fetchDataset(pIDCatalogDataset, pIDEntry)
421
+ {
422
+ this.pict.views['Pict-Section-Modal'].toast('Fetching data from endpoint...', {type: 'info'});
423
+ this.pict.providers.Facto.fetchCatalogDataset(pIDCatalogDataset).then(
424
+ (pResponse) =>
425
+ {
426
+ if (pResponse && pResponse.Success)
427
+ {
428
+ let tmpMsg = 'Fetched! ' + pResponse.Ingested + ' records ingested (v' + pResponse.DatasetVersion + ', ' + pResponse.Format + ')';
429
+ if (pResponse.IsDuplicate) tmpMsg += ' [duplicate content]';
430
+ this.pict.views['Pict-Section-Modal'].toast(tmpMsg, {type: 'success'});
431
+ this.viewEntry(pIDEntry);
432
+ }
433
+ else
434
+ {
435
+ this.pict.views['Pict-Section-Modal'].toast('Fetch error: ' + ((pResponse && pResponse.Error) || 'Unknown'), {type: 'error'});
436
+ }
437
+ });
438
+ }
439
+
440
+ importCatalog()
441
+ {
442
+ let tmpTextArea = document.getElementById('Facto-Full-Research-ImportJSON');
443
+ if (!tmpTextArea || !tmpTextArea.value)
444
+ {
445
+ this.pict.views['Pict-Section-Modal'].toast('Paste JSON to import', {type: 'warning'});
446
+ return;
447
+ }
448
+
449
+ let tmpEntries;
450
+ try { tmpEntries = JSON.parse(tmpTextArea.value); }
451
+ catch (pErr) { this.pict.views['Pict-Section-Modal'].toast('Invalid JSON: ' + pErr.message, {type: 'error'}); return; }
452
+
453
+ this.pict.providers.Facto.importCatalog(tmpEntries).then(
454
+ (pResponse) =>
455
+ {
456
+ if (pResponse && pResponse.Success)
457
+ {
458
+ this.pict.views['Pict-Section-Modal'].toast('Imported ' + pResponse.EntriesCreated + ' entries with ' + pResponse.DatasetsCreated + ' datasets', {type: 'success'});
459
+ tmpTextArea.value = '';
460
+ return this.pict.providers.Facto.loadCatalogEntries();
461
+ }
462
+ else
463
+ {
464
+ this.pict.views['Pict-Section-Modal'].toast('Import error: ' + ((pResponse && pResponse.Error) || 'Unknown'), {type: 'error'});
465
+ }
466
+ }).then(
467
+ () => { this.refreshList(); });
468
+ }
469
+
470
+ exportCatalog()
471
+ {
472
+ this.pict.providers.Facto.exportCatalog().then(
473
+ (pResponse) =>
474
+ {
475
+ let tmpTextArea = document.getElementById('Facto-Full-Research-ImportJSON');
476
+ if (tmpTextArea)
477
+ {
478
+ tmpTextArea.value = JSON.stringify(pResponse && pResponse.Entries ? pResponse.Entries : pResponse, null, 2);
479
+ }
480
+ this.pict.views['Pict-Section-Modal'].toast('Catalog exported to JSON text area', {type: 'success'});
481
+ });
482
+ }
483
+ }
484
+
485
+ module.exports = FactoFullSourceResearchView;
486
+
487
+ module.exports.default_configuration = _ViewConfiguration;
@@ -0,0 +1,165 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _ViewConfiguration =
4
+ {
5
+ ViewIdentifier: "Facto-Full-Sources",
6
+
7
+ DefaultRenderable: "Facto-Full-Sources-Content",
8
+ DefaultDestinationAddress: "#Facto-Full-Content-Container",
9
+
10
+ AutoRender: false,
11
+
12
+ Templates:
13
+ [
14
+ {
15
+ Hash: "Facto-Full-Sources-Template",
16
+ Template: /*html*/`
17
+ <div class="facto-content">
18
+ <div class="facto-content-header">
19
+ <h1>Sources</h1>
20
+ <p>Manage data sources that feed into the warehouse.</p>
21
+ </div>
22
+
23
+ <div id="Facto-Full-Sources-List"></div>
24
+
25
+ <div class="facto-section" style="margin-top:2em;">
26
+ <div class="facto-section-title">Add Source</div>
27
+ <div class="facto-inline-group">
28
+ <div>
29
+ <label>Name</label>
30
+ <input type="text" id="Facto-Full-Source-Name" placeholder="Source name">
31
+ </div>
32
+ <div>
33
+ <label>Type</label>
34
+ <select id="Facto-Full-Source-Type">
35
+ <option value="API">API</option>
36
+ <option value="File">File</option>
37
+ <option value="Database">Database</option>
38
+ <option value="Manual">Manual</option>
39
+ </select>
40
+ </div>
41
+ <div>
42
+ <label>URL</label>
43
+ <input type="text" id="Facto-Full-Source-URL" placeholder="https://...">
44
+ </div>
45
+ </div>
46
+ <button class="facto-btn facto-btn-primary" onclick="{~P~}.views['Facto-Full-Sources'].addSource()">Add Source</button>
47
+ </div>
48
+
49
+ </div>
50
+ `
51
+ }
52
+ ],
53
+
54
+ Renderables:
55
+ [
56
+ {
57
+ RenderableHash: "Facto-Full-Sources-Content",
58
+ TemplateHash: "Facto-Full-Sources-Template",
59
+ DestinationAddress: "#Facto-Full-Content-Container",
60
+ RenderMethod: "replace"
61
+ }
62
+ ]
63
+ };
64
+
65
+ class FactoFullSourcesView extends libPictView
66
+ {
67
+ constructor(pFable, pOptions, pServiceHash)
68
+ {
69
+ super(pFable, pOptions, pServiceHash);
70
+ }
71
+
72
+ onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
73
+ {
74
+ this.pict.providers.Facto.loadSources().then(
75
+ () => { this.refreshList(); }).catch(
76
+ (pError) =>
77
+ {
78
+ this.pict.views['Pict-Section-Modal'].toast('Error loading sources: ' + pError.message, {type: 'error'});
79
+ });
80
+
81
+ return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
82
+ }
83
+
84
+
85
+
86
+ refreshList()
87
+ {
88
+ let tmpContainer = document.getElementById('Facto-Full-Sources-List');
89
+ if (!tmpContainer) return;
90
+
91
+ let tmpSources = this.pict.AppData.Facto.Sources;
92
+ if (!tmpSources || tmpSources.length === 0)
93
+ {
94
+ tmpContainer.innerHTML = '<div class="facto-empty">No sources yet. Add one below or provision from Source Research.</div>';
95
+ return;
96
+ }
97
+
98
+ let tmpHtml = '<table><thead><tr><th>ID</th><th>Hash</th><th>Name</th><th>Type</th><th>URL</th><th>Active</th><th>Actions</th></tr></thead><tbody>';
99
+ for (let i = 0; i < tmpSources.length; i++)
100
+ {
101
+ let tmpSource = tmpSources[i];
102
+ let tmpActive = tmpSource.Active ? '<span class="facto-badge facto-badge-success">Active</span>' : '<span class="facto-badge facto-badge-muted">Inactive</span>';
103
+ let tmpToggleBtn = tmpSource.Active
104
+ ? '<button class="facto-btn facto-btn-secondary facto-btn-small" onclick="pict.views[\'Facto-Full-Sources\'].toggleActive(' + tmpSource.IDSource + ', false)">Deactivate</button>'
105
+ : '<button class="facto-btn facto-btn-success facto-btn-small" onclick="pict.views[\'Facto-Full-Sources\'].toggleActive(' + tmpSource.IDSource + ', true)">Activate</button>';
106
+ tmpHtml += '<tr>';
107
+ tmpHtml += '<td>' + (tmpSource.IDSource || '') + '</td>';
108
+ tmpHtml += '<td><code>' + (tmpSource.Hash || '-') + '</code></td>';
109
+ tmpHtml += '<td>' + (tmpSource.Name || '') + '</td>';
110
+ tmpHtml += '<td><span class="facto-badge facto-badge-primary">' + (tmpSource.Type || '') + '</span></td>';
111
+ tmpHtml += '<td style="max-width:250px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">' + (tmpSource.URL || '') + '</td>';
112
+ tmpHtml += '<td>' + tmpActive + '</td>';
113
+ let tmpViewBtn = '<button class="facto-btn facto-btn-primary facto-btn-small" onclick="pict.PictApplication.navigateTo(\'/Source/' + tmpSource.IDSource + '\')">View</button>';
114
+ tmpHtml += '<td>' + tmpViewBtn + ' ' + tmpToggleBtn + '</td>';
115
+ tmpHtml += '</tr>';
116
+ }
117
+ tmpHtml += '</tbody></table>';
118
+ tmpContainer.innerHTML = tmpHtml;
119
+ }
120
+
121
+ toggleActive(pIDSource, pActivate)
122
+ {
123
+ let tmpPromise = pActivate
124
+ ? this.pict.providers.Facto.activateSource(pIDSource)
125
+ : this.pict.providers.Facto.deactivateSource(pIDSource);
126
+
127
+ tmpPromise.then(
128
+ () => { return this.pict.providers.Facto.loadSources(); }).then(
129
+ () => { this.refreshList(); this.pict.views['Pict-Section-Modal'].toast(pActivate ? 'Source activated' : 'Source deactivated', {type: 'success'}); });
130
+ }
131
+
132
+ addSource()
133
+ {
134
+ let tmpName = this.pict.providers.FactoUI.getVal('Facto-Full-Source-Name');
135
+ let tmpType = this.pict.providers.FactoUI.getVal('Facto-Full-Source-Type');
136
+ let tmpURL = this.pict.providers.FactoUI.getVal('Facto-Full-Source-URL');
137
+
138
+ if (!tmpName)
139
+ {
140
+ this.pict.views['Pict-Section-Modal'].toast('Source name is required', {type: 'warning'});
141
+ return;
142
+ }
143
+
144
+ this.pict.providers.Facto.createSource({ Name: tmpName, Type: tmpType, URL: tmpURL, Active: 1 }).then(
145
+ (pResponse) =>
146
+ {
147
+ if (pResponse && pResponse.IDSource)
148
+ {
149
+ this.pict.views['Pict-Section-Modal'].toast('Source created: ' + tmpName, {type: 'success'});
150
+ document.getElementById('Facto-Full-Source-Name').value = '';
151
+ document.getElementById('Facto-Full-Source-URL').value = '';
152
+ return this.pict.providers.Facto.loadSources();
153
+ }
154
+ else
155
+ {
156
+ this.pict.views['Pict-Section-Modal'].toast('Error creating source', {type: 'error'});
157
+ }
158
+ }).then(
159
+ () => { this.refreshList(); });
160
+ }
161
+ }
162
+
163
+ module.exports = FactoFullSourcesView;
164
+
165
+ module.exports.default_configuration = _ViewConfiguration;