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
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
foreach (var action in actions)
await AnalyzeAction(action, flow, attributeUsages);

// Also check for dynamic content references that might reference Dataverse fields
AnalyzeDynamicContent(flowDefinition, flow, attributeUsages);
// Also check for dynamic content references that might reference Dataverse fields - problematic to do, as you can known if what is inside the query are CDS props
// AnalyzeDynamicContent(flowDefinition, flow, attributeUsages);
}

private IEnumerable<JToken> ExtractActions(JObject flowDefinition)
Expand Down Expand Up @@ -75,7 +75,7 @@
var actionType = action.SelectToken("type")?.ToString();

// Check if this is a Dataverse/CDS action
if (IsDataverseAction(actionType))

Check warning on line 78 in Generator/Services/Power Automate/PowerAutomateFlowAnalyzer.cs

View workflow job for this annotation

GitHub Actions / generator

Possible null reference argument for parameter 'actionType' in 'bool PowerAutomateFlowAnalyzer.IsDataverseAction(string actionType)'.
await AnalyzeDataverseAction(action, flow, attributeUsages);
}
catch (Exception ex) { Console.WriteLine($"Error analyzing action: {ex.Message}"); }
Expand Down Expand Up @@ -115,11 +115,11 @@
return ValidPowerAutomateConnectors.Contains(actionType ?? string.Empty);
}

private async Task AnalyzeDataverseAction(JToken action, PowerAutomateFlow flow, Dictionary<string, Dictionary<string, List<AttributeUsage>>> attributeUsages)

Check warning on line 118 in Generator/Services/Power Automate/PowerAutomateFlowAnalyzer.cs

View workflow job for this annotation

GitHub Actions / generator

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
var flowName = flow.Name ?? "Unknown Flow";

var actionName = action.Parent.Path.Split('.').Last();

Check warning on line 122 in Generator/Services/Power Automate/PowerAutomateFlowAnalyzer.cs

View workflow job for this annotation

GitHub Actions / generator

Dereference of a possibly null reference.

// Extract entity name
var entityName = ExtractEntityName(action);
Expand Down Expand Up @@ -396,7 +396,7 @@
{
"@odata.context", "@odata.etag", "@odata.id", "@odata.type",
"entityName", "entitySetName", "uri", "path", "method", "headers",
"authentication", "retryPolicy", "pagination", "timeout"
"authentication", "retryPolicy", "pagination", "timeout", "recordId"
};

if (systemFields.Contains(name)) return false;
Expand Down
62 changes: 42 additions & 20 deletions Generator/Services/WebResources/WebResourceAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ namespace Generator.Services.WebResources;

