Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion Generator/DTO/SolutionComponent.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,49 @@
namespace Generator.DTO;

/// <summary>
/// Solution component types from Dataverse.
/// See: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/entities/solutioncomponent
/// </summary>
public enum SolutionComponentType
{
Entity = 1,
Attribute = 2,
Relationship = 3,
OptionSet = 9,
Relationship = 10,
EntityKey = 14,
SecurityRole = 20,
SavedQuery = 26,
Workflow = 29,
RibbonCustomization = 50,
SavedQueryVisualization = 59,
SystemForm = 60,
WebResource = 61,
SiteMap = 62,
ConnectionRole = 63,
HierarchyRule = 65,
CustomControl = 66,
FieldSecurityProfile = 70,
ModelDrivenApp = 80,
PluginAssembly = 91,
SDKMessageProcessingStep = 92,
CanvasApp = 300,
ConnectionReference = 372,
EnvironmentVariableDefinition = 380,
EnvironmentVariableValue = 381,
Dataflow = 418,
ConnectionRoleObjectTypeCode = 3233,
CustomAPI = 10240,
CustomAPIRequestParameter = 10241,
CustomAPIResponseProperty = 10242,
PluginPackage = 10639,
OrganizationSetting = 10563,
AppAction = 10645,
AppActionRule = 10948,
FxExpression = 11492,
DVFileSearch = 11723,
DVFileSearchAttribute = 11724,
DVFileSearchEntity = 11725,
AISkillConfig = 12075,
}

