Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7401a63
chore: initial instructions
Lucki2g Sep 2, 2025
54b9e6f
chore: radix to mui migration. tailwind v3 to v4 migration. next-js v…
Lucki2g Sep 2, 2025
6c54283
fix: fixed postcss issues with tailwind
Lucki2g Sep 2, 2025
1badd50
fix: last build errors
Lucki2g Sep 3, 2025
2e429ad
chore: loginpage recreated
Lucki2g Sep 3, 2025
abab4cf
chore: login touches
Lucki2g Sep 3, 2025
312963c
chore: loading state and beautify login/loading
Lucki2g Sep 3, 2025
11c7ac5
feat: initial start on sidebar, header and layout components
Lucki2g Sep 3, 2025
77b3389
chore: starting on the main layout components
Lucki2g Sep 3, 2025
a726dc9
chore: removed hook and moved logic to context
Lucki2g Sep 4, 2025
82131d6
chore: working notch Box
Lucki2g Sep 4, 2025
ee362bf
chore: replaced notchbox with svg clip-path logic
Lucki2g Sep 4, 2025
491c8a6
feat: Carousel component
Lucki2g Sep 4, 2025
977bd9d
fix: scrolling broken on homepage
Lucki2g Sep 4, 2025
060a159
chore: slide animation out for carousel
Lucki2g Sep 6, 2025
7bbe528
chore: initial dark theme implementation
Lucki2g Sep 6, 2025
00a4d99
chore: minro dark mode and settings pane settings
Lucki2g Sep 6, 2025
cb06a8c
chore: additional work on login screen, to allow dark mode settings c…
Lucki2g Sep 6, 2025
3aee2e3
fix: changed authhook to context for faster performance. and Fixed re…
Lucki2g Sep 6, 2025
38dd443
fix: mobile sidebar and responsiveness on welcome message. Smooth tra…
Lucki2g Sep 6, 2025
8565c8a
chore: initial redesign and work on metadata page. Sidebar changed to…
Lucki2g Sep 6, 2025
7868fbb
chore: minor changes to sidebar for metadata view
Lucki2g Sep 6, 2025
1748e38
fix: font preload to remove jitter
Lucki2g Sep 6, 2025
0f7f1c5
chore: performance changes to the sidebar for metadata and also the s…
Lucki2g Sep 6, 2025
667332d
chore: debugging
Lucki2g Sep 6, 2025
8b0b8a5
feat: swaped the font to an online one, due to constant jitter...
Lucki2g Sep 6, 2025
1bfe555
chore: section darkmode support (headers, security roles, etc.)
Lucki2g Sep 6, 2025
6a09ddd
chore: Global search style changing and not visible when settings are
Lucki2g Sep 6, 2025
ff0ecc0
chore: increased mobile feeling
Lucki2g Sep 6, 2025
fcd7dee
chore: adjustments to sidebar and spinner for loading section.
Lucki2g Sep 6, 2025
46a2138
chore: About page updated for dark theme
Lucki2g Sep 6, 2025
d2bd443
chore: chip style changes
Lucki2g Sep 7, 2025
0296062
chore: cleaning up the diagram view and ensuring it still works. No m…
Lucki2g Sep 7, 2025
3647b19
feat: refactored Magnuses plugin logic for future process logic
Lucki2g Sep 7, 2025
eccdb43
feat: inital process logic
Lucki2g Sep 7, 2025
5669fd5
chore: uninstallation of lucide and refactor to MUI Icons
Lucki2g Sep 7, 2025
ab501ba
feat: initial processes page with serachable information about procce…
Lucki2g Sep 7, 2025
489b5b4
fix: removed overflow in sidebar
Lucki2g Sep 7, 2025
7d2117a
chore: new hightlight in navbar
Lucki2g Sep 7, 2025
4160c1e
fix: mobile size increased. Close sidebar when section is clicked
Lucki2g Sep 8, 2025
6c7ec96
chore: mobile friendlyness for attributes table. npm audit fix to fix…
Lucki2g Sep 8, 2025
f8f8e66
fix: show tooltip on nav links
Lucki2g Sep 8, 2025
81ea37e
chore: removed a-tag from section title
Lucki2g Sep 8, 2025
b114b6e
feat: Power Automate Analzyer
Lucki2g Sep 9, 2025
aacea90
fix: only search for attributes with usages
Lucki2g Sep 9, 2025
6b2e2d8
fix: truncate long groupname
Lucki2g Sep 9, 2025
0840cc4
chore: homepage change
Lucki2g Sep 9, 2025
26d0158
fix: package removals/upgrades and lint fixes
Lucki2g Sep 9, 2025
7987467
chore: more updates to homepage
Lucki2g Sep 9, 2025
9b04832
chore: operationtype other, and final lint error
Lucki2g Sep 9, 2025
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
3 changes: 3 additions & 0 deletions Generator/DTO/Analyzeable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ο»Ώnamespace Generator.DTO;