public class WebResourceAnalyzer : BaseComponentAnalyzer<WebResource>
{
private record AttributeCall(string AttributeName, string Type, OperationType Operation);

private readonly Func<string, string> webresourceNamingFunc;
public WebResourceAnalyzer(ServiceClient service, IConfiguration configuration) : base(service)
{
var lambda = configuration.GetValue<string>("WebResourceNameFunc") ?? "name.Split('.').First()";
var lambda = configuration.GetValue<string>("WebResourceNameFunc") ?? "np(name.Split('/').LastOrDefault()).Split('.').Reverse().Skip(1).FirstOrDefault()";
webresourceNamingFunc = DynamicExpressionParser.ParseLambda<string, string>(
new ParsingConfig { ResolveTypesBySimpleName = true },
false,
Expand Down Expand Up @@ -43,11 +45,14 @@ private void AnalyzeOnChangeHandlers(WebResource webResource, Dictionary<string,
{
var content = webResource.Content;

var attributeNames = ExtractGetAttributeCalls(content);
var attributeNames = new List<AttributeCall>();
ExtractGetAttributeCalls(content, attributeNames);
ExtractGetControlCalls(content, attributeNames);
// TODO get attributes used in XrmApi or XrmQuery calls

foreach (var attributeName in attributeNames)
{
string entityName = null;
string entityName;
try
{
entityName = webresourceNamingFunc(webResource.Name);
Expand All @@ -62,41 +67,58 @@ private void AnalyzeOnChangeHandlers(WebResource webResource, Dictionary<string,
Console.WriteLine($"Warning: Naming function returned an invalid value for web resource '{webResource.Name}'. Skipping attribute usage.");
continue;
}
AddAttributeUsage(attributeUsages, entityName.ToLower(), attributeName, new AttributeUsage(
AddAttributeUsage(attributeUsages, entityName.ToLower(), attributeName.AttributeName, new AttributeUsage(
webResource.Name,
$"getAttribute call",
OperationType.Read,
attributeName.Type,
attributeName.Operation,
SupportedType
));
}
}

// TODO get attributes used in XrmApi or XrmQuery calls

// TODO get attributes from getControl

private List<string> ExtractGetAttributeCalls(string code)
private void ExtractGetAttributeCalls(string code, List<AttributeCall> attributes)
{
var attributes = new List<string>();

if (string.IsNullOrEmpty(code))
return attributes;
return;

// Examples:
// formContext.getAttribute("firstname")
// Xrm.Page.getAttribute("lastname")
// formContext.getAttribute("firstname").setValue("some value")
// Xrm.Page.getAttribute("lastname").getValue()
// executionContext.getFormContext().getAttribute("email")
// context.getAttribute("phonenumber")
// this.getAttribute("address1_city")
var getAttributePattern = @"(\w+(?:\.\w+)*\.getAttribute)\([""']([^""']+)[""']\)";
var getAttributePattern = @"(?<recv>\b\w+(?:\.\w+)*\.getAttribute)\(\s*[""'](?<attr>[^""']+)[""']\s*\)(?:\s*\.\s*(?<op>getValue|setValue)\s*\((?<args>[^)]*)\))?";
var matches = Regex.Matches(code, getAttributePattern, RegexOptions.IgnoreCase);

foreach (Match match in matches)
{
var attributeName = match.Groups[2].Value;
attributes.Add(attributeName);
var attributeName = match.Groups["attr"].Value;
var operation = match.Groups["op"].Value.StartsWith("get") ? OperationType.Read : OperationType.Update;
attributes.Add(new AttributeCall(attributeName, $"getAttribute call, {operation}", operation));
}
return;
}

// getControl calls also return tabs, subgrids etc. which is a bit problematic
private void ExtractGetControlCalls(string code, List<AttributeCall> attributes)
{
if (string.IsNullOrEmpty(code))
return;

// Examples:
// formContext.getControl("firstname")
// Xrm.Page.getControl("lastname")
// executionContext.getFormContext().getControl("email")
// context.getControl("phonenumber")
// this.getControl("address1_city")
var getAttributePattern = @"(?<recv>\b\w+(?:\.\w+)*\.getControl)\(\s*[""'](?<attr>[^""']+)[""']?";
var matches = Regex.Matches(code, getAttributePattern, RegexOptions.IgnoreCase);

return attributes.Distinct().ToList();
foreach (Match match in matches)
{
var attributeName = match.Groups["attr"].Value;
attributes.Add(new AttributeCall(attributeName, "getControl call", OperationType.Read));
}
return;
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ The pipeline expects a variable group called `DataModel`. It must have the follo
* (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()`
* (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: `np(name.Split('/').LastOrDefault()).Split('.').Reverse().Skip(1).FirstOrDefault()`

## After deployment
* Go to portal.azure.com
Expand Down
14 changes: 12 additions & 2 deletions Website/components/datamodelview/entity/AttributeDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { AttributeType, CalculationMethods, ComponentType, RequiredLevel } from "@/lib/Types";
import { AddCircleOutlineRounded, CalculateRounded, ElectricBoltRounded, ErrorRounded, FunctionsRounded, LockRounded, VisibilityRounded } from "@mui/icons-material";
import { AccountTreeRounded, AddCircleOutlineRounded, CalculateRounded, ElectricBoltRounded, ErrorRounded, FunctionsRounded, JavascriptRounded, LockRounded, VisibilityRounded } from "@mui/icons-material";
import { Tooltip } from "@mui/material";

export function AttributeDetails({ attribute }: { attribute: AttributeType }) {
Expand Down Expand Up @@ -35,10 +35,20 @@ export function AttributeDetails({ attribute }: { attribute: AttributeType }) {
}

if (attribute.AttributeUsages.some(a => a.ComponentType == ComponentType.Plugin)) {
const tooltip = `Plugins ${attribute.AttributeUsages.map(au => au.Name).join(", ")}`;
const tooltip = `Plugins ${attribute.AttributeUsages.filter(au => au.ComponentType == ComponentType.Plugin).map(au => au.Name).join(", ")}`;
details.push({ icon: <ElectricBoltRounded className="h-4 w-4" />, tooltip });
}

if (attribute.AttributeUsages.some(a => a.ComponentType == ComponentType.PowerAutomateFlow)) {
const tooltip = `Power Automate Flows ${attribute.AttributeUsages.filter(au => au.ComponentType == ComponentType.PowerAutomateFlow).map(au => au.Name).join(", ")}`;
details.push({ icon: <AccountTreeRounded className="h-4 w-4" />, tooltip });
}

if (attribute.AttributeUsages.some(a => a.ComponentType == ComponentType.WebResource)) {
const tooltip = `Web Resources ${attribute.AttributeUsages.filter(au => au.ComponentType == ComponentType.WebResource).map(au => au.Name).join(", ")}`;
details.push({ icon: <JavascriptRounded className="h-4 w-4" />, tooltip });
}

return (
<div className="flex flex-row gap-1">
{details.map((detail, index) => (
Expand Down
6 changes: 3 additions & 3 deletions Website/components/processesview/ProcessesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export const ProcessesView = ({ }: IProcessesViewProps) => {
title="attribute usages"
value={typeDistribution[ComponentType.WebResource] || 0}
highlightedWord="Web Resource"
tooltipTitle="Only includes getAttribute from Web Resource."
tooltipTitle="Only includes getAttribute/getControl from Web Resource."
tooltipWarning="Limitations"
imageSrc="/webresource.svg"
imageAlt="Web Resource icon"
Expand Down Expand Up @@ -418,7 +418,7 @@ export const ProcessesView = ({ }: IProcessesViewProps) => {
</Box>
) : (
<Box
className="overflow-x-auto md:overflow-x-visible"
className="overflow-x-auto md:overflow-x-visible rounded-b-2xl"
sx={{
borderTop: 1,
borderColor: 'border.main',
Expand All @@ -443,7 +443,7 @@ export const ProcessesView = ({ }: IProcessesViewProps) => {
>
<Table
stickyHeader
className="w-full min-w-[600px] md:min-w-0"
className="w-full min-w-[600px] md:min-w-0 rounded-b-2xl"
sx={{
borderColor: 'border.main'
}}
Expand Down
Loading
Loading