From 4b2556ebcaebecb7bbdfd6aed6dde8de3a7a56b4 Mon Sep 17 00:00:00 2001 From: Lucki2g Date: Sun, 14 Sep 2025 10:45:59 +0200 Subject: [PATCH 1/8] feat: example of how you would read webresource entityname from forms (does not work as many forms are implicitly added - hence doers have solutioncomponent) --- Generator/DTO/WebResource.cs | 13 ++ Generator/DataverseService.cs | 21 +++ Generator/Queries/WebResourceQueries.cs | 156 ++++++++++++++++++ Generator/Services/ComponentAnalyzerBase.cs | 16 ++ Generator/Services/Plugins/PluginAnalyzer.cs | 11 +- .../PowerAutomateFlowAnalyzer.cs | 16 -- .../WebResources/WebResourceAnalyzer.cs | 74 +++++++++ Generator/UtilityExtensions.cs | 6 + 8 files changed, 287 insertions(+), 26 deletions(-) create mode 100644 Generator/DTO/WebResource.cs create mode 100644 Generator/Queries/WebResourceQueries.cs create mode 100644 Generator/Services/WebResources/WebResourceAnalyzer.cs create mode 100644 Generator/UtilityExtensions.cs diff --git a/Generator/DTO/WebResource.cs b/Generator/DTO/WebResource.cs new file mode 100644 index 0000000..6922ee4 --- /dev/null +++ b/Generator/DTO/WebResource.cs @@ -0,0 +1,13 @@ +using Microsoft.Xrm.Sdk; + +namespace Generator.DTO; + +public record Form(string Id, string Name, string EntityName); + +public record WebResource( + IEnumerable
dependencies, + string Id, + string Name, + string Content, + OptionSetValue WebResourceType, + string? Description = null) : Analyzeable(); diff --git a/Generator/DataverseService.cs b/Generator/DataverseService.cs index 259974c..a068722 100644 --- a/Generator/DataverseService.cs +++ b/Generator/DataverseService.cs @@ -5,6 +5,7 @@ using Generator.Queries; using Generator.Services; using Generator.Services.Plugins; +using Generator.Services.WebResources; using Microsoft.Crm.Sdk.Messages; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; @@ -14,6 +15,7 @@ using Microsoft.Xrm.Sdk.Metadata; using Microsoft.Xrm.Sdk.Query; using System.Collections.Concurrent; +using System.Diagnostics; using System.Reflection; using Attribute = Generator.DTO.Attributes.Attribute; @@ -27,6 +29,7 @@ internal class DataverseService private readonly PluginAnalyzer pluginAnalyzer; private readonly PowerAutomateFlowAnalyzer flowAnalyzer; + private readonly WebResourceAnalyzer webResourceAnalyzer; public DataverseService(IConfiguration configuration, ILogger logger) { @@ -47,6 +50,7 @@ public DataverseService(IConfiguration configuration, ILogger pluginAnalyzer = new PluginAnalyzer(client); flowAnalyzer = new PowerAutomateFlowAnalyzer(client); + webResourceAnalyzer = new WebResourceAnalyzer(client); } public async Task> GetFilteredMetadata() @@ -96,15 +100,32 @@ public async Task> GetFilteredMetadata() // Processes analysis var attributeUsages = new Dictionary>>(); // Plugins + var pluginStopWatch = new Stopwatch(); + pluginStopWatch.Start(); 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); + pluginStopWatch.Stop(); + logger.LogInformation($"Plugin analysis took {pluginStopWatch.ElapsedMilliseconds} ms."); // Flows + var flowStopWatch = new Stopwatch(); + flowStopWatch.Start(); 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); + flowStopWatch.Stop(); + logger.LogInformation($"Power Automate flow analysis took {flowStopWatch.ElapsedMilliseconds} ms."); + // WebResources + var resourceStopWatch = new Stopwatch(); + resourceStopWatch.Start(); + var webresourceCollection = await client.GetWebResourcesAsync(solutionIds); + logger.LogInformation($"There are {webresourceCollection.Count()} WebResources in the environment."); + foreach (var resource in webresourceCollection) + await webResourceAnalyzer.AnalyzeComponentAsync(resource, attributeUsages); + resourceStopWatch.Stop(); + logger.LogInformation($"WebResource analysis took {resourceStopWatch.ElapsedMilliseconds} ms."); var records = entitiesInSolutionMetadata diff --git a/Generator/Queries/WebResourceQueries.cs b/Generator/Queries/WebResourceQueries.cs new file mode 100644 index 0000000..557b188 --- /dev/null +++ b/Generator/Queries/WebResourceQueries.cs @@ -0,0 +1,156 @@ +using Generator.DTO; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; +using System.Xml.Linq; + +namespace Generator.Queries; + +public static class WebResourceQueries +{ + + public static async Task> GetWebResourcesAsync(this ServiceClient service, List? solutionIds = null) + { + 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, 61) // 61 = Web Resource + } + }, + LinkEntities = + { + new LinkEntity( + "solutioncomponent", + "webresource", + "objectid", + "webresourceid", + JoinOperator.Inner) + { + Columns = new ColumnSet("webresourceid", "name", "content", "webresourcetype", "description"), + EntityAlias = "webresource", + LinkCriteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("webresourcetype", ConditionOperator.Equal, 3) // JS Resources + } + } + } + } + }; + + var results = (await service.RetrieveMultipleAsync(query)).Entities; + var formsDependencies = await service.GetDependentForms(solutionIds); + + var webResources = results.Select(e => + { + var content = ""; + var contentValue = e.GetAttributeValue("webresource.content")?.Value; + var webresourceId = e.GetAttributeValue("webresource.webresourceid").Value?.ToString() ?? ""; + var webresourceName = e.GetAttributeValue("webresource.name").Value?.ToString(); + var dependencies = formsDependencies.GetValueOrDefault(webresourceName, Enumerable.Empty()); + if (contentValue != null) + { + // Content is base64 encoded, decode it + var base64Content = contentValue.ToString(); + if (!string.IsNullOrEmpty(base64Content)) + { + try + { + var bytes = Convert.FromBase64String(base64Content); + content = System.Text.Encoding.UTF8.GetString(bytes); + } + catch + { + // If decoding fails, keep the base64 content + content = base64Content; + } + } + } + + return new WebResource( + dependencies, + webresourceId, + webresourceName, + content, + (OptionSetValue)e.GetAttributeValue("webresource.webresourcetype").Value, + e.GetAttributeValue("webresource.description")?.Value?.ToString() + ); + }); + + return webResources; + } + + /// + /// Retrieve all forms in the solutions and return the dependencies to webresources. + /// + /// + /// + /// A dictionary where the key is the weresource id and the value is a list of forms depending on this weresource. + private static async Task>> GetDependentForms(this ServiceClient service, List? solutionIds = null) + { + 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, 60) // 60 = System Form OBS not 24 = Form + } + }, + LinkEntities = + { + new LinkEntity( + "solutioncomponent", + "systemform", + "objectid", + "formid", + JoinOperator.Inner) + { + EntityAlias = "form", + Columns = new ColumnSet("formid", "name", "objecttypecode", "formxml"), + } + } + }; + + var forms = await service.RetrieveMultipleAsync(query); + + var webresources = forms.Entities.SelectMany(form => + { + var content = form.GetAttributeValue("form.formxml").Value.ToString() ?? ""; + var formid = form.GetAttributeValue("form.formid").Value.ToString(); + var formname = form.GetAttributeValue("form.name").Value.ToString(); + var entityname = form.GetAttributeValue("form.objecttypecode").Value.ToString(); + + var doc = XDocument.Parse(content, LoadOptions.PreserveWhitespace); + + var libraries = doc.Descendants("formLibraries") + .Descendants("Library") + .Select(lib => new + { + Name = lib.Attribute("name")?.Value, + Form = formid, + FormName = formname, + FormEntity = entityname + }); + + return libraries; + }); + + return webresources + .Where(x => !string.IsNullOrWhiteSpace(x.Name)) + .GroupBy(x => x.Name!) + .ToDictionary( + g => g.Key, + g => g.Select(x => new Form(x.Form, x.FormName, x.FormEntity)) + .Distinct() + ); + } +} diff --git a/Generator/Services/ComponentAnalyzerBase.cs b/Generator/Services/ComponentAnalyzerBase.cs index fd6d260..6975537 100644 --- a/Generator/Services/ComponentAnalyzerBase.cs +++ b/Generator/Services/ComponentAnalyzerBase.cs @@ -11,6 +11,22 @@ public abstract class BaseComponentAnalyzer(ServiceClient service) : ICompone public abstract ComponentType SupportedType { get; } public abstract Task AnalyzeComponentAsync(T component, Dictionary>> attributeUsages); + protected void AddAttributeUsage(Dictionary>> attributeUsages, + string entityName, string attributeName, AttributeUsage usage) + { + if (!attributeUsages.ContainsKey(entityName)) + { + attributeUsages[entityName] = new Dictionary>(); + } + + if (!attributeUsages[entityName].ContainsKey(attributeName)) + { + attributeUsages[entityName][attributeName] = new List(); + } + + attributeUsages[entityName][attributeName].Add(usage); + } + protected List ExtractFieldsFromODataFilter(string filter) { var fields = new List(); diff --git a/Generator/Services/Plugins/PluginAnalyzer.cs b/Generator/Services/Plugins/PluginAnalyzer.cs index 745b4d6..5db7e71 100644 --- a/Generator/Services/Plugins/PluginAnalyzer.cs +++ b/Generator/Services/Plugins/PluginAnalyzer.cs @@ -23,17 +23,8 @@ public override async Task AnalyzeComponentAsync(SDKStep sdkStep, Dictionary>(); + AddAttributeUsage(attributeUsages, logicalTableName, attribute, new AttributeUsage(pluginName, $"Used in filterattributes", OperationType.Other, SupportedType)); - if (!attributeUsages[logicalTableName].ContainsKey(attribute)) - attributeUsages[logicalTableName][attribute] = new List(); - - // Add the usage information (assuming AttributeUsage is a defined class) - - attributeUsages[logicalTableName][attribute].Add(new AttributeUsage(pluginName, $"Used in filterattributes", OperationType.Other, SupportedType)); - } } catch (Exception ex) { diff --git a/Generator/Services/Power Automate/PowerAutomateFlowAnalyzer.cs b/Generator/Services/Power Automate/PowerAutomateFlowAnalyzer.cs index b295d1a..de995fc 100644 --- a/Generator/Services/Power Automate/PowerAutomateFlowAnalyzer.cs +++ b/Generator/Services/Power Automate/PowerAutomateFlowAnalyzer.cs @@ -448,20 +448,4 @@ private OperationType DetermineOperationTypeFromAction(string actionName) // Default to Other for unknown actions return OperationType.Other; } - - private void AddAttributeUsage(Dictionary>> attributeUsages, - string entityName, string attributeName, AttributeUsage usage) - { - if (!attributeUsages.ContainsKey(entityName)) - { - attributeUsages[entityName] = new Dictionary>(); - } - - if (!attributeUsages[entityName].ContainsKey(attributeName)) - { - attributeUsages[entityName][attributeName] = new List(); - } - - attributeUsages[entityName][attributeName].Add(usage); - } } diff --git a/Generator/Services/WebResources/WebResourceAnalyzer.cs b/Generator/Services/WebResources/WebResourceAnalyzer.cs new file mode 100644 index 0000000..ccc2226 --- /dev/null +++ b/Generator/Services/WebResources/WebResourceAnalyzer.cs @@ -0,0 +1,74 @@ +using Generator.DTO; +using Microsoft.PowerPlatform.Dataverse.Client; +using System.Text.RegularExpressions; + +namespace Generator.Services.WebResources; + +public class WebResourceAnalyzer : BaseComponentAnalyzer +{ + public WebResourceAnalyzer(ServiceClient service) : base(service) { } + + public override ComponentType SupportedType => ComponentType.WebResource; + + public override async Task AnalyzeComponentAsync(WebResource webResource, Dictionary>> attributeUsages) + { + try + { + if (string.IsNullOrEmpty(webResource.Content)) + return; + + // Analyze JavaScript content for onChange event handlers and getAttribute calls + AnalyzeOnChangeHandlers(webResource, attributeUsages); + } + catch (Exception ex) + { + Console.WriteLine($"Error analyzing web resource {webResource.Name}: {ex.Message}"); + } + + await Task.CompletedTask; + } + + private void AnalyzeOnChangeHandlers(WebResource webResource, Dictionary>> attributeUsages) + { + var content = webResource.Content; + + var attributeNames = ExtractGetAttributeCalls(content); + foreach (var attributeName in attributeNames) + foreach (var form in webResource.dependencies) + AddAttributeUsage(attributeUsages, form.EntityName, attributeName, new AttributeUsage( + webResource.Name, + $"getAttribute call", + OperationType.Read, + SupportedType + )); + } + + // TODO get attributes used in XrmApi or XrmQuery calls + + // TODO get attributes from getControl + + private List ExtractGetAttributeCalls(string code) + { + var attributes = new List(); + + if (string.IsNullOrEmpty(code)) + return attributes; + + // Examples: + // formContext.getAttribute("firstname") + // Xrm.Page.getAttribute("lastname") + // executionContext.getFormContext().getAttribute("email") + // context.getAttribute("phonenumber") + // this.getAttribute("address1_city") + var getAttributePattern = @"(\w+(?:\.\w+)*\.getAttribute)\([""']([^""']+)[""']\)"; + var matches = Regex.Matches(code, getAttributePattern, RegexOptions.IgnoreCase); + + foreach (Match match in matches) + { + var attributeName = match.Groups[2].Value; + attributes.Add(attributeName); + } + + return attributes.Distinct().ToList(); + } +} diff --git a/Generator/UtilityExtensions.cs b/Generator/UtilityExtensions.cs new file mode 100644 index 0000000..0e17d69 --- /dev/null +++ b/Generator/UtilityExtensions.cs @@ -0,0 +1,6 @@ +namespace Generator; + +public static class UtilityExtensions +{ + public static string StripGuid(this string guid) => guid.Replace("{", "").Replace("}", "").ToLower(); +} From 5ee3e9cbddbdcacf11d90e4a2baa933d492f1b1d Mon Sep 17 00:00:00 2001 From: Lucki2g Date: Sun, 14 Sep 2025 11:56:38 +0200 Subject: [PATCH 2/8] feat: solution warnings and lambda expression for webresource entity name extraction --- Generator/DTO/Warnings/AttributeWarning.cs | 6 ++ Generator/DTO/Warnings/SolutionWarning.cs | 11 +++ Generator/DTO/WebResource.cs | 3 - Generator/DataverseService.cs | 19 +++-- Generator/Generator.csproj | 1 + Generator/Program.cs | 4 +- Generator/Queries/WebResourceQueries.cs | 72 ------------------- .../WebResources/WebResourceAnalyzer.cs | 26 ++++--- Generator/WebsiteBuilder.cs | 21 +++++- Website/lib/Types.ts | 9 +++ 10 files changed, 80 insertions(+), 92 deletions(-) create mode 100644 Generator/DTO/Warnings/AttributeWarning.cs create mode 100644 Generator/DTO/Warnings/SolutionWarning.cs diff --git a/Generator/DTO/Warnings/AttributeWarning.cs b/Generator/DTO/Warnings/AttributeWarning.cs new file mode 100644 index 0000000..3df0e95 --- /dev/null +++ b/Generator/DTO/Warnings/AttributeWarning.cs @@ -0,0 +1,6 @@ +namespace Generator.DTO.Warnings; + +public record AttributeWarning(string Message) : SolutionWarning( + SolutionWarningType.Attribute, + Message + ); diff --git a/Generator/DTO/Warnings/SolutionWarning.cs b/Generator/DTO/Warnings/SolutionWarning.cs new file mode 100644 index 0000000..25871b8 --- /dev/null +++ b/Generator/DTO/Warnings/SolutionWarning.cs @@ -0,0 +1,11 @@ +namespace Generator.DTO.Warnings; + +public enum SolutionWarningType +{ + Attribute, +} + +public record SolutionWarning( + SolutionWarningType Type, + string Message + ); diff --git a/Generator/DTO/WebResource.cs b/Generator/DTO/WebResource.cs index 6922ee4..b69c714 100644 --- a/Generator/DTO/WebResource.cs +++ b/Generator/DTO/WebResource.cs @@ -2,10 +2,7 @@ namespace Generator.DTO; -public record Form(string Id, string Name, string EntityName); - public record WebResource( - IEnumerable dependencies, string Id, string Name, string Content, diff --git a/Generator/DataverseService.cs b/Generator/DataverseService.cs index a068722..3bf58bd 100644 --- a/Generator/DataverseService.cs +++ b/Generator/DataverseService.cs @@ -2,6 +2,7 @@ using Azure.Identity; using Generator.DTO; using Generator.DTO.Attributes; +using Generator.DTO.Warnings; using Generator.Queries; using Generator.Services; using Generator.Services.Plugins; @@ -50,11 +51,12 @@ public DataverseService(IConfiguration configuration, ILogger pluginAnalyzer = new PluginAnalyzer(client); flowAnalyzer = new PowerAutomateFlowAnalyzer(client); - webResourceAnalyzer = new WebResourceAnalyzer(client); + webResourceAnalyzer = new WebResourceAnalyzer(client, configuration); } - public async Task> GetFilteredMetadata() + public async Task<(IEnumerable, IEnumerable)> GetFilteredMetadata() { + var warnings = new List(); // used to collect warnings for the insights dashboard var (publisherPrefix, solutionIds) = await GetSolutionIds(); var solutionComponents = await GetSolutionComponents(solutionIds); // (id, type, rootcomponentbehavior) @@ -144,8 +146,17 @@ public async Task> GetFilteredMetadata() .Where(x => x.EntityMetadata.DisplayName.UserLocalizedLabel?.Label != null) .ToList(); + // Warn about attributes that were used in processes, but the entity could not be resolved from e.g. JavaScript file name or similar + var hash = entitiesInSolutionMetadata.SelectMany(r => [r.LogicalCollectionName?.ToLower() ?? "", r.LogicalName.ToLower()]).ToHashSet(); + warnings.AddRange(attributeUsages.Keys + .Where(k => !hash.Contains(k.ToLower())) + .SelectMany(entityKey => attributeUsages.GetValueOrDefault(entityKey)! + .SelectMany(attributeDict => attributeDict.Value + .Select(usage => + new AttributeWarning($"{attributeDict.Key} was used inside a {usage.ComponentType} component [{usage.Name}]. However, the entity {entityKey} could not be resolved in the provided solutions."))))); - return records + + return (records .Select(x => { logicalNameToSecurityRoles.TryGetValue(x.EntityMetadata.LogicalName, out var securityRoles); @@ -163,7 +174,7 @@ public async Task> GetFilteredMetadata() entityIconMap, attributeUsages, configuration); - }); + }), warnings); } private static Record MakeRecord( diff --git a/Generator/Generator.csproj b/Generator/Generator.csproj index e575853..73b95c5 100644 --- a/Generator/Generator.csproj +++ b/Generator/Generator.csproj @@ -13,6 +13,7 @@ + diff --git a/Generator/Program.cs b/Generator/Program.cs index 59b8316..255f86d 100644 --- a/Generator/Program.cs +++ b/Generator/Program.cs @@ -18,8 +18,8 @@ var logger = loggerFactory.CreateLogger(); var dataverseService = new DataverseService(configuration, logger); -var entities = (await dataverseService.GetFilteredMetadata()).ToList(); +var (entities, warnings) = await dataverseService.GetFilteredMetadata(); -var websiteBuilder = new WebsiteBuilder(configuration, entities); +var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings); websiteBuilder.AddData(); diff --git a/Generator/Queries/WebResourceQueries.cs b/Generator/Queries/WebResourceQueries.cs index 557b188..a30aa4e 100644 --- a/Generator/Queries/WebResourceQueries.cs +++ b/Generator/Queries/WebResourceQueries.cs @@ -2,7 +2,6 @@ using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; -using System.Xml.Linq; namespace Generator.Queries; @@ -45,7 +44,6 @@ public static async Task> GetWebResourcesAsync(this Ser }; var results = (await service.RetrieveMultipleAsync(query)).Entities; - var formsDependencies = await service.GetDependentForms(solutionIds); var webResources = results.Select(e => { @@ -53,7 +51,6 @@ public static async Task> GetWebResourcesAsync(this Ser var contentValue = e.GetAttributeValue("webresource.content")?.Value; var webresourceId = e.GetAttributeValue("webresource.webresourceid").Value?.ToString() ?? ""; var webresourceName = e.GetAttributeValue("webresource.name").Value?.ToString(); - var dependencies = formsDependencies.GetValueOrDefault(webresourceName, Enumerable.Empty()); if (contentValue != null) { // Content is base64 encoded, decode it @@ -74,7 +71,6 @@ public static async Task> GetWebResourcesAsync(this Ser } return new WebResource( - dependencies, webresourceId, webresourceName, content, @@ -85,72 +81,4 @@ public static async Task> GetWebResourcesAsync(this Ser return webResources; } - - /// - /// Retrieve all forms in the solutions and return the dependencies to webresources. - /// - /// - /// - /// A dictionary where the key is the weresource id and the value is a list of forms depending on this weresource. - private static async Task>> GetDependentForms(this ServiceClient service, List? solutionIds = null) - { - 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, 60) // 60 = System Form OBS not 24 = Form - } - }, - LinkEntities = - { - new LinkEntity( - "solutioncomponent", - "systemform", - "objectid", - "formid", - JoinOperator.Inner) - { - EntityAlias = "form", - Columns = new ColumnSet("formid", "name", "objecttypecode", "formxml"), - } - } - }; - - var forms = await service.RetrieveMultipleAsync(query); - - var webresources = forms.Entities.SelectMany(form => - { - var content = form.GetAttributeValue("form.formxml").Value.ToString() ?? ""; - var formid = form.GetAttributeValue("form.formid").Value.ToString(); - var formname = form.GetAttributeValue("form.name").Value.ToString(); - var entityname = form.GetAttributeValue("form.objecttypecode").Value.ToString(); - - var doc = XDocument.Parse(content, LoadOptions.PreserveWhitespace); - - var libraries = doc.Descendants("formLibraries") - .Descendants("Library") - .Select(lib => new - { - Name = lib.Attribute("name")?.Value, - Form = formid, - FormName = formname, - FormEntity = entityname - }); - - return libraries; - }); - - return webresources - .Where(x => !string.IsNullOrWhiteSpace(x.Name)) - .GroupBy(x => x.Name!) - .ToDictionary( - g => g.Key, - g => g.Select(x => new Form(x.Form, x.FormName, x.FormEntity)) - .Distinct() - ); - } } diff --git a/Generator/Services/WebResources/WebResourceAnalyzer.cs b/Generator/Services/WebResources/WebResourceAnalyzer.cs index ccc2226..30046af 100644 --- a/Generator/Services/WebResources/WebResourceAnalyzer.cs +++ b/Generator/Services/WebResources/WebResourceAnalyzer.cs @@ -1,12 +1,22 @@ using Generator.DTO; +using Microsoft.Extensions.Configuration; using Microsoft.PowerPlatform.Dataverse.Client; +using System.Linq.Dynamic.Core; using System.Text.RegularExpressions; namespace Generator.Services.WebResources; public class WebResourceAnalyzer : BaseComponentAnalyzer { - public WebResourceAnalyzer(ServiceClient service) : base(service) { } + private readonly Func webresourceNamingFunc; + public WebResourceAnalyzer(ServiceClient service, IConfiguration configuration) : base(service) + { + webresourceNamingFunc = DynamicExpressionParser.ParseLambda( + new ParsingConfig { ResolveTypesBySimpleName = true }, + false, + configuration.GetValue("WebResourceNamingFunc") ?? "name => name.Split('.').First()" + ).Compile(); + } public override ComponentType SupportedType => ComponentType.WebResource; @@ -33,14 +43,14 @@ private void AnalyzeOnChangeHandlers(WebResource webResource, Dictionary records; + private readonly IEnumerable records; + private readonly IEnumerable warnings; private readonly string OutputFolder; - public WebsiteBuilder(IConfiguration configuration, List records) + public WebsiteBuilder(IConfiguration configuration, IEnumerable records, IEnumerable warnings) { this.configuration = configuration; this.records = records; + this.warnings = warnings; // Assuming execution in bin/xxx/net8.0 OutputFolder = configuration["OutputFolder"] ?? Path.Combine(System.Reflection.Assembly.GetExecutingAssembly().Location, "../../../../../Website/generated"); @@ -23,13 +26,15 @@ public WebsiteBuilder(IConfiguration configuration, List records) internal void AddData() { var sb = new StringBuilder(); - sb.AppendLine("import { GroupType } from \"@/lib/Types\";"); + sb.AppendLine("import { GroupType, SolutionWarningType } from \"@/lib/Types\";"); sb.AppendLine(""); sb.AppendLine($"export const LastSynched: Date = new Date('{DateTimeOffset.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ}');"); var logoUrl = configuration.GetValue("Logo", defaultValue: null); var jsValue = logoUrl != null ? $"\"{logoUrl}\"" : "null"; sb.AppendLine($"export const Logo: string | null = {jsValue};"); sb.AppendLine(""); + + // ENTITIES sb.AppendLine("export let Groups: GroupType[] = ["); var groups = records.GroupBy(x => x.Group).OrderBy(x => x.Key); foreach (var group in groups) @@ -50,6 +55,16 @@ internal void AddData() sb.AppendLine("]"); + // WARNINGS + sb.AppendLine(""); + sb.AppendLine("export let SolutionWarnings: SolutionWarningType[] = ["); + foreach (var warning in warnings) + { + sb.AppendLine($" {JsonConvert.SerializeObject(warning)},"); + } + + sb.AppendLine("]"); + File.WriteAllText(Path.Combine(OutputFolder, "Data.ts"), sb.ToString()); } } diff --git a/Website/lib/Types.ts b/Website/lib/Types.ts index 2eae3e2..4beb467 100644 --- a/Website/lib/Types.ts +++ b/Website/lib/Types.ts @@ -1,3 +1,12 @@ +export type SolutionWarningType = { + Type: WarningType, + Message: string, +} + +export enum WarningType { + Attribute +} + export type GroupType = { Name: string, Entities: EntityType[] From 5daca5006f0da69cf90e842ebdda8ddde310eab3 Mon Sep 17 00:00:00 2001 From: Lucki2g Date: Sun, 14 Sep 2025 16:05:43 +0200 Subject: [PATCH 3/8] feat: statcard component and processes adjustments --- .../WebResources/WebResourceAnalyzer.cs | 5 +- .../processesview/ProcessesView.tsx | 86 ++++++++++++++----- .../components/shared/elements/StatCard.tsx | 76 ++++++++++++++++ Website/public/plugin.svg | 10 +++ Website/public/powerautomate.svg | 50 +++++++++++ Website/public/webresource.svg | 4 + 6 files changed, 206 insertions(+), 25 deletions(-) create mode 100644 Website/components/shared/elements/StatCard.tsx create mode 100644 Website/public/plugin.svg create mode 100644 Website/public/powerautomate.svg create mode 100644 Website/public/webresource.svg diff --git a/Generator/Services/WebResources/WebResourceAnalyzer.cs b/Generator/Services/WebResources/WebResourceAnalyzer.cs index 30046af..f3cf9ca 100644 --- a/Generator/Services/WebResources/WebResourceAnalyzer.cs +++ b/Generator/Services/WebResources/WebResourceAnalyzer.cs @@ -11,10 +11,11 @@ public class WebResourceAnalyzer : BaseComponentAnalyzer private readonly Func webresourceNamingFunc; public WebResourceAnalyzer(ServiceClient service, IConfiguration configuration) : base(service) { + var lambda = configuration.GetValue("WebResourceNameFunc") ?? "name.Split('.').First()"; webresourceNamingFunc = DynamicExpressionParser.ParseLambda( new ParsingConfig { ResolveTypesBySimpleName = true }, false, - configuration.GetValue("WebResourceNamingFunc") ?? "name => name.Split('.').First()" + "name => " + lambda ).Compile(); } @@ -45,7 +46,7 @@ private void AnalyzeOnChangeHandlers(WebResource webResource, Dictionary { close(); }, [setElement, close]) + const typeDistribution = useMemo(() => { + return groups.reduce((acc, group) => { + group.Entities.forEach(entity => { + entity.Attributes.forEach(attribute => { + attribute.AttributeUsages.forEach(au => { + const componentTypeName = au.ComponentType; + acc[componentTypeName] = (acc[componentTypeName] || 0) + 1; + }); + }); + }); + return acc; + }, { } as Record); + }, [groups]) + const chartData = useMemo(() => { if (!selectedAttribute) return []; @@ -129,9 +144,11 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { const getProcessChip = (componentType: ComponentType) => { switch (componentType) { case ComponentType.Plugin: - return } />; + return } />; case ComponentType.PowerAutomateFlow: - return } />; + return } />; + case ComponentType.WebResource: + return } />; } } @@ -141,15 +158,42 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { - {/* Page Title */} - - Processes - + + + + + + + + + + + + {/* Search Bar */} @@ -171,6 +215,7 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { sx={{ '& .MuiOutlinedInput-root': { backgroundColor: 'background.paper', + borderRadius: '8px', '& fieldset': { borderColor: 'divider', }, @@ -237,10 +282,10 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { } secondary={ - + {result.entity.DisplayName} • {result.group} - + {result.attribute.SchemaName} @@ -265,12 +310,6 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { )} - - - Currently only supports Plugin triggers and CDS Power Automate Actions - - - {!selectedAttribute && ( Welcome to the processes search. Please search and select an attribute to see related processes. @@ -304,6 +343,7 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { cornerRadius={3} activeOuterRadiusOffset={8} borderWidth={1} + colors={{ scheme: 'blues' }} borderColor={{ from: 'color', modifiers: [ @@ -410,7 +450,7 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { > - { : 'rgba(0, 0, 0, 0.02)' }} > - + {getProcessChip(usage.ComponentType)} diff --git a/Website/components/shared/elements/StatCard.tsx b/Website/components/shared/elements/StatCard.tsx new file mode 100644 index 0000000..6ca997d --- /dev/null +++ b/Website/components/shared/elements/StatCard.tsx @@ -0,0 +1,76 @@ +import React, { useState, useEffect } from 'react' +import { Box, Typography, Paper, Tooltip } from '@mui/material' +import { WarningRounded } from '@mui/icons-material' + +interface IStatCardProps { + title: string + value: number | string + highlightedWord: string + tooltipTitle: string + tooltipWarning: string + imageSrc: string + imageAlt?: string +} + +export const StatCard = ({ + title, + value, + highlightedWord, + tooltipTitle, + tooltipWarning, + imageSrc, + imageAlt = "Icon" +}: IStatCardProps) => { + const [animatedValue, setAnimatedValue] = useState(0) + const targetValue = typeof value === 'number' ? value : parseInt(value.toString()) || 0 + + useEffect(() => { + if (targetValue === 0) { + setAnimatedValue(0) + return + } + + const duration = 1000 + const steps = 60 + const increment = targetValue / steps + const stepDuration = duration / steps + + let currentStep = 0 + const timer = setInterval(() => { + currentStep++ + const newValue = Math.min(Math.round(increment * currentStep), targetValue) + setAnimatedValue(newValue) + + if (currentStep >= steps || newValue >= targetValue) { + setAnimatedValue(targetValue) + clearInterval(timer) + } + }, stepDuration) + + return () => clearInterval(timer) + }, [targetValue]) + + return ( + + + + Total {highlightedWord} {title} + + + {animatedValue} + + + + + + See {tooltipWarning} + + + + + + + + + ) +} \ No newline at end of file diff --git a/Website/public/plugin.svg b/Website/public/plugin.svg new file mode 100644 index 0000000..c023aa9 --- /dev/null +++ b/Website/public/plugin.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Website/public/powerautomate.svg b/Website/public/powerautomate.svg new file mode 100644 index 0000000..ad5cdb0 --- /dev/null +++ b/Website/public/powerautomate.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Website/public/webresource.svg b/Website/public/webresource.svg new file mode 100644 index 0000000..9650ca7 --- /dev/null +++ b/Website/public/webresource.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 6dae72f6b27a07fe7c6cab054dbf0ed3d259ee53 Mon Sep 17 00:00:00 2001 From: Lucki2g Date: Sun, 14 Sep 2025 16:20:33 +0200 Subject: [PATCH 4/8] chore: attribute warnings on the processes page --- .../datamodelview/dataLoaderWorker.js | 4 +- .../processesview/ProcessesView.tsx | 741 +++++++++--------- .../components/shared/elements/StatCard.tsx | 2 +- Website/contexts/DatamodelDataContext.tsx | 9 +- 4 files changed, 388 insertions(+), 368 deletions(-) diff --git a/Website/components/datamodelview/dataLoaderWorker.js b/Website/components/datamodelview/dataLoaderWorker.js index df2f774..0fb6036 100644 --- a/Website/components/datamodelview/dataLoaderWorker.js +++ b/Website/components/datamodelview/dataLoaderWorker.js @@ -1,5 +1,5 @@ -import { Groups } from '../../generated/Data'; +import { Groups, SolutionWarnings } from '../../generated/Data'; self.onmessage = function() { - self.postMessage(Groups); + self.postMessage({ groups: Groups, warnings: SolutionWarnings }); }; \ No newline at end of file diff --git a/Website/components/processesview/ProcessesView.tsx b/Website/components/processesview/ProcessesView.tsx index 508b693..2357d74 100644 --- a/Website/components/processesview/ProcessesView.tsx +++ b/Website/components/processesview/ProcessesView.tsx @@ -3,9 +3,9 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react' import { useSidebar } from '@/contexts/SidebarContext' import { useDatamodelData } from '@/contexts/DatamodelDataContext' -import { Box, Typography, Paper, TextField, InputAdornment, Grid, List, ListItem, ListItemButton, ListItemText, Chip, IconButton, Table, TableHead, TableBody, TableRow, TableCell, useTheme, Alert, Tooltip } from '@mui/material' +import { Box, Typography, Paper, TextField, InputAdornment, Grid, List, ListItem, ListItemButton, ListItemText, Chip, IconButton, Table, TableHead, TableBody, TableRow, TableCell, useTheme, Alert, Tooltip, Divider } from '@mui/material' import { AccountTreeRounded, AddAlertRounded, CloseRounded, ExtensionRounded, JavascriptRounded, SearchRounded, WarningRounded } from '@mui/icons-material' -import { AttributeType, EntityType, ComponentType, OperationType } from '@/lib/Types' +import { AttributeType, EntityType, ComponentType, OperationType, WarningType } from '@/lib/Types' import LoadingOverlay from '@/components/shared/LoadingOverlay' import NotchedBox from '../shared/elements/NotchedBox' import { StatCard } from '../shared/elements/StatCard' @@ -21,7 +21,7 @@ interface AttributeSearchResult { export const ProcessesView = ({ }: IProcessesViewProps) => { const { setElement, close } = useSidebar() - const { groups } = useDatamodelData() + const { groups, warnings } = useDatamodelData() const theme = useTheme() const [searchTerm, setSearchTerm] = useState('') const [isSearching, setIsSearching] = useState(false) @@ -154,392 +154,407 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { return ( <> - - - - + + + + - - - + + + + + + + + + + - - - - - - - - {/* Search Bar */} - - setSearchTerm(e.target.value)} - slotProps={{ - input: { - startAdornment: ( - - - - ), - } - }} - sx={{ - '& .MuiOutlinedInput-root': { - backgroundColor: 'background.paper', - borderRadius: '8px', - '& fieldset': { - borderColor: 'divider', - }, - '&:hover fieldset': { - borderColor: 'primary.main', + {/* Search Bar */} + + setSearchTerm(e.target.value)} + slotProps={{ + input: { + startAdornment: ( + + + + ), + } + }} + sx={{ + '& .MuiOutlinedInput-root': { + backgroundColor: 'background.paper', + borderRadius: '8px', + '& fieldset': { + borderColor: 'divider', + }, + '&:hover fieldset': { + borderColor: 'primary.main', + }, + '&.Mui-focused fieldset': { + borderColor: 'primary.main', + }, }, - '&.Mui-focused fieldset': { - borderColor: 'primary.main', + '& .MuiInputBase-input': { + fontSize: '1.1rem', + padding: '14px 16px', }, - }, - '& .MuiInputBase-input': { - fontSize: '1.1rem', - padding: '14px 16px', - }, - }} - /> - - - {/* Search Results */} - {searchTerm.trim() && searchTerm.length >= 2 && !isSearching && ( - - - Attribute Search Results ({searchResults.length}) - - - {searchResults.length > 0 ? ( - - - {searchResults.map((result, index) => ( - - handleAttributeSelect(result)} - selected={selectedAttribute?.attribute.SchemaName === result.attribute.SchemaName && - selectedAttribute?.entity.SchemaName === result.entity.SchemaName} - > - - - {result.attribute.DisplayName} - - - {result.attribute.IsCustomAttribute && ( + /> + + + {/* Search Results */} + {searchTerm.trim() && searchTerm.length >= 2 && !isSearching && ( + + + Attribute Search Results ({searchResults.length}) + + + {searchResults.length > 0 ? ( + + + {searchResults.map((result, index) => ( + + handleAttributeSelect(result)} + selected={selectedAttribute?.attribute.SchemaName === result.attribute.SchemaName && + selectedAttribute?.entity.SchemaName === result.entity.SchemaName} + > + + + {result.attribute.DisplayName} + - )} - - } - secondary={ - - - {result.entity.DisplayName} • {result.group} - - - {result.attribute.SchemaName} - - - } - /> - - - ))} - - - ) : ( - - No attributes found matching "{searchTerm}" - + {result.attribute.IsCustomAttribute && ( + + )} + + } + secondary={ + + + {result.entity.DisplayName} • {result.group} + + + {result.attribute.SchemaName} + + + } + /> + + + ))} + + + ) : ( + + No attributes found matching "{searchTerm}" + + )} + )} - - )} - - {searchTerm.trim() && searchTerm.length < 2 && ( - - Enter at least 2 characters to search attributes - - )} - {!selectedAttribute && ( - - Welcome to the processes search. Please search and select an attribute to see related processes. + {searchTerm.trim() && searchTerm.length < 2 && ( + + Enter at least 2 characters to search attributes - )} + )} - {/* GRID WITH SELECTED ATTRIBUTE */} - {selectedAttribute && ( - - - setSelectedAttribute(null)}>} - className='flex flex-col items-center justify-center h-full w-full' - > - - - Selected Attribute - - - [{selectedAttribute.group} ({selectedAttribute.entity.DisplayName})]: {selectedAttribute.attribute.DisplayName} - - - - {chartData.length > 0 ? ( - + Welcome to the processes search. Please search and select an attribute to see related processes. + + )} + + {/* GRID WITH SELECTED ATTRIBUTE */} + {selectedAttribute && ( + + + setSelectedAttribute(null)}>} + className='flex flex-col items-center justify-center h-full w-full' + > + + + Selected Attribute + + + [{selectedAttribute.group} ({selectedAttribute.entity.DisplayName})]: {selectedAttribute.attribute.DisplayName} + + + + {chartData.length > 0 ? ( + - ) : ( - - No usage data available - - )} - - - - - - - Processes - - - {selectedAttribute?.attribute.AttributeUsages.length === 0 ? ( - - No process usage data available for this attribute - - ) : ( - - + ) : ( + + No usage data available + + )} + + + + + + + Processes + + + {selectedAttribute?.attribute.AttributeUsages.length === 0 ? ( + + No process usage data available for this attribute + + ) : ( + - - - - Process - - - Name - - - Type - - - Usage - - - - - {selectedAttribute?.attribute.AttributeUsages.map((usage, idx) => ( - - - {getProcessChip(usage.ComponentType)} +
+ + + + Process - - {usage.Name} + + Name - - {OperationType[usage.OperationType]} + + Type - - {usage.Usage} + + Usage - ))} - -
-
- )} -
-
-
-
)} +
+ + {selectedAttribute?.attribute.AttributeUsages.map((usage, idx) => ( + + + {getProcessChip(usage.ComponentType)} + + + {usage.Name} + + + {OperationType[usage.OperationType]} + + + {usage.Usage} + + + ))} + + +
+ )} +
+ + + )} + + + + {/* Warnings */} + + Errorness attributes + + {warnings.filter(warning => warning.Type === WarningType.Attribute).map((warning, index) => ( + + {warning.Message} + + ))} + + +
- ) } \ No newline at end of file diff --git a/Website/components/shared/elements/StatCard.tsx b/Website/components/shared/elements/StatCard.tsx index 6ca997d..6a65292 100644 --- a/Website/components/shared/elements/StatCard.tsx +++ b/Website/components/shared/elements/StatCard.tsx @@ -54,7 +54,7 @@ export const StatCard = ({ - Total {highlightedWord} {title} + {highlightedWord} {title} {animatedValue} diff --git a/Website/contexts/DatamodelDataContext.tsx b/Website/contexts/DatamodelDataContext.tsx index 744788d..62203f3 100644 --- a/Website/contexts/DatamodelDataContext.tsx +++ b/Website/contexts/DatamodelDataContext.tsx @@ -1,17 +1,19 @@ 'use client' import React, { createContext, useContext, useReducer, ReactNode } from "react"; -import { GroupType } from "@/lib/Types"; +import { GroupType, SolutionWarningType } from "@/lib/Types"; import { useSearchParams } from "next/navigation"; interface DatamodelDataState { groups: GroupType[]; + warnings: SolutionWarningType[]; search: string; filtered: any[]; } const initialState: DatamodelDataState = { groups: [], + warnings: [], search: "", filtered: [] }; @@ -23,6 +25,8 @@ const datamodelDataReducer = (state: DatamodelDataState, action: any): Datamodel switch (action.type) { case "SET_GROUPS": return { ...state, groups: action.payload }; + case "SET_WARNINGS": + return { ...state, warnings: action.payload }; case "SET_SEARCH": return { ...state, search: action.payload }; case "SET_FILTERED": @@ -46,7 +50,8 @@ export const DatamodelDataProvider = ({ children }: { children: ReactNode }) => const worker = new Worker(new URL("../components/datamodelview/dataLoaderWorker.js", import.meta.url)); worker.onmessage = (e) => { - dispatch({ type: "SET_GROUPS", payload: e.data }); + dispatch({ type: "SET_GROUPS", payload: e.data.groups || [] }); + dispatch({ type: "SET_WARNINGS", payload: e.data.warnings || [] }); worker.terminate(); }; worker.postMessage({}); From 6d01f4068bf0f4f53f74e99bb5ed22ea322b3c1c Mon Sep 17 00:00:00 2001 From: Lucki2g Date: Sun, 14 Sep 2025 16:25:37 +0200 Subject: [PATCH 5/8] chore: updated the frontpage news --- Website/components/homeview/HomeView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Website/components/homeview/HomeView.tsx b/Website/components/homeview/HomeView.tsx index b31520d..459870d 100644 --- a/Website/components/homeview/HomeView.tsx +++ b/Website/components/homeview/HomeView.tsx @@ -25,9 +25,9 @@ export const HomeView = ({ }: IHomeViewProps) => { const carouselItems: CarouselItem[] = [ { image: '/processes.jpg', - title: 'Process Explorer!', - text: "Work has started on the process explorer! This will be a place to figure out what processes are touching your fields. Everything from server- to client side.", - type: '(v2.0.0) Alpha Feature', + title: 'Webresource support!', + text: "View your attributes used inside your JS webresources in the Processes Explorer. Now supports the getAttribute method with more to come soon.", + type: '(v2.0.1) Feature update', actionlabel: 'Try it out', action: () => router.push('/processes') }, From 51b85bbb177fa6a216ed2f6df736bd55d709ee43 Mon Sep 17 00:00:00 2001 From: Lucki2g Date: Sun, 14 Sep 2025 16:31:06 +0200 Subject: [PATCH 6/8] chore: ReadMe and AI-rules update --- README.md | 7 ++++--- Website/.github/instructions/copilot.instructions.md | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dbcb4b1..dfdad3b 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,6 @@ Afterwards go into the "Website"-folder from VS Code and open the terminal (of t # Settings in pipeline The pipeline expects a variable group called `DataModel`. It must have the following variables. The app user only requires the `Environment Maker` security role. -* AdoWikiName: Name of your wiki found under "Overview -> Wiki" in ADO. (will be encoded so dont worry about space) -* AdoWikiPagePath: Path to the introduction page you wish to show in DMV. (will also be encoded so dont worry about spaces) * AzureClientId: Client id for an Azure App Registration with access to the Dataverse Environment. * AzureClientSecret: Client Secret for the above. Remember to set its variable type to "Secret"! * AzureTenantId: Azure Tenant ID (where your App Regustration is placed and resource group will be placed). @@ -91,7 +89,10 @@ The pipeline expects a variable group called `DataModel`. It must have the follo * WebsiteName: Used for the url of the web app presenting the data model to the user. The full URL will be in the format "https://wa-{WebsiteName}.azurewebsites.net/" and must be globally unique. * WebsitePassword: Password used by DMV users to login to the generated site. * WebsiteSessionSecret: Key to encrypt the session token with (You can set it to whatever you like, but recommended 32 random characters). -* TableGroups: Enter a semi-colon separated list of group names and for each group a comma-separated list of table schema names within that group. Then this configuration will be used to order the tables in groups in the DMV side-menu. Example: `Org. tables: team, systemuser, businessunit; Sales: opportunity, lead` +* (Optional) TableGroups: Enter a semi-colon separated list of group names and for each group a comma-separated list of table schema names within that group. Then this configuration will be used to order the tables in groups in the DMV side-menu. Example: `Org. tables: team, systemuser, businessunit; Sales: opportunity, lead` +* (Optional) AdoWikiName: Name of your wiki found under "Overview -> Wiki" in ADO. (will be encoded so dont worry about space) +* (Optional) AdoWikiPagePath: Path to the introduction page you wish to show in DMV. (will also be encoded so dont worry about spaces) +* (Optional) WebResourceNameFunc: Function to fetch the entity logicalname from a webresource. The function must be a valid C# LINQ expression that works on the `name` input parameter. Default: `name.Split('.').First()` ## After deployment * Go to portal.azure.com diff --git a/Website/.github/instructions/copilot.instructions.md b/Website/.github/instructions/copilot.instructions.md index 6575040..00bc087 100644 --- a/Website/.github/instructions/copilot.instructions.md +++ b/Website/.github/instructions/copilot.instructions.md @@ -5,3 +5,6 @@ You are an expert in **TypeScript**, **Node.js**, **Next.js App Router**, **Reac # Design specification If you are doing any styling or creating new components, you MUST follow the design guidelines outlined in the [design instructions](.github/instructions/design/design.instructions.md). + +# New functionality +If you implement any new major functionality, you MUST create a news entry for the home page inside the `components/homeview/HomeView.tsx` file. Follow the existing format for news entries. From e955b0d0e62545f77889b63332215b0067117331 Mon Sep 17 00:00:00 2001 From: Lucki2g Date: Sun, 14 Sep 2025 16:38:33 +0200 Subject: [PATCH 7/8] chore: linting error fixes and according for erroneous attributes --- .../processesview/ProcessesView.tsx | 789 +++++++++--------- 1 file changed, 417 insertions(+), 372 deletions(-) diff --git a/Website/components/processesview/ProcessesView.tsx b/Website/components/processesview/ProcessesView.tsx index 2357d74..e84a3f5 100644 --- a/Website/components/processesview/ProcessesView.tsx +++ b/Website/components/processesview/ProcessesView.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react' import { useSidebar } from '@/contexts/SidebarContext' import { useDatamodelData } from '@/contexts/DatamodelDataContext' -import { Box, Typography, Paper, TextField, InputAdornment, Grid, List, ListItem, ListItemButton, ListItemText, Chip, IconButton, Table, TableHead, TableBody, TableRow, TableCell, useTheme, Alert, Tooltip, Divider } from '@mui/material' -import { AccountTreeRounded, AddAlertRounded, CloseRounded, ExtensionRounded, JavascriptRounded, SearchRounded, WarningRounded } from '@mui/icons-material' +import { Box, Typography, Paper, TextField, InputAdornment, Grid, List, ListItem, ListItemButton, ListItemText, Chip, IconButton, Table, TableHead, TableBody, TableRow, TableCell, useTheme, Alert, Divider, Accordion, AccordionSummary, AccordionDetails } from '@mui/material' +import { AccountTreeRounded, CloseRounded, ExtensionRounded, JavascriptRounded, SearchRounded, WarningRounded, ExpandMoreRounded } from '@mui/icons-material' import { AttributeType, EntityType, ComponentType, OperationType, WarningType } from '@/lib/Types' import LoadingOverlay from '@/components/shared/LoadingOverlay' import NotchedBox from '../shared/elements/NotchedBox' @@ -157,404 +157,449 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { - + - - - - - - - - - + {!selectedAttribute && ( + + Welcome to the Processes Explorer. Please search and select an attribute to see related processes. + + )} + + + + + + + + + + + - - {/* Search Bar */} - - setSearchTerm(e.target.value)} - slotProps={{ - input: { - startAdornment: ( - - - - ), - } - }} - sx={{ - '& .MuiOutlinedInput-root': { - backgroundColor: 'background.paper', - borderRadius: '8px', - '& fieldset': { - borderColor: 'divider', - }, - '&:hover fieldset': { - borderColor: 'primary.main', + {/* Search Bar */} + + setSearchTerm(e.target.value)} + slotProps={{ + input: { + startAdornment: ( + + + + ), + } + }} + sx={{ + '& .MuiOutlinedInput-root': { + backgroundColor: 'background.paper', + borderRadius: '8px', + '& fieldset': { + borderColor: 'divider', + }, + '&:hover fieldset': { + borderColor: 'primary.main', + }, + '&.Mui-focused fieldset': { + borderColor: 'primary.main', + }, }, - '&.Mui-focused fieldset': { - borderColor: 'primary.main', + '& .MuiInputBase-input': { + fontSize: '1.1rem', + padding: '14px 16px', }, - }, - '& .MuiInputBase-input': { - fontSize: '1.1rem', - padding: '14px 16px', - }, - }} - /> - - - {/* Search Results */} - {searchTerm.trim() && searchTerm.length >= 2 && !isSearching && ( - - - Attribute Search Results ({searchResults.length}) - - - {searchResults.length > 0 ? ( - - - {searchResults.map((result, index) => ( - - handleAttributeSelect(result)} - selected={selectedAttribute?.attribute.SchemaName === result.attribute.SchemaName && - selectedAttribute?.entity.SchemaName === result.entity.SchemaName} - > - - - {result.attribute.DisplayName} - - - {result.attribute.IsCustomAttribute && ( + /> + + + {/* Search Results */} + {searchTerm.trim() && searchTerm.length >= 2 && !isSearching && ( + + + Attribute Search Results ({searchResults.length}) + + + {searchResults.length > 0 ? ( + + + {searchResults.map((result, index) => ( + + handleAttributeSelect(result)} + selected={selectedAttribute?.attribute.SchemaName === result.attribute.SchemaName && + selectedAttribute?.entity.SchemaName === result.entity.SchemaName} + > + + + {result.attribute.DisplayName} + - )} - - } - secondary={ - - - {result.entity.DisplayName} • {result.group} - - - {result.attribute.SchemaName} - - - } - /> - - - ))} - - - ) : ( - - No attributes found matching "{searchTerm}" - + {result.attribute.IsCustomAttribute && ( + + )} + + } + secondary={ + + + {result.entity.DisplayName} • {result.group} + + + {result.attribute.SchemaName} + + + } + /> + + + ))} + + + ) : ( + + No attributes found matching "{searchTerm}" + + )} + )} - - )} - - {searchTerm.trim() && searchTerm.length < 2 && ( - - Enter at least 2 characters to search attributes - - )} - {!selectedAttribute && ( - - Welcome to the processes search. Please search and select an attribute to see related processes. + {searchTerm.trim() && searchTerm.length < 2 && ( + + Enter at least 2 characters to search attributes - )} + )} - {/* GRID WITH SELECTED ATTRIBUTE */} - {selectedAttribute && ( - - - setSelectedAttribute(null)}>} - className='flex flex-col items-center justify-center h-full w-full' - > - - - Selected Attribute - - - [{selectedAttribute.group} ({selectedAttribute.entity.DisplayName})]: {selectedAttribute.attribute.DisplayName} - - - - {chartData.length > 0 ? ( - + + setSelectedAttribute(null)}>} + className='flex flex-col items-center justify-center h-full w-full' + > + + + Selected Attribute + + + [{selectedAttribute.group} ({selectedAttribute.entity.DisplayName})]: {selectedAttribute.attribute.DisplayName} + + + + {chartData.length > 0 ? ( + - ) : ( - - No usage data available - - )} - - - - - - - Processes - - - {selectedAttribute?.attribute.AttributeUsages.length === 0 ? ( - - No process usage data available for this attribute - - ) : ( - - + ) : ( + + No usage data available + + )} + + + + + + + Processes + + + {selectedAttribute?.attribute.AttributeUsages.length === 0 ? ( + + No process usage data available for this attribute + + ) : ( + - - - - Process - - - Name - - - Type - - - Usage - - - - - {selectedAttribute?.attribute.AttributeUsages.map((usage, idx) => ( - - - {getProcessChip(usage.ComponentType)} +
+ + + + Process - - {usage.Name} + + Name - - {OperationType[usage.OperationType]} + + Type - - {usage.Usage} + + Usage - ))} - -
-
- )} -
-
-
-
)} + + + {selectedAttribute?.attribute.AttributeUsages.map((usage, idx) => ( + + + {getProcessChip(usage.ComponentType)} + + + {usage.Name} + + + {OperationType[usage.OperationType]} + + + {usage.Usage} + + + ))} + + + + )} + + +
+ )} - + - {/* Warnings */} - - Errorness attributes - - {warnings.filter(warning => warning.Type === WarningType.Attribute).map((warning, index) => ( - - {warning.Message} - - ))} - - + {/* Warnings */} + + } + aria-controls="warnings-content" + id="warnings-header" + sx={{ + backgroundColor: 'background.paper', + color: 'text.secondary', + '&:hover': { + backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)' + } + }} + > + + + + Erroneous Attributes ({warnings.filter(warning => warning.Type === WarningType.Attribute).length}) + + + + + {warnings.filter(warning => warning.Type === WarningType.Attribute).length > 0 ? ( + + {warnings.filter(warning => warning.Type === WarningType.Attribute).map((warning, index) => ( + + {warning.Message} + + ))} + + ) : ( + + No attribute errors found. + + )} + + + - ) } \ No newline at end of file From 0249963722e6813792381886b36d3191a7c205fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaare=20B=C3=B8rsting=20=28Lucki2g=29?= Date: Sun, 14 Sep 2025 16:44:42 +0200 Subject: [PATCH 8/8] Update Generator/Services/WebResources/WebResourceAnalyzer.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../WebResources/WebResourceAnalyzer.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Generator/Services/WebResources/WebResourceAnalyzer.cs b/Generator/Services/WebResources/WebResourceAnalyzer.cs index f3cf9ca..f1e4b6e 100644 --- a/Generator/Services/WebResources/WebResourceAnalyzer.cs +++ b/Generator/Services/WebResources/WebResourceAnalyzer.cs @@ -46,12 +46,29 @@ private void AnalyzeOnChangeHandlers(WebResource webResource, Dictionary