Skip to content

Commit fa27ea9

Browse files
committed
Merge remote-tracking branch 'origin/main' into features/global-option-set
2 parents edf9415 + 6c53230 commit fa27ea9

11 files changed

Lines changed: 1425 additions & 178 deletions

File tree

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>, Dictionary<string, GlobalOptionSetUsage>)> GetFilteredMetadata()
75+
public async Task<(IEnumerable<Record>, IEnumerable<SolutionWarning>, IEnumerable<SolutionComponentCollection>, Dictionary<string, GlobalOptionSetUsage>)> GetFilteredMetadata()
7376
{
7477
// used to collect warnings for the insights dashboard
7578
var warnings = new List<SolutionWarning>();
@@ -315,8 +318,79 @@ public DataverseService(
315318
})
316319
.ToList();
317320

318-
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed - returning {records.Count} records with {globalOptionSetUsages.Count} global option sets");
319-
return (records, warnings, globalOptionSetUsages);
321+
/// SOLUTION COMPONENTS FOR INSIGHTS
322+
List<SolutionComponentCollection> solutionComponentCollections;
323+
try
324+
{
325+
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracting solution components for insights view");
326+
327+
// Build name lookups from entity metadata for the extractor
328+
var entityNameLookup = entitiesInSolutionMetadata.ToDictionary(
329+
e => e.MetadataId!.Value,
330+
e => e.DisplayName.ToLabelString() ?? e.SchemaName);
331+
332+
var attributeNameLookup = entitiesInSolutionMetadata
333+
.SelectMany(e => e.Attributes.Where(a => a.MetadataId.HasValue))
334+
.ToDictionary(
335+
a => a.MetadataId!.Value,
336+
a => a.DisplayName.ToLabelString() ?? a.SchemaName);
337+
338+
var relationshipNameLookup = entitiesInSolutionMetadata
339+
.SelectMany(e => e.ManyToManyRelationships.Cast<RelationshipMetadataBase>()
340+
.Concat(e.OneToManyRelationships)
341+
.Concat(e.ManyToOneRelationships))
342+
.Where(r => r.MetadataId.HasValue)
343+
.DistinctBy(r => r.MetadataId!.Value)
344+
.ToDictionary(
345+
r => r.MetadataId!.Value,
346+
r => r.SchemaName);
347+
348+
// Build entity lookups for attributes, relationships, and keys (maps component ID to parent entity name)
349+
var attributeEntityLookup = entitiesInSolutionMetadata
350+
.SelectMany(e => e.Attributes.Where(a => a.MetadataId.HasValue)
351+
.Select(a => (AttributeId: a.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
352+
.ToDictionary(x => x.AttributeId, x => x.EntityName);
353+
354+
var relationshipEntityLookup = entitiesInSolutionMetadata
355+
.SelectMany(e => e.ManyToManyRelationships.Cast<RelationshipMetadataBase>()
356+
.Concat(e.OneToManyRelationships)
357+
.Concat(e.ManyToOneRelationships)
358+
.Where(r => r.MetadataId.HasValue)
359+
.Select(r => (RelationshipId: r.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
360+
.DistinctBy(x => x.RelationshipId)
361+
.ToDictionary(x => x.RelationshipId, x => x.EntityName);
362+
363+
var keyEntityLookup = entitiesInSolutionMetadata
364+
.SelectMany(e => (e.Keys ?? Array.Empty<EntityKeyMetadata>())
365+
.Where(k => k.MetadataId.HasValue)
366+
.Select(k => (KeyId: k.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
367+
.ToDictionary(x => x.KeyId, x => x.EntityName);
368+
369+
// Build solution name lookup
370+
var solutionNameLookup = solutionLookup.ToDictionary(
371+
kvp => kvp.Key,
372+
kvp => kvp.Value.Name);
373+
374+
solutionComponentCollections = await solutionComponentExtractor.ExtractSolutionComponentsAsync(
375+
solutionIds,
376+
solutionNameLookup,
377+
entityNameLookup,
378+
attributeNameLookup,
379+
relationshipNameLookup,
380+
attributeEntityLookup,
381+
relationshipEntityLookup,
382+
keyEntityLookup);
383+
384+
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracted components for {solutionComponentCollections.Count} solutions");
385+
}
386+
catch (Exception ex)
387+
{
388+
logger.LogWarning(ex, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Failed to extract solution components for insights, continuing without them");
389+
solutionComponentCollections = new List<SolutionComponentCollection>();
390+
}
391+
392+
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed");
393+
return (records, warnings, solutionComponentCollections, globalOptionSetUsages);
320394
}
321395
}
322396

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, globalOptionSetUsages) = await dataverseService.GetFilteredMetadata();
69+
var (entities, warnings, solutionComponents, globalOptionSetUsages) = await dataverseService.GetFilteredMetadata();
6970

70-
var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings, globalOptionSetUsages);
71+
var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings, globalOptionSetUsages, 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)