@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 +29 -0
- package/dist/app.js +612 -0
- package/dist/index.html +91 -0
- package/dist/styles.css +564 -0
- package/icon.png +0 -0
- package/package.json +46 -0
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
|
+
}
|
package/dist/index.html
ADDED
|
@@ -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>
|
package/dist/styles.css
ADDED
|
@@ -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
|
+
}
|