public record Analyzeable { }
27 changes: 27 additions & 0 deletions Generator/DTO/AttributeUsage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
ο»Ώnamespace Generator.DTO;

public enum ComponentType
{
PowerAutomateFlow,
Plugin,
WebResource,
WorkflowActivity,
CustomApi
}

public enum OperationType
{
Create,
Read,
Update,
Delete,
List,
Other
}

public record AttributeUsage(
string Name,
string Usage,
OperationType OperationType,
ComponentType ComponentType
);
3 changes: 1 addition & 2 deletions Generator/DTO/Attributes/Attribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ public abstract class Attribute
public bool IsStandardFieldModified { get; set; }
public bool IsCustomAttribute { get; set; }
public bool IsPrimaryId { get; set; }
public HashSet<string> PluginTypeNames { get; set; } = new HashSet<string>();
public bool HasPluginStep => PluginTypeNames.Count > 0;
public List<AttributeUsage> AttributeUsages { get; set; } = new List<AttributeUsage>();
public string DisplayName { get; }
public string SchemaName { get; }
public string Description { get; }
Expand Down
6 changes: 6 additions & 0 deletions Generator/DTO/PowerAutomateFlow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ο»Ώnamespace Generator.DTO;

public record PowerAutomateFlow(
string Id,
string Name,
string ClientData) : Analyzeable();
10 changes: 10 additions & 0 deletions Generator/DTO/SDKStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ο»Ώusing Microsoft.Xrm.Sdk;

namespace Generator.DTO;

public record SDKStep(
string Id,
string Name,
string FilteringAttributes,
string PrimaryObjectTypeCode,
OptionSetValue State) : Analyzeable();
136 changes: 32 additions & 104 deletions Generator/DataverseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
using Azure.Identity;
using Generator.DTO;
using Generator.DTO.Attributes;
using Generator.Queries;
using Generator.Services;
using Generator.Services.Plugins;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
Expand All @@ -22,6 +25,9 @@ internal class DataverseService
private readonly IConfiguration configuration;
private readonly ILogger<DataverseService> logger;

private readonly PluginAnalyzer pluginAnalyzer;
private readonly PowerAutomateFlowAnalyzer flowAnalyzer;

