Skip to content
Draft
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
23 changes: 23 additions & 0 deletions Generator/DTO/Solution.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Generator.DTO;

internal record Solution(
Guid SolutionId,
string UniqueName,
string DisplayName,
List<SolutionComponent> Components);

internal record SolutionComponent(
Guid ObjectId,
int ComponentType,
int RootComponentBehavior,
string? ComponentTypeName,
string? ComponentDisplayName);

internal record SolutionOverview(
List<Solution> Solutions,
List<ComponentOverlap> Overlaps);

internal record ComponentOverlap(
List<string> SolutionNames,
List<SolutionComponent> SharedComponents,
int ComponentCount);
132 changes: 132 additions & 0 deletions Generator/DataverseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,138 @@ await Parallel.ForEachAsync(
.ToList();
}

public async Task<DTO.SolutionOverview> GetSolutionOverview()
{
var solutionNameArg = configuration["DataverseSolutionNames"];
if (solutionNameArg == null)
{
throw new Exception("Specify one or more solutions");
}
var solutionNames = solutionNameArg.Split(",").Select(x => x.Trim()).ToList();

// Get solution details with display names
var solutionQuery = new QueryExpression("solution")
{
ColumnSet = new ColumnSet("solutionid", "uniquename", "friendlyname"),
Criteria = new FilterExpression(LogicalOperator.And)
{
Conditions =
{
new ConditionExpression("uniquename", ConditionOperator.In, solutionNames.Select(x => x.ToLower()).ToList())
}
}
};

var solutionEntities = (await client.RetrieveMultipleAsync(solutionQuery)).Entities;
var solutions = new List<DTO.Solution>();

foreach (var solutionEntity in solutionEntities)
{
var solutionId = solutionEntity.GetAttributeValue<Guid>("solutionid");
var uniqueName = solutionEntity.GetAttributeValue<string>("uniquename");
var displayName = solutionEntity.GetAttributeValue<string>("friendlyname");

// Get components for this specific solution
var componentQuery = new QueryExpression("solutioncomponent")
{
ColumnSet = new ColumnSet("objectid", "componenttype", "rootcomponentbehavior"),
Criteria = new FilterExpression(LogicalOperator.And)
{
Conditions =
{
new ConditionExpression("componenttype", ConditionOperator.In, new List<int>() { 1, 2, 20, 92 }),
new ConditionExpression("solutionid", ConditionOperator.Equal, solutionId)
}
}
};

var componentEntities = (await client.RetrieveMultipleAsync(componentQuery)).Entities;
var components = componentEntities.Select(e => new DTO.SolutionComponent(
e.GetAttributeValue<Guid>("objectid"),
e.GetAttributeValue<OptionSetValue>("componenttype").Value,
e.Contains("rootcomponentbehavior") ? e.GetAttributeValue<OptionSetValue>("rootcomponentbehavior").Value : -1,
GetComponentTypeName(e.GetAttributeValue<OptionSetValue>("componenttype").Value),
null // Component display name will be filled later if needed
)).ToList();

solutions.Add(new DTO.Solution(solutionId, uniqueName, displayName, components));
}

// Calculate overlaps
var overlaps = CalculateComponentOverlaps(solutions);

return new DTO.SolutionOverview(solutions, overlaps);
}

private static string GetComponentTypeName(int componentType)
{
return componentType switch
{
1 => "Entity",
2 => "Attribute",
20 => "Security Role",
92 => "Plugin Step",
_ => "Unknown"
};
}

private static List<DTO.ComponentOverlap> CalculateComponentOverlaps(List<DTO.Solution> solutions)
{
var overlaps = new List<DTO.ComponentOverlap>();

// Generate all possible combinations of solutions
for (int i = 1; i <= solutions.Count; i++)
{
var combinations = GetCombinations(solutions, i);
foreach (var combination in combinations)
{
var solutionNames = combination.Select(s => s.UniqueName).ToList();
var sharedComponents = GetSharedComponents(combination);

if (sharedComponents.Any())
{
overlaps.Add(new DTO.ComponentOverlap(
solutionNames,
sharedComponents,
sharedComponents.Count
));
}
}
}

return overlaps;
}