public record SolutionComponent(
Expand All @@ -14,3 +53,22 @@ public record SolutionComponent(
SolutionComponentType ComponentType,
string PublisherName,
string PublisherPrefix);

/// <summary>
/// Represents a solution component with its solution membership info for the insights view.
/// </summary>
public record SolutionComponentData(
string Name,
string SchemaName,
SolutionComponentType ComponentType,
Guid ObjectId,
bool IsExplicit,
string? RelatedTable = null);

/// <summary>
/// Collection of solution components grouped by solution.
/// </summary>
public record SolutionComponentCollection(
Guid SolutionId,
string SolutionName,
List<SolutionComponentData> Components);
80 changes: 77 additions & 3 deletions Generator/DataverseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal class DataverseService
private readonly EntityIconService entityIconService;
private readonly RecordMappingService recordMappingService;
private readonly SolutionComponentService solutionComponentService;
private readonly SolutionComponentExtractor solutionComponentExtractor;
private readonly WorkflowService workflowService;
private readonly RelationshipService relationshipService;

Expand All @@ -38,6 +39,7 @@ public DataverseService(
EntityIconService entityIconService,
RecordMappingService recordMappingService,
SolutionComponentService solutionComponentService,
SolutionComponentExtractor solutionComponentExtractor,
WorkflowService workflowService,
RelationshipService relationshipService)
{
Expand All @@ -49,6 +51,7 @@ public DataverseService(
this.recordMappingService = recordMappingService;
this.workflowService = workflowService;
this.relationshipService = relationshipService;
this.solutionComponentExtractor = solutionComponentExtractor;

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

public async Task<(IEnumerable<Record>, IEnumerable<SolutionWarning>)> GetFilteredMetadata()
public async Task<(IEnumerable<Record>, IEnumerable<SolutionWarning>, IEnumerable<SolutionComponentCollection>)> GetFilteredMetadata()
{
// used to collect warnings for the insights dashboard
var warnings = new List<SolutionWarning>();
Expand Down Expand Up @@ -275,8 +278,79 @@ public DataverseService(
})
.ToList();

logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed - returning empty results");
return (records, warnings);
/// SOLUTION COMPONENTS FOR INSIGHTS
List<SolutionComponentCollection> solutionComponentCollections;
try
{
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracting solution components for insights view");

// Build name lookups from entity metadata for the extractor
var entityNameLookup = entitiesInSolutionMetadata.ToDictionary(
e => e.MetadataId!.Value,
e => e.DisplayName.ToLabelString() ?? e.SchemaName);

var attributeNameLookup = entitiesInSolutionMetadata
.SelectMany(e => e.Attributes.Where(a => a.MetadataId.HasValue))
.ToDictionary(
a => a.MetadataId!.Value,
a => a.DisplayName.ToLabelString() ?? a.SchemaName);

var relationshipNameLookup = entitiesInSolutionMetadata
.SelectMany(e => e.ManyToManyRelationships.Cast<RelationshipMetadataBase>()
.Concat(e.OneToManyRelationships)
.Concat(e.ManyToOneRelationships))
.Where(r => r.MetadataId.HasValue)
.DistinctBy(r => r.MetadataId!.Value)
.ToDictionary(
r => r.MetadataId!.Value,
r => r.SchemaName);

// Build entity lookups for attributes, relationships, and keys (maps component ID to parent entity name)
var attributeEntityLookup = entitiesInSolutionMetadata
.SelectMany(e => e.Attributes.Where(a => a.MetadataId.HasValue)
.Select(a => (AttributeId: a.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
.ToDictionary(x => x.AttributeId, x => x.EntityName);

var relationshipEntityLookup = entitiesInSolutionMetadata
.SelectMany(e => e.ManyToManyRelationships.Cast<RelationshipMetadataBase>()
.Concat(e.OneToManyRelationships)
.Concat(e.ManyToOneRelationships)
.Where(r => r.MetadataId.HasValue)
.Select(r => (RelationshipId: r.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
.DistinctBy(x => x.RelationshipId)
.ToDictionary(x => x.RelationshipId, x => x.EntityName);

var keyEntityLookup = entitiesInSolutionMetadata
.SelectMany(e => (e.Keys ?? Array.Empty<EntityKeyMetadata>())
.Where(k => k.MetadataId.HasValue)
.Select(k => (KeyId: k.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName)))
.ToDictionary(x => x.KeyId, x => x.EntityName);

// Build solution name lookup
var solutionNameLookup = solutionLookup.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Name);

solutionComponentCollections = await solutionComponentExtractor.ExtractSolutionComponentsAsync(
solutionIds,
solutionNameLookup,
entityNameLookup,
attributeNameLookup,
relationshipNameLookup,
attributeEntityLookup,
relationshipEntityLookup,
keyEntityLookup);

logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracted components for {solutionComponentCollections.Count} solutions");
}
catch (Exception ex)
{
logger.LogWarning(ex, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Failed to extract solution components for insights, continuing without them");
solutionComponentCollections = new List<SolutionComponentCollection>();
}

logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed");
return (records, warnings, solutionComponentCollections);
}
}

Expand Down
5 changes: 3 additions & 2 deletions Generator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,16 @@
services.AddSingleton<DataverseService>();
services.AddSingleton<WorkflowService>();
services.AddSingleton<SolutionComponentService>();
services.AddSingleton<SolutionComponentExtractor>();

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

// Resolve and use DataverseService
var dataverseService = serviceProvider.GetRequiredService<DataverseService>();
var (entities, warnings) = await dataverseService.GetFilteredMetadata();
var (entities, warnings, solutionComponents) = await dataverseService.GetFilteredMetadata();

var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings);
var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings, solutionComponents);
websiteBuilder.AddData();

// Token provider function
Expand Down
23 changes: 14 additions & 9 deletions Generator/Services/EntityIconService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,25 @@ public async Task<Dictionary<string, string>> GetEntityIconMap(IEnumerable<Entit
.Where(x => x.IconVectorName != null)
.ToDictionary(x => x.LogicalName, x => x.IconVectorName);

var query = new QueryExpression("webresource")
var iconNameToSvg = new Dictionary<string, string>();

if (logicalNameToIconName.Count > 0)
{
ColumnSet = new ColumnSet("content", "name"),
Criteria = new FilterExpression(LogicalOperator.And)
var query = new QueryExpression("webresource")
{
Conditions =
ColumnSet = new ColumnSet("content", "name"),
Criteria = new FilterExpression(LogicalOperator.And)
{
new ConditionExpression("name", ConditionOperator.In, logicalNameToIconName.Values.ToList())
Conditions =
{
new ConditionExpression("name", ConditionOperator.In, logicalNameToIconName.Values.ToList())
}
}
}
};
};

var webresources = await client.RetrieveMultipleAsync(query);
var iconNameToSvg = webresources.Entities.ToDictionary(x => x.GetAttributeValue<string>("name"), x => x.GetAttributeValue<string>("content"));
var webresources = await client.RetrieveMultipleAsync(query);
iconNameToSvg = webresources.Entities.ToDictionary(x => x.GetAttributeValue<string>("name"), x => x.GetAttributeValue<string>("content"));
}

var logicalNameToSvg =
logicalNameToIconName
Expand Down
Loading
Loading