Skip to content

Commit 6c53230

Browse files
authored
Merge pull request #81 from delegateas/features/solution-insights-enhancements
feat: Solution Insights enhancements - Improved ComponentTypes Overviews
2 parents 31fcf73 + 338c833 commit 6c53230

File tree

11 files changed

+1423
-179
lines changed

11 files changed

+1423
-179
lines changed

Generator/DTO/SolutionComponent.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,49 @@
11
namespace Generator.DTO;
22

3+
/// <summary>
4+
/// Solution component types from Dataverse.
5+
/// See: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/entities/solutioncomponent
6+
/// </summary>
37
public enum SolutionComponentType
48
{
59
Entity = 1,
610
Attribute = 2,
7-
Relationship = 3,
11+
OptionSet = 9,
12+
Relationship = 10,
13+
EntityKey = 14,
14+
SecurityRole = 20,
15+
SavedQuery = 26,
16+
Workflow = 29,
17+
RibbonCustomization = 50,
18+
SavedQueryVisualization = 59,
19+
SystemForm = 60,
20+
WebResource = 61,
21+
SiteMap = 62,
22+
ConnectionRole = 63,
23+
HierarchyRule = 65,
24+
CustomControl = 66,
25+
FieldSecurityProfile = 70,
26+
ModelDrivenApp = 80,
27+
PluginAssembly = 91,
28+
SDKMessageProcessingStep = 92,
29+
CanvasApp = 300,
30+
ConnectionReference = 372,
31+
EnvironmentVariableDefinition = 380,
32+
EnvironmentVariableValue = 381,
33+
Dataflow = 418,
34+
ConnectionRoleObjectTypeCode = 3233,
35+
CustomAPI = 10240,
36+
CustomAPIRequestParameter = 10241,
37+
CustomAPIResponseProperty = 10242,
38+
PluginPackage = 10639,
39+
OrganizationSetting = 10563,
40+
AppAction = 10645,
41+
AppActionRule = 10948,
42+
FxExpression = 11492,
43+
DVFileSearch = 11723,
44+
DVFileSearchAttribute = 11724,
45+
DVFileSearchEntity = 11725,
46+
AISkillConfig = 12075,
847
}
948