private static IEnumerable<List<T>> GetCombinations<T>(List<T> list, int length)
{
if (length == 1) return list.Select(t => new List<T> { t });

return GetCombinations(list, length - 1)
.SelectMany(t => list.Where(e => list.IndexOf(e) > list.IndexOf(t.Last())),
(t1, t2) => t1.Concat(new List<T> { t2 }).ToList());
}

private static List<DTO.SolutionComponent> GetSharedComponents(List<DTO.Solution> solutions)
{
if (!solutions.Any()) return new List<DTO.SolutionComponent>();

var firstSolutionComponents = solutions.First().Components;
var sharedComponents = new List<DTO.SolutionComponent>();

foreach (var component in firstSolutionComponents)
{
var isSharedAcrossAll = solutions.Skip(1).All(s =>
s.Components.Any(c => c.ObjectId == component.ObjectId && c.ComponentType == component.ComponentType));

if (isSharedAcrossAll)
{
sharedComponents.Add(component);
}
}

return sharedComponents;
}

private async Task<Dictionary<string, List<SecurityRole>>> GetSecurityRoles(List<Guid> rolesInSolution, Dictionary<string, SecurityPrivilegeMetadata[]> priviledges)
{
if (rolesInSolution.Count == 0) return [];
Expand Down
3 changes: 2 additions & 1 deletion Generator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@

var dataverseService = new DataverseService(configuration, logger);
var entities = (await dataverseService.GetFilteredMetadata()).ToList();
var solutionOverview = await dataverseService.GetSolutionOverview();

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

10 changes: 7 additions & 3 deletions Generator/WebsiteBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ internal class WebsiteBuilder
{
private readonly IConfiguration configuration;
private readonly List<Record> records;
private readonly DTO.SolutionOverview solutionOverview;
private readonly string OutputFolder;

public WebsiteBuilder(IConfiguration configuration, List<Record> records)
public WebsiteBuilder(IConfiguration configuration, List<Record> records, DTO.SolutionOverview solutionOverview)
{
this.configuration = configuration;
this.records = records;
this.solutionOverview = solutionOverview;

// Assuming execution in bin/xxx/net8.0
OutputFolder = configuration["OutputFolder"] ?? Path.Combine(System.Reflection.Assembly.GetExecutingAssembly().Location, "../../../../../Website/generated");
Expand All @@ -23,7 +25,7 @@ public WebsiteBuilder(IConfiguration configuration, List<Record> records)
internal void AddData()
{
var sb = new StringBuilder();
sb.AppendLine("import { GroupType } from \"@/lib/Types\";");
sb.AppendLine("import { GroupType, SolutionOverviewType } 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<string?>("Logo", defaultValue: null);
Expand All @@ -48,7 +50,9 @@ internal void AddData()
sb.AppendLine(" },");
}

sb.AppendLine("]");
sb.AppendLine("];");
sb.AppendLine("");
sb.AppendLine($"export const SolutionOverview: SolutionOverviewType = {JsonConvert.SerializeObject(solutionOverview)};");

File.WriteAllText(Path.Combine(OutputFolder, "Data.ts"), sb.ToString());
}
Expand Down
15 changes: 15 additions & 0 deletions Website/app/solution-overview/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SolutionOverviewView } from "@/components/solutionoverviewview/SolutionOverviewView";
import { TouchProvider } from "@/components/shared/ui/hybridtooltop";
import { Loading } from "@/components/shared/ui/loading";
import { TooltipProvider } from "@/components/shared/ui/tooltip";
import { Suspense } from "react";

export default function SolutionOverview() {
return <Suspense fallback={<Loading />}>
<TouchProvider>
<TooltipProvider>
<SolutionOverviewView />
</TooltipProvider>
</TouchProvider>
</Suspense>
}
10 changes: 9 additions & 1 deletion Website/components/shared/SidebarNavRail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import { useRouter, usePathname } from "next/navigation";
import { LogOut, Info, Database, PencilRuler, PlugZap, Sparkles, Home, ChartPie } from "lucide-react";
import { LogOut, Info, Database, PencilRuler, PlugZap, Sparkles, Home, ChartPie, Target } from "lucide-react";
import { Button } from "@/components/shared/ui/button";
import { useSidebarDispatch } from "@/contexts/SidebarContext";
import { Tooltip, TooltipContent } from "./ui/tooltip";
Expand All @@ -15,6 +15,14 @@ const navItems = [
disabled: false,
new: true,
},
{
label: "Solution Overview",
icon: <Target />,
href: "/solution-overview",
active: false,
disabled: false,
new: true,
},
{
label: "Insight viewer",
icon: <ChartPie />,
Expand Down
130 changes: 130 additions & 0 deletions Website/components/solutionoverviewview/ComponentDetailsPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client'

import React from 'react';
import { SolutionComponentType } from '@/lib/Types';

interface IComponentDetailsPaneProps {
solutionNames: string[];
components: SolutionComponentType[];
}

export const ComponentDetailsPane = ({ solutionNames, components }: IComponentDetailsPaneProps) => {
const groupedComponents = components.reduce((acc, component) => {
const type = component.ComponentTypeName || 'Unknown';
if (!acc[type]) {
acc[type] = [];
}
acc[type].push(component);
return acc;
}, {} as Record<string, SolutionComponentType[]>);

const getComponentTypeIcon = (componentType: string) => {
switch (componentType) {
case 'Entity':
return '🗂️';
case 'Attribute':
return '📝';
case 'Security Role':
return '🔐';
case 'Plugin Step':
return '⚙️';
default:
return '❓';
}
};

const getComponentTypeColor = (componentType: string) => {
switch (componentType) {
case 'Entity':
return 'bg-blue-100 text-blue-800 border-blue-300';
case 'Attribute':
return 'bg-green-100 text-green-800 border-green-300';
case 'Security Role':
return 'bg-purple-100 text-purple-800 border-purple-300';
case 'Plugin Step':
return 'bg-orange-100 text-orange-800 border-orange-300';
default:
return 'bg-gray-100 text-gray-800 border-gray-300';
}
};

return (
<div className="space-y-4">
{/* Header with solution names */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="font-semibold text-blue-900 mb-2">
{solutionNames.length === 1 ? 'Solution' : 'Solutions'}
</h3>
<div className="flex flex-wrap gap-2">
{solutionNames.map((name, index) => (
<span
key={index}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{name}
</span>
))}
</div>
<div className="mt-2 text-sm text-blue-700">
Total: {components.length} component{components.length !== 1 ? 's' : ''}
</div>
</div>

{/* Component types summary */}
<div className="grid grid-cols-2 gap-3">
{Object.entries(groupedComponents).map(([type, comps]) => (
<div key={type} className={`p-3 rounded-lg border ${getComponentTypeColor(type)}`}>
<div className="flex items-center gap-2 mb-1">
<span className="text-lg">{getComponentTypeIcon(type)}</span>
<span className="font-medium text-sm">{type}</span>
</div>
<div className="text-sm font-semibold">{comps.length}</div>
</div>
))}
</div>

{/* Detailed component list */}
<div className="space-y-3 max-h-96 overflow-y-auto">
{Object.entries(groupedComponents).map(([type, comps]) => (
<div key={type} className="border rounded-lg">
<div className={`px-4 py-2 rounded-t-lg ${getComponentTypeColor(type)} border-b`}>
<div className="flex items-center gap-2">
<span className="text-lg">{getComponentTypeIcon(type)}</span>
<span className="font-semibold">{type} ({comps.length})</span>
</div>
</div>
<div className="max-h-40 overflow-y-auto">
{comps.map((component, index) => (
<div
key={index}
className="px-4 py-2 border-b last:border-b-0 hover:bg-gray-50"
>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="font-medium text-sm">
{component.ComponentDisplayName || 'Unnamed Component'}
</div>
<div className="text-xs text-gray-500 font-mono">
ID: {component.ObjectId}
</div>
</div>
<div className="text-right">
<div className="text-xs text-gray-500">
Type: {component.ComponentType}
</div>
{component.RootComponentBehavior !== -1 && (
<div className="text-xs text-gray-500">
Behavior: {component.RootComponentBehavior}
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
};
Loading