public DataverseService(IConfiguration configuration, ILogger<DataverseService> logger)
{
this.configuration = configuration;
Expand All @@ -38,6 +44,9 @@ public DataverseService(IConfiguration configuration, ILogger<DataverseService>
client = new ServiceClient(
instanceUrl: new Uri(dataverseUrl),
tokenProviderFunction: url => TokenProviderFunction(url, cache, logger));

pluginAnalyzer = new PluginAnalyzer(client);
flowAnalyzer = new PowerAutomateFlowAnalyzer(client);
}

public async Task<IEnumerable<Record>> GetFilteredMetadata()
Expand Down Expand Up @@ -84,7 +93,18 @@ public async Task<IEnumerable<Record>> GetFilteredMetadata()
var attributeLogicalToSchema = allEntityMetadata.ToDictionary(x => x.LogicalName, x => x.Attributes?.ToDictionary(attr => attr.LogicalName, attr => attr.DisplayName.UserLocalizedLabel?.Label ?? attr.SchemaName) ?? []);

var entityIconMap = await GetEntityIconMap(allEntityMetadata);
var pluginStepAttributeMap = await GetPluginStepAttributes(logicalToSchema.Keys.ToHashSet(), pluginStepsInSolution);
// Processes analysis
var attributeUsages = new Dictionary<string, Dictionary<string, List<AttributeUsage>>>();
// Plugins
var pluginCollection = await client.GetSDKMessageProcessingStepsAsync(solutionIds);
logger.LogInformation($"There are {pluginCollection.Count()} plugin sdk steps in the environment.");
foreach (var plugin in pluginCollection)
await pluginAnalyzer.AnalyzeComponentAsync(plugin, attributeUsages);
// Flows
var flowCollection = await client.GetPowerAutomateFlowsAsync(solutionIds);
logger.LogInformation($"There are {flowCollection.Count()} Power Automate flows in the environment.");
foreach (var flow in flowCollection)
await flowAnalyzer.AnalyzeComponentAsync(flow, attributeUsages);

var records =
entitiesInSolutionMetadata
Expand Down Expand Up @@ -120,7 +140,7 @@ public async Task<IEnumerable<Record>> GetFilteredMetadata()
securityRoles ?? [],
keys ?? [],
entityIconMap,
pluginStepAttributeMap,
attributeUsages,
configuration);
});
}
Expand All @@ -135,16 +155,14 @@ private static Record MakeRecord(
List<SecurityRole> securityRoles,
List<Key> keys,
Dictionary<string, string> entityIconMap,
Dictionary<string, Dictionary<string, HashSet<string>>> pluginStepAttributeMap,
Dictionary<string, Dictionary<string, List<AttributeUsage>>> attributeUsages,
IConfiguration configuration)
{
var attributes =
relevantAttributes
.Select(metadata =>
{
pluginStepAttributeMap.TryGetValue(entity.LogicalName, out var entityPluginAttributes);
var pluginTypeNames = entityPluginAttributes?.GetValueOrDefault(metadata.LogicalName) ?? new HashSet<string>();
var attr = GetAttribute(metadata, entity, logicalToSchema, pluginTypeNames, logger);
var attr = GetAttribute(metadata, entity, logicalToSchema, attributeUsages, logger);
attr.IsStandardFieldModified = MetadataExtensions.StandardFieldHasChanged(metadata, entity.DisplayName.UserLocalizedLabel?.Label ?? string.Empty);
return attr;
})
Expand Down Expand Up @@ -220,7 +238,7 @@ private static Record MakeRecord(
iconBase64);
}

