diff --git a/Generator/DTO/Solution.cs b/Generator/DTO/Solution.cs new file mode 100644 index 0000000..cc1212a --- /dev/null +++ b/Generator/DTO/Solution.cs @@ -0,0 +1,23 @@ +namespace Generator.DTO; + +internal record Solution( + Guid SolutionId, + string UniqueName, + string DisplayName, + List Components); + +internal record SolutionComponent( + Guid ObjectId, + int ComponentType, + int RootComponentBehavior, + string? ComponentTypeName, + string? ComponentDisplayName); + +internal record SolutionOverview( + List Solutions, + List Overlaps); + +internal record ComponentOverlap( + List SolutionNames, + List SharedComponents, + int ComponentCount); \ No newline at end of file diff --git a/Generator/DataverseService.cs b/Generator/DataverseService.cs index ccdeca3..962ee0f 100644 --- a/Generator/DataverseService.cs +++ b/Generator/DataverseService.cs @@ -369,6 +369,138 @@ await Parallel.ForEachAsync( .ToList(); } + public async Task 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(); + + foreach (var solutionEntity in solutionEntities) + { + var solutionId = solutionEntity.GetAttributeValue("solutionid"); + var uniqueName = solutionEntity.GetAttributeValue("uniquename"); + var displayName = solutionEntity.GetAttributeValue("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() { 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("objectid"), + e.GetAttributeValue("componenttype").Value, + e.Contains("rootcomponentbehavior") ? e.GetAttributeValue("rootcomponentbehavior").Value : -1, + GetComponentTypeName(e.GetAttributeValue("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 CalculateComponentOverlaps(List solutions) + { + var overlaps = new List(); + + // 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> GetCombinations(List list, int length) + { + if (length == 1) return list.Select(t => new List { t }); + + return GetCombinations(list, length - 1) + .SelectMany(t => list.Where(e => list.IndexOf(e) > list.IndexOf(t.Last())), + (t1, t2) => t1.Concat(new List { t2 }).ToList()); + } + + private static List GetSharedComponents(List solutions) + { + if (!solutions.Any()) return new List(); + + var firstSolutionComponents = solutions.First().Components; + var sharedComponents = new List(); + + 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>> GetSecurityRoles(List rolesInSolution, Dictionary priviledges) { if (rolesInSolution.Count == 0) return []; diff --git a/Generator/Program.cs b/Generator/Program.cs index 59b8316..ed17b44 100644 --- a/Generator/Program.cs +++ b/Generator/Program.cs @@ -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(); diff --git a/Generator/WebsiteBuilder.cs b/Generator/WebsiteBuilder.cs index 6a612b5..10c128d 100644 --- a/Generator/WebsiteBuilder.cs +++ b/Generator/WebsiteBuilder.cs @@ -9,12 +9,14 @@ internal class WebsiteBuilder { private readonly IConfiguration configuration; private readonly List records; + private readonly DTO.SolutionOverview solutionOverview; private readonly string OutputFolder; - public WebsiteBuilder(IConfiguration configuration, List records) + public WebsiteBuilder(IConfiguration configuration, List 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"); @@ -23,7 +25,7 @@ public WebsiteBuilder(IConfiguration configuration, List 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("Logo", defaultValue: null); @@ -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()); } diff --git a/Website/app/solution-overview/page.tsx b/Website/app/solution-overview/page.tsx new file mode 100644 index 0000000..4a6eb98 --- /dev/null +++ b/Website/app/solution-overview/page.tsx @@ -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 }> + + + + + + +} \ No newline at end of file diff --git a/Website/components/shared/SidebarNavRail.tsx b/Website/components/shared/SidebarNavRail.tsx index c6125a2..5993bb9 100644 --- a/Website/components/shared/SidebarNavRail.tsx +++ b/Website/components/shared/SidebarNavRail.tsx @@ -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"; @@ -15,6 +15,14 @@ const navItems = [ disabled: false, new: true, }, + { + label: "Solution Overview", + icon: , + href: "/solution-overview", + active: false, + disabled: false, + new: true, + }, { label: "Insight viewer", icon: , diff --git a/Website/components/solutionoverviewview/ComponentDetailsPane.tsx b/Website/components/solutionoverviewview/ComponentDetailsPane.tsx new file mode 100644 index 0000000..723b3aa --- /dev/null +++ b/Website/components/solutionoverviewview/ComponentDetailsPane.tsx @@ -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); + + 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 ( +
+ {/* Header with solution names */} +
+

+ {solutionNames.length === 1 ? 'Solution' : 'Solutions'} +

+
+ {solutionNames.map((name, index) => ( + + {name} + + ))} +
+
+ Total: {components.length} component{components.length !== 1 ? 's' : ''} +
+
+ + {/* Component types summary */} +
+ {Object.entries(groupedComponents).map(([type, comps]) => ( +
+
+ {getComponentTypeIcon(type)} + {type} +
+
{comps.length}
+
+ ))} +
+ + {/* Detailed component list */} +
+ {Object.entries(groupedComponents).map(([type, comps]) => ( +
+
+
+ {getComponentTypeIcon(type)} + {type} ({comps.length}) +
+
+
+ {comps.map((component, index) => ( +
+
+
+
+ {component.ComponentDisplayName || 'Unnamed Component'} +
+
+ ID: {component.ObjectId} +
+
+
+
+ Type: {component.ComponentType} +
+ {component.RootComponentBehavior !== -1 && ( +
+ Behavior: {component.RootComponentBehavior} +
+ )} +
+
+
+ ))} +
+
+ ))} +
+
+ ); +}; \ No newline at end of file diff --git a/Website/components/solutionoverviewview/SolutionOverviewView.tsx b/Website/components/solutionoverviewview/SolutionOverviewView.tsx new file mode 100644 index 0000000..f25e517 --- /dev/null +++ b/Website/components/solutionoverviewview/SolutionOverviewView.tsx @@ -0,0 +1,82 @@ +'use client' + +import React, { useEffect, useState } from 'react'; +import { AppSidebar } from '../shared/AppSidebar' +import { useSidebarDispatch } from '@/contexts/SidebarContext' +import { SolutionOverview } from '@/generated/Data' +import { SolutionComponentType } from '@/lib/Types' +import { SolutionVennDiagram } from './SolutionVennDiagram' +import { ComponentDetailsPane } from './ComponentDetailsPane' +import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/shared/ui/sheet' + +interface ISolutionOverviewViewProps { } + +export const SolutionOverviewView = ({}: ISolutionOverviewViewProps) => { + const dispatch = useSidebarDispatch(); + const [selectedOverlap, setSelectedOverlap] = useState<{ + solutionNames: string[]; + components: SolutionComponentType[]; + } | null>(null); + const [isDetailsPaneOpen, setIsDetailsPaneOpen] = useState(false); + + useEffect(() => { + dispatch({ type: 'SET_ELEMENT', payload: <> }); + dispatch({ type: 'SET_SHOW_ELEMENT', payload: false }); + }, [dispatch]); + + const handleOverlapClick = (solutionNames: string[], components: SolutionComponentType[]) => { + setSelectedOverlap({ solutionNames, components }); + setIsDetailsPaneOpen(true); + }; + + return ( +
+ + +
+
+

Solution Overview

+
+ + {/* Full width Venn Diagram Section */} +
+

Solution Component Overlaps

+ {SolutionOverview && SolutionOverview.Solutions.length > 0 ? ( + + ) : ( +
+ No solution data available. Please run the generator with multiple solutions configured. +
+ )} + + {SolutionOverview && SolutionOverview.Solutions.length > 0 && ( +
+ Click on any section of the diagram to view detailed component information +
+ )} +
+ + {/* Component Details Flyout */} + + + + Component Details + + + {selectedOverlap && ( +
+ +
+ )} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/Website/components/solutionoverviewview/SolutionVennDiagram.tsx b/Website/components/solutionoverviewview/SolutionVennDiagram.tsx new file mode 100644 index 0000000..47dca20 --- /dev/null +++ b/Website/components/solutionoverviewview/SolutionVennDiagram.tsx @@ -0,0 +1,448 @@ +'use client' + +import React from 'react'; +import { SolutionOverviewType, SolutionComponentType } from '@/lib/Types'; + +interface ISolutionVennDiagramProps { + solutionOverview: SolutionOverviewType; + onOverlapClick: (solutionNames: string[], components: SolutionComponentType[]) => void; +} + +export const SolutionVennDiagram = ({ solutionOverview, onOverlapClick }: ISolutionVennDiagramProps) => { + const solutions = solutionOverview.Solutions; + const overlaps = solutionOverview.Overlaps; + + // Color palette for different sections + const colors = [ + '#3B82F6', // blue + '#EF4444', // red + '#10B981', // green + '#F59E0B', // yellow + '#8B5CF6', // purple + '#F97316', // orange + '#06B6D4', // cyan + '#EC4899', // pink + ]; + + // Helper function to get component type summary as array for vertical display + const getComponentTypeSummary = (components: SolutionComponentType[]) => { + const typeCounts = components.reduce((acc, component) => { + const type = component.ComponentTypeName || 'Unknown'; + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {} as Record); + + return Object.entries(typeCounts) + .map(([type, count]) => `${count} ${type}${count > 1 ? 's' : ''}`); + }; + + // For simplicity, we'll render based on the number of solutions + if (solutions.length === 1) { + return renderSingleSolution(); + } else if (solutions.length === 2) { + return renderTwoSolutions(); + } else if (solutions.length === 3) { + return renderThreeSolutions(); + } else { + return renderMultipleSolutions(); + } + + function renderSingleSolution() { + const solution = solutions[0]; + const overlap = overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution.UniqueName); + const typeSummary = overlap ? getComponentTypeSummary(overlap.SharedComponents) : []; + + return ( +
+ + overlap && onOverlapClick(overlap.SolutionNames, overlap.SharedComponents)} + /> + + {solution.DisplayName} + + + {overlap?.ComponentCount || 0} components + + {typeSummary.map((typeText, index) => ( + + {typeText} + + ))} + +
+ ); + } + + function renderTwoSolutions() { + const [solution1, solution2] = solutions; + const overlap1 = overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution1.UniqueName); + const overlap2 = overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution2.UniqueName); + const overlapBoth = overlaps.find(o => o.SolutionNames.length === 2); + + const typeSummary1 = overlap1 ? getComponentTypeSummary(overlap1.SharedComponents) : []; + const typeSummary2 = overlap2 ? getComponentTypeSummary(overlap2.SharedComponents) : []; + const typeSummaryBoth = overlapBoth ? getComponentTypeSummary(overlapBoth.SharedComponents) : []; + + return ( +
+ + {/* Solution 1 Circle */} + overlap1 && onOverlapClick(overlap1.SolutionNames, overlap1.SharedComponents)} + /> + + {/* Solution 2 Circle */} + overlap2 && onOverlapClick(overlap2.SolutionNames, overlap2.SharedComponents)} + /> + + {/* Overlap area - invisible clickable region */} + overlapBoth && onOverlapClick(overlapBoth.SolutionNames, overlapBoth.SharedComponents)} + /> + + {/* Solution 1 Labels */} + + {solution1.DisplayName} + + + {overlap1?.ComponentCount || 0} components + + {typeSummary1.map((typeText, index) => ( + + {typeText} + + ))} + + {/* Solution 2 Labels */} + + {solution2.DisplayName} + + + {overlap2?.ComponentCount || 0} components + + {typeSummary2.map((typeText, index) => ( + + {typeText} + + ))} + + {/* Overlap Labels */} + + {overlapBoth?.ComponentCount || 0} + + {typeSummaryBoth.map((typeText, index) => ( + + {typeText} + + ))} + +
+ ); + } + + function renderThreeSolutions() { + const [solution1, solution2, solution3] = solutions; + + return ( +
+ + {/* Solution 1 Circle (top) */} + { + const overlap = overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution1.UniqueName); + if (overlap) onOverlapClick(overlap.SolutionNames, overlap.SharedComponents); + }} + /> + + {/* Solution 2 Circle (bottom left) */} + { + const overlap = overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution2.UniqueName); + if (overlap) onOverlapClick(overlap.SolutionNames, overlap.SharedComponents); + }} + /> + + {/* Solution 3 Circle (bottom right) */} + { + const overlap = overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution3.UniqueName); + if (overlap) onOverlapClick(overlap.SolutionNames, overlap.SharedComponents); + }} + /> + + {/* Clickable regions for overlaps */} + {overlaps.map((overlap, index) => { + if (overlap.SolutionNames.length > 1) { + const centerX = overlap.SolutionNames.length === 2 ? + (overlap.SolutionNames.includes(solution1.UniqueName) && overlap.SolutionNames.includes(solution2.UniqueName) ? 215 : + overlap.SolutionNames.includes(solution1.UniqueName) && overlap.SolutionNames.includes(solution3.UniqueName) ? 285 : 250) : + 250; + const centerY = overlap.SolutionNames.length === 2 ? + (overlap.SolutionNames.includes(solution2.UniqueName) && overlap.SolutionNames.includes(solution3.UniqueName) ? 250 : 190) : + 200; + + return ( + onOverlapClick(overlap.SolutionNames, overlap.SharedComponents)} + /> + ); + } + return null; + })} + + {/* Labels */} + + {solution1.DisplayName} + + + {overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution1.UniqueName)?.ComponentCount || 0} components + + {(() => { + const overlap = overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution1.UniqueName); + const typeSummary = overlap ? getComponentTypeSummary(overlap.SharedComponents) : []; + return typeSummary.map((typeText, index) => ( + + {typeText} + + )); + })()} + + + {solution2.DisplayName} + + + {overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution2.UniqueName)?.ComponentCount || 0} components + + {(() => { + const overlap = overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution2.UniqueName); + const typeSummary = overlap ? getComponentTypeSummary(overlap.SharedComponents) : []; + return typeSummary.map((typeText, index) => ( + + {typeText} + + )); + })()} + + + {solution3.DisplayName} + + + {overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution3.UniqueName)?.ComponentCount || 0} components + + {(() => { + const overlap = overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution3.UniqueName); + const typeSummary = overlap ? getComponentTypeSummary(overlap.SharedComponents) : []; + return typeSummary.map((typeText, index) => ( + + {typeText} + + )); + })()} + + {/* Overlap count labels */} + {overlaps.map((overlap, index) => { + if (overlap.SolutionNames.length > 1) { + const centerX = overlap.SolutionNames.length === 2 ? + (overlap.SolutionNames.includes(solution1.UniqueName) && overlap.SolutionNames.includes(solution2.UniqueName) ? 215 : + overlap.SolutionNames.includes(solution1.UniqueName) && overlap.SolutionNames.includes(solution3.UniqueName) ? 285 : 250) : + 250; + const centerY = overlap.SolutionNames.length === 2 ? + (overlap.SolutionNames.includes(solution2.UniqueName) && overlap.SolutionNames.includes(solution3.UniqueName) ? 250 : 190) : + 200; + + const typeSummary = getComponentTypeSummary(overlap.SharedComponents); + + return ( + + + {overlap.ComponentCount} + + {typeSummary.map((typeText, typeIndex) => ( + + {typeText} + + ))} + + ); + } + return null; + })} + +
+ ); + } + + function renderMultipleSolutions() { + return ( +
+
+

Multiple Solutions Overview

+
+ {solutions.map((solution, index) => { + const overlap = overlaps.find(o => o.SolutionNames.length === 1 && o.SolutionNames[0] === solution.UniqueName); + const typeSummary = overlap ? getComponentTypeSummary(overlap.SharedComponents) : []; + return ( +
overlap && onOverlapClick(overlap.SolutionNames, overlap.SharedComponents)} + > +
{solution.DisplayName}
+
{overlap?.ComponentCount || 0} components
+ {typeSummary.map((typeText, typeIndex) => ( +
{typeText}
+ ))} +
+ ); + })} +
+ + {overlaps.filter(o => o.SolutionNames.length > 1).length > 0 && ( +
+

Shared Components

+
+ {overlaps.filter(o => o.SolutionNames.length > 1).map((overlap, index) => { + const typeSummary = getComponentTypeSummary(overlap.SharedComponents); + return ( +
onOverlapClick(overlap.SolutionNames, overlap.SharedComponents)} + > +
{overlap.SolutionNames.join(' + ')}
+
{overlap.ComponentCount} components
+ {typeSummary.map((typeText, typeIndex) => ( +
{typeText}
+ ))} +
+ ); + })} +
+
+ )} +
+
+ ); + } + + return null; +}; \ No newline at end of file diff --git a/Website/lib/Types.ts b/Website/lib/Types.ts index 2b27869..82975dc 100644 --- a/Website/lib/Types.ts +++ b/Website/lib/Types.ts @@ -199,4 +199,30 @@ export type Key = { Name: string, LogicalName: string, KeyAttributes: string[] +} + +export type SolutionType = { + SolutionId: string, + UniqueName: string, + DisplayName: string, + Components: SolutionComponentType[] +} + +export type SolutionComponentType = { + ObjectId: string, + ComponentType: number, + RootComponentBehavior: number, + ComponentTypeName: string | null, + ComponentDisplayName: string | null +} + +export type ComponentOverlapType = { + SolutionNames: string[], + SharedComponents: SolutionComponentType[], + ComponentCount: number +} + +export type SolutionOverviewType = { + Solutions: SolutionType[], + Overlaps: ComponentOverlapType[] } \ No newline at end of file diff --git a/Website/stubs/Data.ts b/Website/stubs/Data.ts index 23226f5..a522bbf 100644 --- a/Website/stubs/Data.ts +++ b/Website/stubs/Data.ts @@ -1,7 +1,7 @@ /// Used in github workflow to generate stubs for data /// This file is a stub and should not be modified directly. -import { GroupType } from "@/lib/Types"; +import { GroupType, SolutionOverviewType } from "@/lib/Types"; export const LastSynched: Date = new Date(); export const Logo: string | null = null; @@ -83,4 +83,366 @@ export let Groups: GroupType[] = [ } ] } -]; \ No newline at end of file +]; + +export const SolutionOverview: SolutionOverviewType = { + "Solutions": [ + { + "SolutionId": "11111111-1111-1111-1111-111111111111", + "UniqueName": "SampleSolution1", + "DisplayName": "Core Platform", + "Components": [ + { + "ObjectId": "22222222-2222-2222-2222-222222222222", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Account" + }, + { + "ObjectId": "33333333-3333-3333-3333-333333333333", + "ComponentType": 2, + "RootComponentBehavior": 0, + "ComponentTypeName": "Attribute", + "ComponentDisplayName": "Account Name" + }, + { + "ObjectId": "44444444-4444-4444-4444-444444444444", + "ComponentType": 20, + "RootComponentBehavior": 0, + "ComponentTypeName": "Security Role", + "ComponentDisplayName": "Sales Manager" + }, + { + "ObjectId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Opportunity" + }, + { + "ObjectId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "ComponentType": 2, + "RootComponentBehavior": 0, + "ComponentTypeName": "Attribute", + "ComponentDisplayName": "Opportunity Name" + } + ] + }, + { + "SolutionId": "55555555-5555-5555-5555-555555555555", + "UniqueName": "SampleSolution2", + "DisplayName": "Sales Module", + "Components": [ + { + "ObjectId": "22222222-2222-2222-2222-222222222222", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Account" + }, + { + "ObjectId": "66666666-6666-6666-6666-666666666666", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Contact" + }, + { + "ObjectId": "77777777-7777-7777-7777-777777777777", + "ComponentType": 92, + "RootComponentBehavior": 0, + "ComponentTypeName": "Plugin Step", + "ComponentDisplayName": "Validate Contact" + }, + { + "ObjectId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Opportunity" + }, + { + "ObjectId": "cccccccc-cccc-cccc-cccc-cccccccccccc", + "ComponentType": 20, + "RootComponentBehavior": 0, + "ComponentTypeName": "Security Role", + "ComponentDisplayName": "Sales Rep" + }, + { + "ObjectId": "dddddddd-dddd-dddd-dddd-dddddddddddd", + "ComponentType": 92, + "RootComponentBehavior": 0, + "ComponentTypeName": "Plugin Step", + "ComponentDisplayName": "Calculate Sales Commission" + } + ] + }, + { + "SolutionId": "88888888-8888-8888-8888-888888888888", + "UniqueName": "SampleSolution3", + "DisplayName": "Customer Service", + "Components": [ + { + "ObjectId": "22222222-2222-2222-2222-222222222222", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Account" + }, + { + "ObjectId": "66666666-6666-6666-6666-666666666666", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Contact" + }, + { + "ObjectId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Opportunity" + }, + { + "ObjectId": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Case" + }, + { + "ObjectId": "ffffffff-ffff-ffff-ffff-ffffffffffff", + "ComponentType": 20, + "RootComponentBehavior": 0, + "ComponentTypeName": "Security Role", + "ComponentDisplayName": "Support Agent" + } + ] + }, + { + "SolutionId": "99999999-9999-9999-9999-999999999999", + "UniqueName": "SampleSolution4", + "DisplayName": "Reporting Module", + "Components": [ + { + "ObjectId": "66666666-6666-6666-6666-666666666666", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Contact" + }, + { + "ObjectId": "10101010-1010-1010-1010-101010101010", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Report" + }, + { + "ObjectId": "11111110-1111-1111-1111-111111111111", + "ComponentType": 2, + "RootComponentBehavior": 0, + "ComponentTypeName": "Attribute", + "ComponentDisplayName": "Report Title" + } + ] + } + ], + "Overlaps": [ + { + "SolutionNames": ["SampleSolution1"], + "SharedComponents": [ + { + "ObjectId": "33333333-3333-3333-3333-333333333333", + "ComponentType": 2, + "RootComponentBehavior": 0, + "ComponentTypeName": "Attribute", + "ComponentDisplayName": "Account Name" + }, + { + "ObjectId": "44444444-4444-4444-4444-444444444444", + "ComponentType": 20, + "RootComponentBehavior": 0, + "ComponentTypeName": "Security Role", + "ComponentDisplayName": "Sales Manager" + }, + { + "ObjectId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "ComponentType": 2, + "RootComponentBehavior": 0, + "ComponentTypeName": "Attribute", + "ComponentDisplayName": "Opportunity Name" + } + ], + "ComponentCount": 3 + }, + { + "SolutionNames": ["SampleSolution2"], + "SharedComponents": [ + { + "ObjectId": "77777777-7777-7777-7777-777777777777", + "ComponentType": 92, + "RootComponentBehavior": 0, + "ComponentTypeName": "Plugin Step", + "ComponentDisplayName": "Validate Contact" + }, + { + "ObjectId": "cccccccc-cccc-cccc-cccc-cccccccccccc", + "ComponentType": 20, + "RootComponentBehavior": 0, + "ComponentTypeName": "Security Role", + "ComponentDisplayName": "Sales Rep" + }, + { + "ObjectId": "dddddddd-dddd-dddd-dddd-dddddddddddd", + "ComponentType": 92, + "RootComponentBehavior": 0, + "ComponentTypeName": "Plugin Step", + "ComponentDisplayName": "Calculate Sales Commission" + } + ], + "ComponentCount": 3 + }, + { + "SolutionNames": ["SampleSolution3"], + "SharedComponents": [ + { + "ObjectId": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Case" + }, + { + "ObjectId": "ffffffff-ffff-ffff-ffff-ffffffffffff", + "ComponentType": 20, + "RootComponentBehavior": 0, + "ComponentTypeName": "Security Role", + "ComponentDisplayName": "Support Agent" + } + ], + "ComponentCount": 2 + }, + { + "SolutionNames": ["SampleSolution4"], + "SharedComponents": [ + { + "ObjectId": "10101010-1010-1010-1010-101010101010", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Report" + }, + { + "ObjectId": "11111110-1111-1111-1111-111111111111", + "ComponentType": 2, + "RootComponentBehavior": 0, + "ComponentTypeName": "Attribute", + "ComponentDisplayName": "Report Title" + } + ], + "ComponentCount": 2 + }, + { + "SolutionNames": ["SampleSolution1", "SampleSolution2"], + "SharedComponents": [ + { + "ObjectId": "22222222-2222-2222-2222-222222222222", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Account" + }, + { + "ObjectId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Opportunity" + } + ], + "ComponentCount": 2 + }, + { + "SolutionNames": ["SampleSolution1", "SampleSolution3"], + "SharedComponents": [ + { + "ObjectId": "22222222-2222-2222-2222-222222222222", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Account" + }, + { + "ObjectId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Opportunity" + } + ], + "ComponentCount": 2 + }, + { + "SolutionNames": ["SampleSolution2", "SampleSolution3"], + "SharedComponents": [ + { + "ObjectId": "22222222-2222-2222-2222-222222222222", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Account" + }, + { + "ObjectId": "66666666-6666-6666-6666-666666666666", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Contact" + }, + { + "ObjectId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Opportunity" + } + ], + "ComponentCount": 3 + }, + { + "SolutionNames": ["SampleSolution2", "SampleSolution4"], + "SharedComponents": [ + { + "ObjectId": "66666666-6666-6666-6666-666666666666", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Contact" + } + ], + "ComponentCount": 1 + }, + { + "SolutionNames": ["SampleSolution1", "SampleSolution2", "SampleSolution3"], + "SharedComponents": [ + { + "ObjectId": "22222222-2222-2222-2222-222222222222", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Account" + }, + { + "ObjectId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "ComponentType": 1, + "RootComponentBehavior": 0, + "ComponentTypeName": "Entity", + "ComponentDisplayName": "Opportunity" + } + ], + "ComponentCount": 2 + } + ] +}; \ No newline at end of file