@darkwheel/pptb-solution-explorer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Solution Explorer
2
+
3
+ A comprehensive Power Platform Tool Box tool for exploring and managing Dataverse solutions.
4
+
5
+ ## Features
6
+
7
+ - **Solution Listing**: View all solutions in your Dataverse environment
8
+ - **Detailed Metadata**: See comprehensive information about each solution including:
9
+ - Solution name, version, and description
10
+ - Publisher information
11
+ - Installation and modification dates
12
+ - Managed vs. unmanaged status
13
+ - Solution components count
14
+ - Package type and version information
15
+ - **Publisher Details**: View publisher information including prefix and option value prefix
16
+ - **Filter & Search**: Easily find specific solutions
17
+ - **Management Ready**: Quick access to solution metadata for administrative tasks
18
+
19
+ ## Usage
20
+
21
+ 1. Connect to your Dataverse environment in Power Platform Tool Box
22
+ 2. Open the Solution Explorer tool
23
+ 3. Click "Load Solutions" to retrieve all solutions
24
+ 4. Click on any solution to view detailed information
25
+
26
+ ## Requirements
27
+
28
+ - Power Platform Tool Box
29
+ - Dataverse connection with appropriate permissions to read solution metadata
package/dist/app.js ADDED
@@ -0,0 +1,612 @@
1
+ "use strict";
2
+ /// <reference types="@pptb/types" />
3
+ /**
4
+ * Solution Explorer Tool for Power Platform Tool Box
5
+ *
6
+ * This tool helps administrators explore and manage Dataverse solutions
7
+ * with comprehensive metadata including publisher info, versions, deployment details, etc.
8
+ */
9
+ // Global API references
10
+ const toolbox = window.toolboxAPI;
11
+ const dataverse = window.dataverseAPI;
12
+ // Application state
13
+ let currentConnection = null;
14
+ let allSolutions = [];
15
+ let filteredSolutions = [];
16
+ let selectedSolutionId = null;
17
+ let currentLimit = '100';
18
+ /**
19
+ * Initialize the application
20
+ */
21
+ async function initialize() {
22
+ log('Solution Explorer initialized', 'info');
23
+ try {
24
+ // Check connection
25
+ await refreshConnection();
26
+ // Setup event handlers
27
+ setupEventHandlers();
28
+ // Subscribe to events
29
+ subscribeToEvents();
30
+ log('Tool initialized successfully', 'success');
31
+ }
32
+ catch (error) {
33
+ log(`Initialization error: ${error.message}`, 'error');
34
+ await toolbox.utils.showNotification({
35
+ title: 'Initialization Error',
36
+ body: error.message,
37
+ type: 'error',
38
+ duration: 3000
39
+ });
40
+ }
41
+ }
42
+ /**
43
+ * Refresh connection information
44
+ */
45
+ async function refreshConnection() {
46
+ try {
47
+ currentConnection = await toolbox.connections.getActiveConnection();
48
+ // Update page title with connection name
49
+ const titleElement = document.getElementById('page-title');
50
+ if (titleElement) {
51
+ if (currentConnection) {
52
+ titleElement.textContent = `📦 Solution Explorer (${currentConnection.name})`;
53
+ log(`Connected to: ${currentConnection.name}`, 'success');
54
+ }
55
+ else {
56
+ titleElement.textContent = '📦 Solution Explorer';
57
+ log('No active connection', 'warning');
58
+ }
59
+ }
60
+ }
61
+ catch (error) {
62
+ log(`Error refreshing connection: ${error.message}`, 'error');
63
+ }
64
+ }
65
+ /**
66
+ * Subscribe to platform events
67
+ */
68
+ function subscribeToEvents() {
69
+ toolbox.events.on((event, payload) => {
70
+ switch (payload.event) {
71
+ case 'connection:updated':
72
+ case 'connection:created':
73
+ refreshConnection();
74
+ break;
75
+ case 'connection:deleted':
76
+ currentConnection = null;
77
+ refreshConnection();
78
+ break;
79
+ }
80
+ });
81
+ }
82
+ /**
83
+ * Setup UI event handlers
84
+ */
85
+ function setupEventHandlers() {
86
+ // Load solutions button
87
+ document.getElementById('load-solutions-btn')?.addEventListener('click', loadSolutions);
88
+ // Refresh button
89
+ document.getElementById('refresh-btn')?.addEventListener('click', loadSolutions);
90
+ // Clear log button
91
+ document.getElementById('clear-log-btn')?.addEventListener('click', clearLog);
92
+ // Search input
93
+ const searchInput = document.getElementById('search-input');
94
+ searchInput?.addEventListener('input', applyFilters);
95
+ // Filter selects
96
+ document.getElementById('type-filter')?.addEventListener('change', applyFilters);
97
+ document.getElementById('visibility-filter')?.addEventListener('change', applyFilters);
98
+ // Limit filter - triggers reload
99
+ document.getElementById('limit-filter')?.addEventListener('change', (e) => {
100
+ currentLimit = e.target.value;
101
+ loadSolutions();
102
+ });
103
+ }
104
+ /**
105
+ * Load all solutions from Dataverse
106
+ */
107
+ async function loadSolutions() {
108
+ if (!currentConnection) {
109
+ await toolbox.utils.showNotification({
110
+ title: 'No Connection',
111
+ body: 'Please connect to a Dataverse environment',
112
+ type: 'warning',
113
+ duration: 3000
114
+ });
115
+ return;
116
+ }
117
+ try {
118
+ const loadBtn = document.getElementById('load-solutions-btn');
119
+ if (loadBtn)
120
+ loadBtn.disabled = true;
121
+ log('Loading solutions...', 'info');
122
+ // Show loading
123
+ await toolbox.utils.showLoading('Loading solutions...');
124
+ // FetchXML to get solutions with publisher information (dynamically set top value)
125
+ const topAttribute = currentLimit === 'all' ? '' : `top="${currentLimit}"`;
126
+ const fetchXml = `
127
+ <fetch ${topAttribute}>
128
+ <entity name="solution">
129
+ <attribute name="solutionid" />
130
+ <attribute name="uniquename" />
131
+ <attribute name="friendlyname" />
132
+ <attribute name="version" />
133
+ <attribute name="description" />
134
+ <attribute name="installedon" />
135
+ <attribute name="createdon" />
136
+ <attribute name="modifiedon" />
137
+ <attribute name="ismanaged" />
138
+ <attribute name="isvisible" />
139
+ <attribute name="solutionpackageversion" />
140
+ <attribute name="solutiontype" />
141
+ <attribute name="publisherid" />
142
+ <link-entity name="publisher" from="publisherid" to="publisherid" alias="publisher">
143
+ <attribute name="friendlyname" />
144
+ <attribute name="uniquename" />
145
+ <attribute name="customizationprefix" />
146
+ <attribute name="customizationoptionvalueprefix" />
147
+ <attribute name="description" />
148
+ </link-entity>
149
+ <link-entity name="systemuser" from="systemuserid" to="modifiedby" alias="modifiedby">
150
+ <attribute name="fullname" />
151
+ <attribute name="domainname" />
152
+ </link-entity>
153
+ <filter>
154
+ <condition attribute="isvisible" operator="eq" value="1" />
155
+ </filter>
156
+ <order attribute="installedon" descending="true" />
157
+ </entity>
158
+ </fetch>`;
159
+ const result = await dataverse.fetchXmlQuery(fetchXml);
160
+ allSolutions = result.value;
161
+ log(`Loaded ${allSolutions.length} solution(s)`, 'success');
162
+ // Apply filters
163
+ applyFilters();
164
+ // Show solutions section
165
+ const solutionsSection = document.getElementById('solutions-section');
166
+ if (solutionsSection)
167
+ solutionsSection.style.display = 'block';
168
+ await toolbox.utils.showNotification({
169
+ title: 'Solutions Loaded',
170
+ body: `Successfully loaded ${allSolutions.length} solution(s)`,
171
+ type: 'success',
172
+ duration: 3000
173
+ });
174
+ }
175
+ catch (error) {
176
+ log(`Error loading solutions: ${error.message}`, 'error');
177
+ await toolbox.utils.showNotification({
178
+ title: 'Load Failed',
179
+ body: error.message,
180
+ type: 'error',
181
+ duration: 5000
182
+ });
183
+ }
184
+ finally {
185
+ await toolbox.utils.hideLoading();
186
+ const loadBtn = document.getElementById('load-solutions-btn');
187
+ if (loadBtn)
188
+ loadBtn.disabled = false;
189
+ }
190
+ }
191
+ /**
192
+ * Apply filters to solutions list
193
+ */
194
+ function applyFilters() {
195
+ const searchInput = document.getElementById('search-input');
196
+ const typeFilter = document.getElementById('type-filter');
197
+ const visibilityFilter = document.getElementById('visibility-filter');
198
+ const searchTerm = searchInput?.value.toLowerCase() || '';
199
+ const typeValue = typeFilter?.value || 'all';
200
+ const visibilityValue = visibilityFilter?.value || 'all';
201
+ filteredSolutions = allSolutions.filter(solution => {
202
+ // Search filter
203
+ const matchesSearch = !searchTerm ||
204
+ (solution.friendlyname || '').toLowerCase().includes(searchTerm) ||
205
+ (solution.uniquename || '').toLowerCase().includes(searchTerm) ||
206
+ (solution['publisher.friendlyname'] || '').toLowerCase().includes(searchTerm);
207
+ // Type filter
208
+ const matchesType = typeValue === 'all' ||
209
+ (typeValue === 'managed' && solution.ismanaged) ||
210
+ (typeValue === 'unmanaged' && !solution.ismanaged);
211
+ // Visibility filter
212
+ const matchesVisibility = visibilityValue === 'all' ||
213
+ (visibilityValue === 'visible' && solution.isvisible) ||
214
+ (visibilityValue === 'hidden' && !solution.isvisible);
215
+ return matchesSearch && matchesType && matchesVisibility;
216
+ });
217
+ displaySolutions();
218
+ }
219
+ /**
220
+ * Display solutions list
221
+ */
222
+ function displaySolutions() {
223
+ const listDiv = document.getElementById('solutions-list');
224
+ const countDiv = document.getElementById('solutions-count');
225
+ if (!listDiv)
226
+ return;
227
+ if (countDiv) {
228
+ countDiv.textContent = `${filteredSolutions.length} solution(s) ${filteredSolutions.length !== allSolutions.length ? `(filtered from ${allSolutions.length})` : ''}`;
229
+ }
230
+ if (filteredSolutions.length === 0) {
231
+ listDiv.innerHTML = '<div class="empty-message">No solutions found matching your criteria</div>';
232
+ return;
233
+ }
234
+ listDiv.innerHTML = filteredSolutions.map(solution => {
235
+ const isManaged = solution.ismanaged;
236
+ const typeLabel = isManaged ? 'Managed' : 'Unmanaged';
237
+ const typeClass = isManaged ? 'managed' : 'unmanaged';
238
+ return `
239
+ <div class="solution-card ${typeClass}" data-solutionid="${solution.solutionid}">
240
+ <div class="solution-header">
241
+ <div class="solution-name">${escapeHtml(solution.friendlyname || solution.uniquename || 'N/A')}</div>
242
+ <span class="solution-type-badge ${typeClass}">${typeLabel}</span>
243
+ </div>
244
+ <div class="solution-meta">
245
+ <div class="solution-version">v${escapeHtml(solution.version || '1.0.0.0')}</div>
246
+ <div class="solution-publisher">${escapeHtml(solution['publisher.friendlyname'] || 'N/A')}</div>
247
+ </div>
248
+ <div class="solution-unique-name">${escapeHtml(solution.uniquename || 'N/A')}</div>
249
+ </div>
250
+ `;
251
+ }).join('');
252
+ // Add click handlers
253
+ document.querySelectorAll('.solution-card').forEach(card => {
254
+ card.addEventListener('click', () => {
255
+ const solutionId = card.getAttribute('data-solutionid');
256
+ if (solutionId) {
257
+ const solution = filteredSolutions.find(s => s.solutionid === solutionId);
258
+ if (solution) {
259
+ selectSolution(solutionId, solution);
260
+ }
261
+ }
262
+ });
263
+ });
264
+ }
265
+ /**
266
+ * Select a solution and display details
267
+ */
268
+ async function selectSolution(solutionId, solutionData) {
269
+ selectedSolutionId = solutionId;
270
+ // Update selected state
271
+ document.querySelectorAll('.solution-card').forEach(card => {
272
+ card.classList.remove('selected');
273
+ if (card.getAttribute('data-solutionid') === solutionId) {
274
+ card.classList.add('selected');
275
+ }
276
+ });
277
+ const detailsPanel = document.getElementById('solution-details-panel');
278
+ if (!detailsPanel)
279
+ return;
280
+ // Show loading state
281
+ detailsPanel.innerHTML = '<div class="loading-indicator">Loading solution details...</div>';
282
+ try {
283
+ // Get component count
284
+ const componentCount = await getSolutionComponentCount(solutionId);
285
+ // Get deployment history
286
+ const deploymentHistory = await getSolutionDeploymentHistory(solutionId);
287
+ // Display details
288
+ displaySolutionDetails(solutionData, componentCount, deploymentHistory);
289
+ log(`Selected solution: ${solutionData.friendlyname}`, 'info');
290
+ }
291
+ catch (error) {
292
+ log(`Error loading solution details: ${error.message}`, 'error');
293
+ detailsPanel.innerHTML = `<div class="empty-state"><p>Error loading details: ${error.message}</p></div>`;
294
+ }
295
+ }
296
+ /**
297
+ * Get solution component count
298
+ */
299
+ async function getSolutionComponentCount(solutionId) {
300
+ try {
301
+ const fetchXml = `
302
+ <fetch aggregate="true">
303
+ <entity name="solutioncomponent">
304
+ <attribute name="solutioncomponentid" alias="count" aggregate="count" />
305
+ <filter>
306
+ <condition attribute="solutionid" operator="eq" value="${solutionId}" />
307
+ </filter>
308
+ </entity>
309
+ </fetch>`;
310
+ const result = await dataverse.fetchXmlQuery(fetchXml);
311
+ return parseInt(result.value[0]?.count || '0', 10);
312
+ }
313
+ catch (error) {
314
+ log(`Error getting component count: ${error.message}`, 'warning');
315
+ return 0;
316
+ }
317
+ }
318
+ /**
319
+ * Get deployment history for a solution
320
+ */
321
+ async function getSolutionDeploymentHistory(solutionId) {
322
+ try {
323
+ const fetchXml = `
324
+ <fetch top="20">
325
+ <entity name="importjob">
326
+ <attribute name="importjobid" />
327
+ <attribute name="solutionname" />
328
+ <attribute name="completedon" />
329
+ <attribute name="startedon" />
330
+ <attribute name="progress" />
331
+ <attribute name="createdonbehalfby" />
332
+ <attribute name="createdby" />
333
+ <attribute name="modifiedby" />
334
+ <filter>
335
+ <condition attribute="solutionid" operator="eq" value="${solutionId}" />
336
+ </filter>
337
+ <order attribute="completedon" descending="true" />
338
+ </entity>
339
+ </fetch>`;
340
+ const result = await dataverse.fetchXmlQuery(fetchXml);
341
+ return result.value || [];
342
+ }
343
+ catch (error) {
344
+ log(`Error getting deployment history: ${error.message}`, 'warning');
345
+ return [];
346
+ }
347
+ }
348
+ /**
349
+ * Display solution details in the right panel
350
+ */
351
+ function displaySolutionDetails(solution, componentCount, deploymentHistory = []) {
352
+ const detailsPanel = document.getElementById('solution-details-panel');
353
+ if (!detailsPanel)
354
+ return;
355
+ const isManaged = solution.ismanaged;
356
+ const typeLabel = isManaged ? 'Managed' : 'Unmanaged';
357
+ const typeClass = isManaged ? 'managed' : 'unmanaged';
358
+ detailsPanel.innerHTML = `
359
+ <div class="solution-details">
360
+ <div class="details-header">
361
+ <h3>${escapeHtml(solution.friendlyname || solution.uniquename || 'N/A')}</h3>
362
+ <span class="solution-type-badge ${typeClass}">${typeLabel}</span>
363
+ </div>
364
+
365
+ <div class="details-section">
366
+ <h4>Basic Information</h4>
367
+ <table class="details-table">
368
+ <tr>
369
+ <td class="label">Display Name:</td>
370
+ <td>${escapeHtml(solution.friendlyname || 'N/A')}</td>
371
+ </tr>
372
+ <tr>
373
+ <td class="label">Unique Name:</td>
374
+ <td><code>${escapeHtml(solution.uniquename || 'N/A')}</code></td>
375
+ </tr>
376
+ <tr>
377
+ <td class="label">Version:</td>
378
+ <td>${escapeHtml(solution.version || '1.0.0.0')}</td>
379
+ </tr>
380
+ <tr>
381
+ <td class="label">Description:</td>
382
+ <td>${escapeHtml(solution.description || 'No description provided')}</td>
383
+ </tr>
384
+ <tr>
385
+ <td class="label">Solution ID:</td>
386
+ <td><code>${escapeHtml(solution.solutionid || 'N/A')}</code></td>
387
+ </tr>
388
+ </table>
389
+ </div>
390
+
391
+ <div class="details-section">
392
+ <h4>Publisher Information</h4>
393
+ <table class="details-table">
394
+ <tr>
395
+ <td class="label">Publisher Name:</td>
396
+ <td>${escapeHtml(solution['publisher.friendlyname'] || 'N/A')}</td>
397
+ </tr>
398
+ <tr>
399
+ <td class="label">Publisher Unique Name:</td>
400
+ <td><code>${escapeHtml(solution['publisher.uniquename'] || 'N/A')}</code></td>
401
+ </tr>
402
+ <tr>
403
+ <td class="label">Customization Prefix:</td>
404
+ <td><code>${escapeHtml(solution['publisher.customizationprefix'] || 'N/A')}</code></td>
405
+ </tr>
406
+ <tr>
407
+ <td class="label">Option Value Prefix:</td>
408
+ <td>${solution['publisher.customizationoptionvalueprefix'] !== null && solution['publisher.customizationoptionvalueprefix'] !== undefined ? solution['publisher.customizationoptionvalueprefix'] : 'N/A'}</td>
409
+ </tr>
410
+ ${solution['publisher.description'] ? `
411
+ <tr>
412
+ <td class="label">Publisher Description:</td>
413
+ <td>${escapeHtml(solution['publisher.description'])}</td>
414
+ </tr>
415
+ ` : ''}
416
+ </table>
417
+ </div>
418
+
419
+ <div class="details-section">
420
+ <h4>Installation & Deployment</h4>
421
+ <table class="details-table">
422
+ <tr>
423
+ <td class="label">Installed On:</td>
424
+ <td>${solution.installedon ? formatDateTime(solution.installedon) : 'N/A'}</td>
425
+ </tr>
426
+ <tr>
427
+ <td class="label">Created On:</td>
428
+ <td>${solution.createdon ? formatDateTime(solution.createdon) : 'N/A'}</td>
429
+ </tr>
430
+ <tr>
431
+ <td class="label">Modified On:</td>
432
+ <td>${solution.modifiedon ? formatDateTime(solution.modifiedon) : 'N/A'}</td>
433
+ </tr>
434
+ <tr>
435
+ <td class="label">Last Modified By:</td>
436
+ <td>
437
+ <div class="modifier-info">
438
+ <div class="modifier-name">${escapeHtml(solution['modifiedby.fullname'] || 'N/A')}</div>
439
+ ${solution['modifiedby.domainname'] ? `<div class="modifier-domain">${escapeHtml(solution['modifiedby.domainname'])}</div>` : ''}
440
+ </div>
441
+ </td>
442
+ </tr>
443
+ <tr>
444
+ <td class="label">Package Type:</td>
445
+ <td>${isManaged ? 'Managed' : 'Unmanaged'}</td>
446
+ </tr>
447
+ <tr>
448
+ <td class="label">Is Visible:</td>
449
+ <td>${solution.isvisible ? 'Yes' : 'No'}</td>
450
+ </tr>
451
+ ${solution.solutionpackageversion ? `
452
+ <tr>
453
+ <td class="label">Package Version:</td>
454
+ <td>${escapeHtml(solution.solutionpackageversion)}</td>
455
+ </tr>
456
+ ` : ''}
457
+ </table>
458
+ </div>
459
+
460
+ <div class="details-section">
461
+ <h4>Components</h4>
462
+ <table class="details-table">
463
+ <tr>
464
+ <td class="label">Component Count:</td>
465
+ <td><strong>${componentCount}</strong> component(s)</td>
466
+ </tr>
467
+ </table>
468
+ </div>
469
+
470
+ ${deploymentHistory.length > 0 ? `
471
+ <div class="details-section">
472
+ <h4>Deployment History</h4>
473
+ <div class="deployment-history">
474
+ ${deploymentHistory.map((deployment, index) => {
475
+ const completedOn = deployment.completedon ? formatDateTime(deployment.completedon) : 'N/A';
476
+ const startedOn = deployment.startedon ? formatDateTime(deployment.startedon) : 'N/A';
477
+ const deployedBy = escapeHtml(deployment['_createdby_value@OData.Community.Display.V1.FormattedValue'] || 'System');
478
+ const progress = deployment.progress !== null && deployment.progress !== undefined ? Math.round(deployment.progress) : 100;
479
+ return `
480
+ <div class="deployment-item">
481
+ <div class="deployment-header">
482
+ <div class="deployment-number">#${index + 1}</div>
483
+ <div class="deployment-date">${completedOn}</div>
484
+ </div>
485
+ <div class="deployment-details">
486
+ <div class="deployment-info">
487
+ <span class="label">Deployed By:</span>
488
+ <span class="value">${deployedBy}</span>
489
+ </div>
490
+ <div class="deployment-info">
491
+ <span class="label">Started:</span>
492
+ <span class="value">${startedOn}</span>
493
+ </div>
494
+ <div class="deployment-info">
495
+ <span class="label">Progress:</span>
496
+ <span class="value">${progress}%</span>
497
+ </div>
498
+ </div>
499
+ </div>
500
+ `;
501
+ }).join('')}
502
+ </div>
503
+ </div>
504
+ ` : ''}
505
+
506
+ <div class="details-section">
507
+ <h4>Management Actions</h4>
508
+ <div class="action-buttons">
509
+ <button class="btn btn-secondary btn-small" onclick="copySolutionId('${solution.solutionid}')">Copy Solution ID</button>
510
+ <button class="btn btn-secondary btn-small" onclick="copySolutionUniqueName('${solution.uniquename}')">Copy Unique Name</button>
511
+ </div>
512
+ </div>
513
+ </div>
514
+ `;
515
+ }
516
+ /**
517
+ * Copy solution ID to clipboard
518
+ */
519
+ async function copySolutionId(solutionId) {
520
+ try {
521
+ await toolbox.utils.copyToClipboard(solutionId);
522
+ await toolbox.utils.showNotification({
523
+ title: 'Copied',
524
+ body: 'Solution ID copied to clipboard',
525
+ type: 'success',
526
+ duration: 2000
527
+ });
528
+ log('Solution ID copied to clipboard', 'info');
529
+ }
530
+ catch (error) {
531
+ log(`Error copying to clipboard: ${error.message}`, 'error');
532
+ }
533
+ }
534
+ /**
535
+ * Copy solution unique name to clipboard
536
+ */
537
+ async function copySolutionUniqueName(uniqueName) {
538
+ try {
539
+ await toolbox.utils.copyToClipboard(uniqueName);
540
+ await toolbox.utils.showNotification({
541
+ title: 'Copied',
542
+ body: 'Unique name copied to clipboard',
543
+ type: 'success',
544
+ duration: 2000
545
+ });
546
+ log('Unique name copied to clipboard', 'info');
547
+ }
548
+ catch (error) {
549
+ log(`Error copying to clipboard: ${error.message}`, 'error');
550
+ }
551
+ }
552
+ // Make functions globally available for onclick handlers
553
+ window.copySolutionId = copySolutionId;
554
+ window.copySolutionUniqueName = copySolutionUniqueName;
555
+ /**
556
+ * Format date/time for display
557
+ */
558
+ function formatDateTime(dateString) {
559
+ try {
560
+ const date = new Date(dateString);
561
+ return date.toLocaleString();
562
+ }
563
+ catch {
564
+ return dateString;
565
+ }
566
+ }
567
+ /**
568
+ * Escape HTML to prevent XSS
569
+ */
570
+ function escapeHtml(text) {
571
+ const div = document.createElement('div');
572
+ div.textContent = text;
573
+ return div.innerHTML;
574
+ }
575
+ /**
576
+ * Log message to event log
577
+ */
578
+ function log(message, type = 'info') {
579
+ const logDiv = document.getElementById('event-log');
580
+ if (!logDiv)
581
+ return;
582
+ const timestamp = new Date().toLocaleTimeString();
583
+ const logEntry = document.createElement('div');
584
+ logEntry.className = `log-entry ${type}`;
585
+ logEntry.innerHTML = `
586
+ <span class="log-timestamp">[${timestamp}]</span>
587
+ <span>${message}</span>
588
+ `;
589
+ logDiv.insertBefore(logEntry, logDiv.firstChild);
590
+ // Keep only last 50 entries
591
+ while (logDiv.children.length > 50) {
592
+ logDiv.removeChild(logDiv.lastChild);
593
+ }
594
+ console.log(`[${type.toUpperCase()}] ${message}`);
595
+ }
596
+ /**
597
+ * Clear event log
598
+ */
599
+ function clearLog() {
600
+ const logDiv = document.getElementById('event-log');
601
+ if (logDiv) {
602
+ logDiv.innerHTML = '';
603
+ log('Log cleared', 'info');
604
+ }
605
+ }
606
+ // Initialize when DOM is ready
607
+ if (document.readyState === 'loading') {
608
+ document.addEventListener('DOMContentLoaded', initialize);
609
+ }
610
+ else {
611
+ initialize();
612
+ }
@@ -0,0 +1,91 @@
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.0">
6
+ <title>Solution Explorer</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <header>
12
+ <h1 id="page-title">📦 Solution Explorer</h1>
13
+ <p class="subtitle">Explore and manage Dataverse solutions with comprehensive metadata</p>
14
+ </header>
15
+
16
+ <!-- Filters Section -->
17
+ <section class="card filters-section">
18
+ <h2>Filters</h2>
19
+ <div class="filter-controls">
20
+ <div class="filter-group">
21
+ <label for="search-input">Search:</label>
22
+ <input
23
+ type="text"
24
+ id="search-input"
25
+ class="search-input"
26
+ placeholder="Search by solution name, unique name, or publisher..."
27
+ />
28
+ </div>
29
+ <div class="filter-group">
30
+ <label for="type-filter">Type:</label>
31
+ <select id="type-filter" class="filter-select">
32
+ <option value="all">All Solutions</option>
33
+ <option value="managed">Managed Only</option>
34
+ <option value="unmanaged">Unmanaged Only</option>
35
+ </select>
36
+ </div>
37
+ <div class="filter-group">
38
+ <label for="visibility-filter">Visibility:</label>
39
+ <select id="visibility-filter" class="filter-select">
40
+ <option value="all">All</option>
41
+ <option value="visible">Visible Only</option>
42
+ <option value="hidden">Hidden Only</option>
43
+ </select>
44
+ </div>
45
+ <div class="filter-group">
46
+ <label for="limit-filter">Show:</label>
47
+ <select id="limit-filter" class="filter-select">
48
+ <option value="100">Top 100</option>
49
+ <option value="250">Top 250</option>
50
+ <option value="500">Top 500</option>
51
+ <option value="1000">Top 1000</option>
52
+ <option value="all">All Solutions</option>
53
+ </select>
54
+ </div>
55
+ </div>
56
+ <div class="filter-actions">
57
+ <button id="load-solutions-btn" class="btn btn-primary">Load Solutions</button>
58
+ <button id="refresh-btn" class="btn btn-secondary">Refresh</button>
59
+ </div>
60
+ </section>
61
+
62
+ <!-- Solutions List Section -->
63
+ <section class="card" id="solutions-section" style="display: none;">
64
+ <h2>Solutions</h2>
65
+ <div id="solutions-count" class="solutions-count"></div>
66
+
67
+ <div class="split-view">
68
+ <div class="left-panel">
69
+ <div id="solutions-list" class="solutions-list"></div>
70
+ </div>
71
+ <div class="right-panel" id="solution-details-panel">
72
+ <div class="empty-state">
73
+ <p>Select a solution to view detailed information</p>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </section>
78
+
79
+ <!-- Event Log -->
80
+ <section class="card">
81
+ <div class="section-header">
82
+ <h2>Event Log</h2>
83
+ <button id="clear-log-btn" class="btn btn-small">Clear Log</button>
84
+ </div>
85
+ <div id="event-log" class="event-log"></div>
86
+ </section>
87
+ </div>
88
+
89
+ <script type="module" src="app.js"></script>
90
+ </body>
91
+ </html>
@@ -0,0 +1,564 @@
1
+ /* Solution Explorer Styles */
2
+
3
+ :root {
4
+ --primary-color: #0078d4;
5
+ --primary-hover: #106ebe;
6
+ --success-color: #107c10;
7
+ --warning-color: #ffb900;
8
+ --error-color: #d13438;
9
+ --text-primary: #323130;
10
+ --text-secondary: #605e5c;
11
+ --border-color: #e1dfdd;
12
+ --bg-primary: #ffffff;
13
+ --bg-secondary: #faf9f8;
14
+ --bg-hover: #f3f2f1;
15
+ --shadow: 0 2px 4px rgba(0,0,0,0.1);
16
+ }
17
+
18
+ [data-theme="dark"] {
19
+ --text-primary: #ffffff;
20
+ --text-secondary: #d2d0ce;
21
+ --border-color: #3b3a39;
22
+ --bg-primary: #1b1a19;
23
+ --bg-secondary: #252423;
24
+ --bg-hover: #323130;
25
+ --shadow: 0 2px 4px rgba(0,0,0,0.3);
26
+ }
27
+
28
+ * {
29
+ box-sizing: border-box;
30
+ }
31
+
32
+ body {
33
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
34
+ margin: 0;
35
+ padding: 0;
36
+ background-color: var(--bg-secondary);
37
+ color: var(--text-primary);
38
+ line-height: 1.6;
39
+ }
40
+
41
+ .container {
42
+ max-width: 1400px;
43
+ margin: 0 auto;
44
+ padding: 20px;
45
+ }
46
+
47
+ header {
48
+ background: var(--bg-primary);
49
+ padding: 24px;
50
+ border-radius: 8px;
51
+ box-shadow: var(--shadow);
52
+ margin-bottom: 20px;
53
+ }
54
+
55
+ h1 {
56
+ margin: 0 0 8px 0;
57
+ font-size: 32px;
58
+ font-weight: 600;
59
+ }
60
+
61
+ .subtitle {
62
+ margin: 0 0 16px 0;
63
+ color: var(--text-secondary);
64
+ font-size: 14px;
65
+ }
66
+
67
+ /* Filters Section */
68
+ .filters-section {
69
+ margin-bottom: 20px;
70
+ }
71
+
72
+ /* Card */
73
+ .card {
74
+ background: var(--bg-primary);
75
+ padding: 24px;
76
+ border-radius: 8px;
77
+ box-shadow: var(--shadow);
78
+ margin-bottom: 20px;
79
+ }
80
+
81
+ .card h2 {
82
+ margin: 0 0 16px 0;
83
+ font-size: 20px;
84
+ font-weight: 600;
85
+ }
86
+
87
+ /* Filters Section */
88
+ .filters-section {
89
+ margin-bottom: 20px;
90
+ }
91
+
92
+ .filter-controls {
93
+ display: flex;
94
+ gap: 16px;
95
+ flex-wrap: wrap;
96
+ margin-bottom: 16px;
97
+ }
98
+
99
+ .filter-actions {
100
+ display: flex;
101
+ gap: 12px;
102
+ padding-top: 16px;
103
+ border-top: 1px solid var(--border-color);
104
+ }
105
+
106
+ .filter-group {
107
+ flex: 1;
108
+ min-width: 200px;
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: 6px;
112
+ }
113
+
114
+ .filter-group label {
115
+ font-weight: 600;
116
+ font-size: 14px;
117
+ color: var(--text-secondary);
118
+ }
119
+
120
+ /* Buttons */
121
+ .btn {
122
+ padding: 10px 20px;
123
+ border: none;
124
+ border-radius: 4px;
125
+ font-size: 14px;
126
+ font-weight: 600;
127
+ cursor: pointer;
128
+ transition: all 0.2s;
129
+ font-family: inherit;
130
+ }
131
+
132
+ .btn:hover:not(:disabled) {
133
+ transform: translateY(-1px);
134
+ box-shadow: var(--shadow);
135
+ }
136
+
137
+ .btn:disabled {
138
+ opacity: 0.5;
139
+ cursor: not-allowed;
140
+ }
141
+
142
+ .btn-primary {
143
+ background-color: var(--primary-color);
144
+ color: white;
145
+ }
146
+
147
+ .btn-primary:hover:not(:disabled) {
148
+ background-color: var(--primary-hover);
149
+ }
150
+
151
+ .btn-secondary {
152
+ background-color: var(--bg-secondary);
153
+ color: var(--text-primary);
154
+ border: 1px solid var(--border-color);
155
+ }
156
+
157
+ .btn-secondary:hover:not(:disabled) {
158
+ background-color: var(--bg-hover);
159
+ }
160
+
161
+ .btn-small {
162
+ padding: 6px 12px;
163
+ font-size: 12px;
164
+ }
165
+
166
+ /* Inputs */
167
+ .search-input,
168
+ .filter-select {
169
+ padding: 10px;
170
+ border: 1px solid var(--border-color);
171
+ border-radius: 4px;
172
+ font-size: 14px;
173
+ font-family: inherit;
174
+ background-color: var(--bg-primary);
175
+ color: var(--text-primary);
176
+ width: 100%;
177
+ }
178
+
179
+ .search-input:focus,
180
+ .filter-select:focus {
181
+ outline: none;
182
+ border-color: var(--primary-color);
183
+ }
184
+
185
+ /* Solutions Count */
186
+ .solutions-count {
187
+ margin-bottom: 16px;
188
+ padding: 8px 12px;
189
+ background-color: var(--bg-secondary);
190
+ border-radius: 4px;
191
+ font-size: 14px;
192
+ font-weight: 600;
193
+ }
194
+
195
+ /* Split View */
196
+ .split-view {
197
+ display: grid;
198
+ grid-template-columns: 1fr 1fr;
199
+ gap: 20px;
200
+ min-height: 400px;
201
+ }
202
+
203
+ .left-panel,
204
+ .right-panel {
205
+ overflow-y: auto;
206
+ max-height: 600px;
207
+ }
208
+
209
+ .left-panel {
210
+ border-right: 1px solid var(--border-color);
211
+ padding-right: 20px;
212
+ }
213
+
214
+ /* Solutions List */
215
+ .solutions-list {
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: 12px;
219
+ }
220
+
221
+ .solution-card {
222
+ padding: 16px;
223
+ border: 1px solid var(--border-color);
224
+ border-radius: 6px;
225
+ cursor: pointer;
226
+ transition: all 0.2s;
227
+ background-color: var(--bg-primary);
228
+ }
229
+
230
+ .solution-card:hover {
231
+ background-color: var(--bg-hover);
232
+ transform: translateX(4px);
233
+ box-shadow: var(--shadow);
234
+ }
235
+
236
+ .solution-card.selected {
237
+ border-color: var(--primary-color);
238
+ background-color: rgba(0, 120, 212, 0.05);
239
+ }
240
+
241
+ .solution-card.managed {
242
+ border-left: 4px solid var(--primary-color);
243
+ }
244
+
245
+ .solution-card.unmanaged {
246
+ border-left: 4px solid var(--success-color);
247
+ }
248
+
249
+ .solution-header {
250
+ display: flex;
251
+ justify-content: space-between;
252
+ align-items: start;
253
+ margin-bottom: 8px;
254
+ }
255
+
256
+ .solution-name {
257
+ font-weight: 600;
258
+ font-size: 16px;
259
+ flex: 1;
260
+ }
261
+
262
+ .solution-type-badge {
263
+ padding: 2px 8px;
264
+ border-radius: 4px;
265
+ font-size: 11px;
266
+ font-weight: 600;
267
+ text-transform: uppercase;
268
+ }
269
+
270
+ .solution-type-badge.managed {
271
+ background-color: rgba(0, 120, 212, 0.2);
272
+ color: var(--primary-color);
273
+ }
274
+
275
+ .solution-type-badge.unmanaged {
276
+ background-color: rgba(16, 124, 16, 0.2);
277
+ color: var(--success-color);
278
+ }
279
+
280
+ .solution-meta {
281
+ display: flex;
282
+ gap: 12px;
283
+ margin-bottom: 6px;
284
+ font-size: 13px;
285
+ color: var(--text-secondary);
286
+ }
287
+
288
+ .solution-version {
289
+ font-weight: 600;
290
+ }
291
+
292
+ .solution-unique-name {
293
+ font-size: 12px;
294
+ color: var(--text-secondary);
295
+ font-family: 'Consolas', 'Courier New', monospace;
296
+ }
297
+
298
+ /* Solution Details */
299
+ .solution-details {
300
+ padding: 0;
301
+ }
302
+
303
+ .details-header {
304
+ display: flex;
305
+ justify-content: space-between;
306
+ align-items: start;
307
+ margin-bottom: 20px;
308
+ padding-bottom: 16px;
309
+ border-bottom: 2px solid var(--border-color);
310
+ }
311
+
312
+ .details-header h3 {
313
+ margin: 0;
314
+ font-size: 20px;
315
+ font-weight: 600;
316
+ }
317
+
318
+ .details-section {
319
+ margin-bottom: 24px;
320
+ }
321
+
322
+ .details-section h4 {
323
+ margin: 0 0 12px 0;
324
+ font-size: 16px;
325
+ font-weight: 600;
326
+ color: var(--text-secondary);
327
+ }
328
+
329
+ .details-table {
330
+ width: 100%;
331
+ border-collapse: collapse;
332
+ }
333
+
334
+ .details-table tr {
335
+ border-bottom: 1px solid var(--border-color);
336
+ }
337
+
338
+ .details-table td {
339
+ padding: 10px 8px;
340
+ font-size: 14px;
341
+ }
342
+
343
+ .details-table td.label {
344
+ font-weight: 600;
345
+ color: var(--text-secondary);
346
+ width: 40%;
347
+ }
348
+
349
+ .details-table code {
350
+ background-color: var(--bg-secondary);
351
+ padding: 2px 6px;
352
+ border-radius: 3px;
353
+ font-family: 'Consolas', 'Courier New', monospace;
354
+ font-size: 13px;
355
+ }
356
+
357
+ .modifier-info {
358
+ display: flex;
359
+ flex-direction: column;
360
+ gap: 4px;
361
+ }
362
+
363
+ .modifier-name {
364
+ font-weight: 600;
365
+ color: var(--text-primary);
366
+ }
367
+
368
+ .modifier-domain {
369
+ font-size: 12px;
370
+ color: var(--text-secondary);
371
+ font-family: 'Consolas', 'Courier New', monospace;
372
+ }
373
+
374
+ /* Deployment History */
375
+ .deployment-history {
376
+ display: flex;
377
+ flex-direction: column;
378
+ gap: 12px;
379
+ }
380
+
381
+ .deployment-item {
382
+ padding: 12px;
383
+ border: 1px solid var(--border-color);
384
+ border-radius: 6px;
385
+ background-color: var(--bg-secondary);
386
+ }
387
+
388
+ .deployment-header {
389
+ display: flex;
390
+ align-items: center;
391
+ gap: 12px;
392
+ margin-bottom: 10px;
393
+ font-weight: 600;
394
+ }
395
+
396
+ .deployment-number {
397
+ color: var(--text-secondary);
398
+ font-size: 13px;
399
+ min-width: 30px;
400
+ }
401
+
402
+ .deployment-date {
403
+ margin-left: auto;
404
+ font-size: 13px;
405
+ color: var(--text-secondary);
406
+ }
407
+
408
+ .deployment-details {
409
+ display: flex;
410
+ flex-direction: column;
411
+ gap: 6px;
412
+ }
413
+
414
+ .deployment-info {
415
+ display: flex;
416
+ font-size: 13px;
417
+ gap: 8px;
418
+ }
419
+
420
+ .deployment-info .label {
421
+ font-weight: 600;
422
+ color: var(--text-secondary);
423
+ min-width: 90px;
424
+ }
425
+
426
+ .deployment-info .value {
427
+ color: var(--text-primary);
428
+ }
429
+
430
+ .action-buttons {
431
+ display: flex;
432
+ gap: 12px;
433
+ flex-wrap: wrap;
434
+ }
435
+
436
+ /* Empty States */
437
+ .empty-state,
438
+ .empty-message {
439
+ padding: 40px 20px;
440
+ text-align: center;
441
+ color: var(--text-secondary);
442
+ }
443
+
444
+ .loading-indicator {
445
+ padding: 40px 20px;
446
+ text-align: center;
447
+ color: var(--text-secondary);
448
+ font-style: italic;
449
+ }
450
+
451
+ /* Event Log */
452
+ .section-header {
453
+ display: flex;
454
+ justify-content: space-between;
455
+ align-items: center;
456
+ margin-bottom: 16px;
457
+ }
458
+
459
+ .section-header h2 {
460
+ margin: 0;
461
+ }
462
+
463
+ .event-log {
464
+ max-height: 300px;
465
+ overflow-y: auto;
466
+ background-color: var(--bg-secondary);
467
+ padding: 12px;
468
+ border-radius: 4px;
469
+ font-family: 'Consolas', 'Courier New', monospace;
470
+ font-size: 12px;
471
+ }
472
+
473
+ .log-entry {
474
+ padding: 6px 8px;
475
+ margin-bottom: 4px;
476
+ border-radius: 3px;
477
+ display: flex;
478
+ gap: 8px;
479
+ }
480
+
481
+ .log-entry.info {
482
+ background-color: rgba(0, 120, 212, 0.1);
483
+ border-left: 3px solid var(--primary-color);
484
+ }
485
+
486
+ .log-entry.success {
487
+ background-color: rgba(16, 124, 16, 0.1);
488
+ border-left: 3px solid var(--success-color);
489
+ }
490
+
491
+ .log-entry.warning {
492
+ background-color: rgba(255, 185, 0, 0.1);
493
+ border-left: 3px solid var(--warning-color);
494
+ }
495
+
496
+ .log-entry.error {
497
+ background-color: rgba(209, 52, 56, 0.1);
498
+ border-left: 3px solid var(--error-color);
499
+ }
500
+
501
+ .log-timestamp {
502
+ color: var(--text-secondary);
503
+ white-space: nowrap;
504
+ }
505
+
506
+ /* Responsive Design */
507
+ @media (max-width: 1024px) {
508
+ .split-view {
509
+ grid-template-columns: 1fr;
510
+ }
511
+
512
+ .left-panel {
513
+ border-right: none;
514
+ border-bottom: 1px solid var(--border-color);
515
+ padding-right: 0;
516
+ padding-bottom: 20px;
517
+ max-height: 400px;
518
+ }
519
+ }
520
+
521
+ @media (max-width: 768px) {
522
+ .container {
523
+ padding: 12px;
524
+ }
525
+
526
+ header {
527
+ padding: 16px;
528
+ }
529
+
530
+ h1 {
531
+ font-size: 24px;
532
+ }
533
+
534
+ .header-actions {
535
+ flex-direction: column;
536
+ }
537
+
538
+ .filter-controls {
539
+ flex-direction: column;
540
+ }
541
+
542
+ .filter-group {
543
+ min-width: 100%;
544
+ }
545
+ }
546
+
547
+ /* Scrollbar Styling */
548
+ ::-webkit-scrollbar {
549
+ width: 8px;
550
+ height: 8px;
551
+ }
552
+
553
+ ::-webkit-scrollbar-track {
554
+ background: var(--bg-secondary);
555
+ }
556
+
557
+ ::-webkit-scrollbar-thumb {
558
+ background: var(--border-color);
559
+ border-radius: 4px;
560
+ }
561
+
562
+ ::-webkit-scrollbar-thumb:hover {
563
+ background: var(--text-secondary);
564
+ }
package/icon.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@darkwheel/pptb-solution-explorer",
3
+ "version": "0.2.0",
4
+ "displayName": "Solution Explorer",
5
+ "description": "A comprehensive tool to explore and manage Dataverse solutions with detailed metadata for administrators",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/darkwheel/pptb-solutionExplorer.git"
9
+ },
10
+ "contributors": [
11
+ {
12
+ "name": "Christoph Schaffer",
13
+ "url": "https://github.com/darkwheel"
14
+ }
15
+ ],
16
+ "keywords": [
17
+ "powerplatform",
18
+ "dataverse",
19
+ "toolbox",
20
+ "pptb",
21
+ "solution",
22
+ "management",
23
+ "deployment"
24
+ ],
25
+ "license": "MIT",
26
+ "configurations": {
27
+ "repository": "https://github.com/darkwheel/pptb-solutionExplorer",
28
+ "website": "https://github.com/darkwheel/pptb-solutionExplorer",
29
+ "iconUrl": "https://raw.githubusercontent.com/darkwheel/pptb-solutionExplorer/refs/heads/main/icon.png",
30
+ "readmeUrl": "https://raw.githubusercontent.com/darkwheel/pptb-solutionExplorer/refs/heads/main/README_NPM.md"
31
+ },
32
+ "features": {
33
+ "multi-connection": false
34
+ },
35
+ "scripts": {
36
+ "build": "tsc && npm run copy-html && npm run copy-css",
37
+ "copy-html": "shx cp src/index.html dist/",
38
+ "copy-css": "shx cp src/styles.css dist/",
39
+ "watch": "tsc --watch"
40
+ },
41
+ "devDependencies": {
42
+ "@pptb/types": "^1.0.11",
43
+ "typescript": "^5.0.0",
44
+ "shx": "^0.4.0"
45
+ }
46
+ }