private static Attribute GetAttribute(AttributeMetadata metadata, EntityMetadata entity, Dictionary<string, ExtendedEntityInformation> logicalToSchema, HashSet<string> pluginTypeNames, ILogger<DataverseService> logger)
private static Attribute GetAttribute(AttributeMetadata metadata, EntityMetadata entity, Dictionary<string, ExtendedEntityInformation> logicalToSchema, Dictionary<string, Dictionary<string, List<AttributeUsage>>> attributeUsages, ILogger<DataverseService> logger)
{
Attribute attr = metadata switch
{
Expand All @@ -239,7 +257,12 @@ private static Attribute GetAttribute(AttributeMetadata metadata, EntityMetadata
FileAttributeMetadata fileAttribute => new FileAttribute(fileAttribute),
_ => new GenericAttribute(metadata)
};
attr.PluginTypeNames = pluginTypeNames;

var schemaname = attributeUsages.GetValueOrDefault(entity.LogicalName)?.GetValueOrDefault(metadata.LogicalName) ?? [];
// also check the plural name, as some workflows like Power Automate use collectionname
var pluralname = attributeUsages.GetValueOrDefault(entity.LogicalCollectionName)?.GetValueOrDefault(metadata.LogicalName) ?? [];

attr.AttributeUsages = [.. schemaname, .. pluralname];
return attr;
}

Expand Down Expand Up @@ -356,7 +379,7 @@ await Parallel.ForEachAsync(
{
Conditions =
{
new ConditionExpression("componenttype", ConditionOperator.In, new List<int>() { 1, 2, 20, 92 }), // entity, attribute, role, pluginstep (https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/entities/solutioncomponent)
new ConditionExpression("componenttype", ConditionOperator.In, new List<int>() { 1, 2, 20, 92 }), // entity, attribute, role, sdkpluginstep (https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/entities/solutioncomponent)
new ConditionExpression("solutionid", ConditionOperator.In, solutionIds)
}
}
Expand Down Expand Up @@ -548,101 +571,6 @@ private static string GetCoreUrl(string url)
return $"{uri.Scheme}://{uri.Host}";
}

private async Task<Dictionary<string, Dictionary<string, HashSet<string>>>> GetPluginStepAttributes(HashSet<string> relevantLogicalNames, List<Guid> pluginStepsInSolution)
{
logger.LogInformation("Retrieving plugin step attributes...");

var pluginStepAttributeMap = new Dictionary<string, Dictionary<string, HashSet<string>>>();

try
{
// Query sdkmessageprocessingstep table for steps with filtering attributes
var stepQuery = new QueryExpression("sdkmessageprocessingstep")
{
ColumnSet = new ColumnSet("filteringattributes", "sdkmessagefilterid", "sdkmessageprocessingstepid"),
Criteria = new FilterExpression
{
Conditions =
{
new ConditionExpression("filteringattributes", ConditionOperator.NotNull),
new ConditionExpression("filteringattributes", ConditionOperator.NotEqual, ""),
new ConditionExpression("statecode", ConditionOperator.Equal, 0) // Only active steps
}
},
LinkEntities =
{
new LinkEntity
{
LinkFromEntityName = "sdkmessageprocessingstep",
LinkFromAttributeName = "sdkmessagefilterid",
LinkToEntityName = "sdkmessagefilter",
LinkToAttributeName = "sdkmessagefilterid",
Columns = new ColumnSet("primaryobjecttypecode"),
EntityAlias = "filter"
},
new LinkEntity
{
LinkFromEntityName = "sdkmessageprocessingstep",
LinkFromAttributeName = "plugintypeid",
LinkToEntityName = "plugintype",
LinkToAttributeName = "plugintypeid",
Columns = new ColumnSet("name"),
EntityAlias = "plugintype"
}
}
};

// Add solution filtering if plugin steps in solution are specified
if (pluginStepsInSolution.Count > 0)
{
stepQuery.Criteria.Conditions.Add(
new ConditionExpression("sdkmessageprocessingstepid", ConditionOperator.In, pluginStepsInSolution));
}

var stepResults = await client.RetrieveMultipleAsync(stepQuery);

foreach (var step in stepResults.Entities)
{
var filteringAttributes = step.GetAttributeValue<string>("filteringattributes");
var entityLogicalName = step.GetAttributeValue<AliasedValue>("filter.primaryobjecttypecode")?.Value as string;
var pluginTypeName = step.GetAttributeValue<AliasedValue>("plugintype.name")?.Value as string;

if (string.IsNullOrEmpty(filteringAttributes) || string.IsNullOrEmpty(entityLogicalName) || string.IsNullOrEmpty(pluginTypeName))
continue;

// Get entity logical name from metadata mapping
if (!relevantLogicalNames.Contains(entityLogicalName))
{
logger.LogDebug("Unknown entity type code: {TypeCode}", entityLogicalName);
continue;
}

if (!pluginStepAttributeMap.ContainsKey(entityLogicalName))
pluginStepAttributeMap[entityLogicalName] = new Dictionary<string, HashSet<string>>();

// Parse comma-separated attribute names
var attributeNames = filteringAttributes.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var attributeName in attributeNames)
{
var trimmedAttributeName = attributeName.Trim();
if (!pluginStepAttributeMap[entityLogicalName].ContainsKey(trimmedAttributeName))
pluginStepAttributeMap[entityLogicalName][trimmedAttributeName] = new HashSet<string>();

var pluginTypeNameParts = pluginTypeName.Split('.');
pluginStepAttributeMap[entityLogicalName][trimmedAttributeName].Add(pluginTypeNameParts[pluginTypeNameParts.Length - 1]);
}
}

logger.LogInformation("Found {Count} entities with plugin step attributes.", pluginStepAttributeMap.Count);
}
catch (Exception ex)
{
logger.LogWarning("Failed to retrieve plugin step attributes: {Message}", ex.Message);
}

return pluginStepAttributeMap;
}