1049
public record SolutionComponent(
@@ -14,3 +53,22 @@ public record SolutionComponent(
1453
SolutionComponentType ComponentType,
1554
string PublisherName,
1655
string PublisherPrefix);
56+
57+
/// <summary>
58+
/// Represents a solution component with its solution membership info for the insights view.
59+
/// </summary>
60+
public record SolutionComponentData(
61+
string Name,
62+
string SchemaName,
63+
SolutionComponentType ComponentType,
64+
Guid ObjectId,
65+
bool IsExplicit,
66+
string? RelatedTable = null);
67+
68+
/// <summary>
69+
/// Collection of solution components grouped by solution.
70+
/// </summary>
71+
public record SolutionComponentCollection(
72+
Guid SolutionId,
73+
string SolutionName,
74+
List<SolutionComponentData> Components);

Generator/DataverseService.cs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ internal class DataverseService
2424
private readonly EntityIconService entityIconService;
2525
private readonly RecordMappingService recordMappingService;
2626
private readonly SolutionComponentService solutionComponentService;
27+
private readonly SolutionComponentExtractor solutionComponentExtractor;
2728
private readonly WorkflowService workflowService;
2829
private readonly RelationshipService relationshipService;
2930

@@ -38,6 +39,7 @@ public DataverseService(
3839
EntityIconService entityIconService,
3940
RecordMappingService recordMappingService,
4041
SolutionComponentService solutionComponentService,
42+
SolutionComponentExtractor solutionComponentExtractor,
4143
WorkflowService workflowService,
4244
RelationshipService relationshipService)
4345
{
@@ -49,6 +51,7 @@ public DataverseService(
4951
this.recordMappingService = recordMappingService;
5052
this.workflowService = workflowService;
5153
this.relationshipService = relationshipService;
54+
this.solutionComponentExtractor = solutionComponentExtractor;
5255

5356
// Register all analyzers with their query functions
5457
analyzerRegistrations = new List<IAnalyzerRegistration>
@@ -69,7 +72,7 @@ public DataverseService(
6972
this.solutionComponentService = solutionComponentService;
7073
}
7174

72-
public async Task<(IEnumerable<Record>, IEnumerable<SolutionWarning>)> GetFilteredMetadata()
75+
public async Task<(IEnumerable<Record>, IEnumerable<SolutionWarning>, IEnumerable<SolutionComponentCollection>)> GetFilteredMetadata()
7376
{
7477
// used to collect warnings for the insights dashboard
7578
var warnings = new List<SolutionWarning>();
@@ -275,8 +278,79 @@ public DataverseService(
275278
})
276279
.ToList();
277280

278-
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed - returning empty results");
279-
return (records, warnings);
281+
/// SOLUTION COMPONENTS FOR INSIGHTS
282+
List<SolutionComponentCollection> solutionComponentCollections;
283+
try
284+
{
285+
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracting solution components for insights view");
286+
287+
// Build name lookups from entity metadata for the extractor
288+
var entityNameLookup = entitiesInSolutionMetadata.ToDictionary(
289+
e => e.MetadataId!.Value,
290+
e => e.DisplayName.ToLabelString() ?? e.SchemaName);
291+
292+
var attributeNameLookup = entitiesInSolutionMetadata
293+
.SelectMany(e => e.Attributes.Where(a => a.MetadataId.HasValue))
294+
.ToDictionary(
295+
a => a.MetadataId!.Value,
296+
a => a.DisplayName.ToLabelString() ?? a.SchemaName);
297+
298+
var relationshipNameLookup = entitiesInSolutionMetadata
299+
.SelectMany(e => e.ManyToManyRelationships.Cast<RelationshipMetadataBase>()
300+
.Concat(e.OneToManyRelationships)
301+
.Concat(e.ManyToOneRelationships))
302+
.Where(r => r.MetadataId.HasValue)
303+
.DistinctBy(r => r.MetadataId!.Value)
304+
.ToDictionary(
305+
r => r.MetadataId!.Value,
306+
r => r.SchemaName);
307+
308+
// Build entity lookups for attributes, relationships, and keys (maps component ID to parent entity name)
309+
var attributeEntityLookup = entitiesInSolutionMetadata
310+
.SelectMany(e => e.Attributes.Where(a => a.MetadataId.HasValue)
311+
.Select(a => (AttributeId: a.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
312+
.ToDictionary(x => x.AttributeId, x => x.EntityName);
313+
314+
var relationshipEntityLookup = entitiesInSolutionMetadata
315+
.SelectMany(e => e.ManyToManyRelationships.Cast<RelationshipMetadataBase>()
316+
.Concat(e.OneToManyRelationships)
317+
.Concat(e.ManyToOneRelationships)
318+
.Where(r => r.MetadataId.HasValue)
319+
.Select(r => (RelationshipId: r.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
320+
.DistinctBy(x => x.RelationshipId)
321+
.ToDictionary(x => x.RelationshipId, x => x.EntityName);
322+
323+
var keyEntityLookup = entitiesInSolutionMetadata
324+
.SelectMany(e => (e.Keys ?? Array.Empty<EntityKeyMetadata>())
325+
.Where(k => k.MetadataId.HasValue)
326+
.Select(k => (KeyId: k.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
327+
.ToDictionary(x => x.KeyId, x => x.EntityName);
328+
329+
// Build solution name lookup
330+
var solutionNameLookup = solutionLookup.ToDictionary(
331+
kvp => kvp.Key,
332+
kvp => kvp.Value.Name);
333+
334+
solutionComponentCollections = await solutionComponentExtractor.ExtractSolutionComponentsAsync(
335+
solutionIds,
336+
solutionNameLookup,
337+
entityNameLookup,
338+
attributeNameLookup,
339+
relationshipNameLookup,
340+
attributeEntityLookup,
341+
relationshipEntityLookup,
342+
keyEntityLookup);
343+
344+
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracted components for {solutionComponentCollections.Count} solutions");
345+
}
346+
catch (Exception ex)
347+
{
348+
logger.LogWarning(ex, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Failed to extract solution components for insights, continuing without them");
349+
solutionComponentCollections = new List<SolutionComponentCollection>();
350+
}
351+
352+
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed");
353+
return (records, warnings, solutionComponentCollections);
280354
}
281355
}
282356

Generator/Program.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,16 @@
5959
services.AddSingleton<DataverseService>();
6060
services.AddSingleton<WorkflowService>();
6161
services.AddSingleton<SolutionComponentService>();
62+
services.AddSingleton<SolutionComponentExtractor>();
6263

6364
// Build service provider
6465
var serviceProvider = services.BuildServiceProvider();
6566

6667
// Resolve and use DataverseService
6768
var dataverseService = serviceProvider.GetRequiredService<DataverseService>();
68-
var (entities, warnings) = await dataverseService.GetFilteredMetadata();
69+
var (entities, warnings, solutionComponents) = await dataverseService.GetFilteredMetadata();
6970

70-
var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings);
71+
var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings, solutionComponents);
7172
websiteBuilder.AddData();
7273

7374
// Token provider function

Generator/Services/EntityIconService.cs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,25 @@ public async Task<Dictionary<string, string>> GetEntityIconMap(IEnumerable<Entit
2727
.Where(x => x.IconVectorName != null)
2828
.ToDictionary(x => x.LogicalName, x => x.IconVectorName);
2929

30-
var query = new QueryExpression("webresource")
30+
var iconNameToSvg = new Dictionary<string, string>();
31+
32+
if (logicalNameToIconName.Count > 0)
3133
{
32-
ColumnSet = new ColumnSet("content", "name"),
33-
Criteria = new FilterExpression(LogicalOperator.And)
34+
var query = new QueryExpression("webresource")
3435
{
35-
Conditions =
36+
ColumnSet = new ColumnSet("content", "name"),
37+
Criteria = new FilterExpression(LogicalOperator.And)
3638
{
37-
new ConditionExpression("name", ConditionOperator.In, logicalNameToIconName.Values.ToList())
39+
Conditions =
40+
{
41+
new ConditionExpression("name", ConditionOperator.In, logicalNameToIconName.Values.ToList())
42+
}
3843
}
39-
}
40-
};
44+
};
4145

42-
var webresources = await client.RetrieveMultipleAsync(query);
43-
var iconNameToSvg = webresources.Entities.ToDictionary(x => x.GetAttributeValue<string>("name"), x => x.GetAttributeValue<string>("content"));
46+
var webresources = await client.RetrieveMultipleAsync(query);
47+
iconNameToSvg = webresources.Entities.ToDictionary(x => x.GetAttributeValue<string>("name"), x => x.GetAttributeValue<string>("content"));
48+
}
4449

4550
var logicalNameToSvg =
4651
logicalNameToIconName

0 commit comments

Comments
 (0)