private static async Task<AccessToken> FetchAccessToken(TokenCredential credential, string scope, ILogger logger)
{
var tokenRequestContext = new TokenRequestContext(new[] { scope });
Expand Down
86 changes: 86 additions & 0 deletions Generator/Queries/PluginQueries.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
ο»Ώusing Generator.DTO;
using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System.Data;

namespace Generator.Queries;

public static class PluginQueries
{

public static async Task<IEnumerable<SDKStep>> GetSDKMessageProcessingStepsAsync(this ServiceClient service, List<Guid>? solutionIds = null)
{
// Retrieve the SDK Message Processing Step entity using the componentId
var query = new QueryExpression("solutioncomponent")
{
ColumnSet = new ColumnSet("objectid"),
Criteria = new FilterExpression(LogicalOperator.And)
{
Conditions =
{
new ConditionExpression("solutionid", ConditionOperator.In, solutionIds),
new ConditionExpression("componenttype", ConditionOperator.Equal, 92)
}
},
LinkEntities =
{
new LinkEntity(
"solutioncomponent",
"sdkmessageprocessingstep",
"objectid",
"sdkmessageprocessingstepid",
JoinOperator.Inner)
{
Columns = new ColumnSet("sdkmessageprocessingstepid", "name", "filteringattributes", "sdkmessageid", "statecode"),
EntityAlias = "step",
LinkEntities =
{
new LinkEntity(
"sdkmessageprocessingstep",
"sdkmessagefilter",
"sdkmessagefilterid",
"sdkmessagefilterid",
JoinOperator.LeftOuter)
{
Columns = new ColumnSet("primaryobjecttypecode"),
EntityAlias = "filter"
}
//new LinkEntity
//{
// LinkFromEntityName = "sdkmessageprocessingstep",
// LinkFromAttributeName = "plugintypeid",
// LinkToEntityName = "plugintype",
// LinkToAttributeName = "plugintypeid",
// Columns = new ColumnSet("name"),
// EntityAlias = "plugintype"
//}
}
}
}
};


//if (solutionIds is not null) query.Criteria.Conditions.Add(new ConditionExpression("solutionid", ConditionOperator.In, solutionIds));
var result = await service.RetrieveMultipleAsync(query);

var steps = result.Entities.Select(e =>
{
var sdkMessageId = e.GetAttributeValue<AliasedValue>("step.sdkmessageid")?.Value as EntityReference;
var sdkMessageName = e.GetAttributeValue<AliasedValue>("step.name")?.Value as string;
var sdkFilterAttributes = e.GetAttributeValue<AliasedValue>("step.filteringattributes")?.Value as string;
var sdkState = e.GetAttributeValue<AliasedValue>("step.statecode")?.Value as OptionSetValue;
var filterTypeCode = e.GetAttributeValue<AliasedValue>("filter.primaryobjecttypecode")?.Value as string;

return new SDKStep(
sdkMessageId.Id.ToString(),

Check warning on line 76 in Generator/Queries/PluginQueries.cs

View workflow job for this annotation

GitHub Actions / generator

Dereference of a possibly null reference.
sdkMessageName ?? "Unknown Name",
sdkFilterAttributes ?? "",
filterTypeCode,

Check warning on line 79 in Generator/Queries/PluginQueries.cs

View workflow job for this annotation

GitHub Actions / generator

Possible null reference argument for parameter 'PrimaryObjectTypeCode' in 'SDKStep.SDKStep(string Id, string Name, string FilteringAttributes, string PrimaryObjectTypeCode, OptionSetValue State)'.
sdkState
);
});

return steps;
}
}
Loading
Loading