From b4fa4b61c4344c794ae24dafeefaf804c835b8ac Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:15:30 +0100 Subject: [PATCH 01/14] Bug on setup: EntityIconService. ConditionOperator.In on empty list not allowed in dataverse --- Generator/Services/EntityIconService.cs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Generator/Services/EntityIconService.cs b/Generator/Services/EntityIconService.cs index c5c04da..853c59a 100644 --- a/Generator/Services/EntityIconService.cs +++ b/Generator/Services/EntityIconService.cs @@ -27,20 +27,25 @@ public async Task> GetEntityIconMap(IEnumerable x.IconVectorName != null) .ToDictionary(x => x.LogicalName, x => x.IconVectorName); - var query = new QueryExpression("webresource") + var iconNameToSvg = new Dictionary(); + + if (logicalNameToIconName.Count > 0) { - ColumnSet = new ColumnSet("content", "name"), - Criteria = new FilterExpression(LogicalOperator.And) + var query = new QueryExpression("webresource") { - Conditions = + ColumnSet = new ColumnSet("content", "name"), + Criteria = new FilterExpression(LogicalOperator.And) { - new ConditionExpression("name", ConditionOperator.In, logicalNameToIconName.Values.ToList()) + Conditions = + { + new ConditionExpression("name", ConditionOperator.In, logicalNameToIconName.Values.ToList()) + } } - } - }; + }; - var webresources = await client.RetrieveMultipleAsync(query); - var iconNameToSvg = webresources.Entities.ToDictionary(x => x.GetAttributeValue("name"), x => x.GetAttributeValue("content")); + var webresources = await client.RetrieveMultipleAsync(query); + iconNameToSvg = webresources.Entities.ToDictionary(x => x.GetAttributeValue("name"), x => x.GetAttributeValue("content")); + } var logicalNameToSvg = logicalNameToIconName From 2b2919ad63f80298873828ccf62b48128451d5e5 Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:51:26 +0100 Subject: [PATCH 02/14] tasks folder with various ideas or to dos for extending data model viewer. --- Tasks/Todo/PerformanceImprovements.md | 1 + Tasks/Todo/SolutionRelationsFullOverview.md | 60 +++++++++++++++++++ .../Todo/SolutionRelationsTypesSharedHover.md | 2 + Tasks/Todo/SolutionSummaryTreeView.md | 4 ++ Tasks/Todo/TypesPerSolutionOverview.md | 3 + Tasks/overview.md | 9 +++ 6 files changed, 79 insertions(+) create mode 100644 Tasks/Todo/PerformanceImprovements.md create mode 100644 Tasks/Todo/SolutionRelationsFullOverview.md create mode 100644 Tasks/Todo/SolutionRelationsTypesSharedHover.md create mode 100644 Tasks/Todo/SolutionSummaryTreeView.md create mode 100644 Tasks/Todo/TypesPerSolutionOverview.md create mode 100644 Tasks/overview.md diff --git a/Tasks/Todo/PerformanceImprovements.md b/Tasks/Todo/PerformanceImprovements.md new file mode 100644 index 0000000..74eaf0a --- /dev/null +++ b/Tasks/Todo/PerformanceImprovements.md @@ -0,0 +1 @@ +NPM install takes forever, generally slow on compile and other areas. \ No newline at end of file diff --git a/Tasks/Todo/SolutionRelationsFullOverview.md b/Tasks/Todo/SolutionRelationsFullOverview.md new file mode 100644 index 0000000..c8511e6 --- /dev/null +++ b/Tasks/Todo/SolutionRelationsFullOverview.md @@ -0,0 +1,60 @@ +Extend the SolutionsInsights in insights Section. Add the capability to see all the related or enabled entity types from all the enabled solutions and make it toggleable which component types to see. + +Tasks: +- analyze requirements and needs to solve problem +- Implement in a clean and effecient manner + +Known Elements: +- New handling of types based on list in bottom of this file. +- checkboxes for which different component types to compare + - all possible types +- Extension of generator and website to handle the new and added types + +Code snippet from other program to reference various component types. + +``` +private static readonly Dictionary ComponentTypeNames = new() + { + { 1, "Entity" }, + { 2, "Attribute" }, + { 9, "OptionSet" }, + { 10, "Relationship" }, + { 14, "Entity Key" }, + { 20, "Security Role" }, + { 26, "SavedQuery (View)" }, + { 29, "Workflow" }, + { 50, "Ribbon Customization" }, + { 59, "Saved Query Visualization" }, + { 60, "SystemForm (Form)" }, + { 61, "Web Resource" }, + { 62, "SiteMap" }, + { 63, "Connection Role" }, + { 65, "Hierarchy Rule" }, + { 66, "Custom Control" }, + { 70, "Field Security Profile" }, + { 80, "Model-driven App" }, + { 91, "Plugin Assembly" }, + { 92, "SDK Message Processing Step" }, + { 300, "Canvas App" }, + { 372, "Connection Reference" }, + { 380, "Environment Variable Definition" }, + { 381, "Environment Variable Value" }, + { 418, "Dataflow" }, + { 3233, "Connection Role Object Type Code" }, + { 10019, "Requirement Resource Preference" }, + { 10020, "Requirement Status" }, + { 10025, "Scheduling Parameter" }, + { 10240, "Custom API" }, + { 10241, "Custom API Request Parameter" }, + { 10242, "Custom API Response Property" }, + { 10639, "Plugin Package" }, + { 10563, "Organization Setting" }, + { 10645, "App Action" }, + { 10948, "App Action Rule" }, + { 11492, "Fx Expression" }, + { 11723, "DV File Search" }, + { 11724, "DV File Search Attribute" }, + { 11725, "DV File Search Entity" }, + { 12075, "AI Skill Config" } + }; +``` \ No newline at end of file diff --git a/Tasks/Todo/SolutionRelationsTypesSharedHover.md b/Tasks/Todo/SolutionRelationsTypesSharedHover.md new file mode 100644 index 0000000..6dd5416 --- /dev/null +++ b/Tasks/Todo/SolutionRelationsTypesSharedHover.md @@ -0,0 +1,2 @@ +- 5412: *mouse over - solution relations datamatrix + - types shared in dialog box \ No newline at end of file diff --git a/Tasks/Todo/SolutionSummaryTreeView.md b/Tasks/Todo/SolutionSummaryTreeView.md new file mode 100644 index 0000000..ebf9ae8 --- /dev/null +++ b/Tasks/Todo/SolutionSummaryTreeView.md @@ -0,0 +1,4 @@ +Tasks: +- 5823: Solution summary -> treeview of all component types "enabled" + Attributes + |- a, b, c \ No newline at end of file diff --git a/Tasks/Todo/TypesPerSolutionOverview.md b/Tasks/Todo/TypesPerSolutionOverview.md new file mode 100644 index 0000000..ef83b20 --- /dev/null +++ b/Tasks/Todo/TypesPerSolutionOverview.md @@ -0,0 +1,3 @@ +- 5231: Types To Solutions Overview + - Must answer the question "Which solutions have plugin assemblies, and which plugins are in each solution" + - each type -> what solutions they are in -> the specific names. \ No newline at end of file diff --git a/Tasks/overview.md b/Tasks/overview.md new file mode 100644 index 0000000..139a8a7 --- /dev/null +++ b/Tasks/overview.md @@ -0,0 +1,9 @@ +*gør hvad der er lækkert* + +Prioritized: +[FullOverView](Todo\SolutionRelationsFullOverview.md) + +Todo: +[SolutionRelationsTypesSharedHover](Todo\SolutionRelationsTypesSharedHover.md) +[SolutionSummaryTreeView](Todo\SolutionSummaryTreeView.md) +[TypesPerSolutionOverview](Todo\TypesPerSolutionOverview.md) From 4e7f5d41cdd2d07b565cb9d136eaaad4cecc8886 Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:30:50 +0100 Subject: [PATCH 03/14] Solution Insights extended components extraction plan. --- .../SolutionRelationsFullOverview-Plan.md | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 Tasks/Plans/SolutionRelationsFullOverview-Plan.md diff --git a/Tasks/Plans/SolutionRelationsFullOverview-Plan.md b/Tasks/Plans/SolutionRelationsFullOverview-Plan.md new file mode 100644 index 0000000..470ec53 --- /dev/null +++ b/Tasks/Plans/SolutionRelationsFullOverview-Plan.md @@ -0,0 +1,287 @@ +# Plan: Extended Solution Insights with Component Type Filtering + +## Summary + +Extend the Solutions Insights page (`/insights?view=solutions`) to show all Dataverse solution component types with toggleable checkbox filtering. Currently only shows Entity, Attribute, and Relationship - will expand to 40+ component types. + +## User Decisions + +- **Default Filter State**: Only Entity, Attribute, Relationship enabled on load (backwards compatible) +- **Filter Panel**: Collapsed by default (user expands to see all options) +- **Component Types**: Include ALL types from the reference list + +--- + +## Component Types to Support (Full List) + +| Type | Code | Category | Label | +|------|------|----------|-------| +| Entity | 1 | Data Model | Table | +| Attribute | 2 | Data Model | Column | +| OptionSet | 9 | Data Model | Choice | +| Relationship | 10 | Data Model | Relationship | +| EntityKey | 14 | Data Model | Key | +| SecurityRole | 20 | Security | Security Role | +| SavedQuery | 26 | User Interface | View | +| Workflow | 29 | Code | Cloud Flow | +| RibbonCustomization | 50 | User Interface | Ribbon | +| SavedQueryVisualization | 59 | User Interface | Chart | +| SystemForm | 60 | User Interface | Form | +| WebResource | 61 | Code | Web Resource | +| SiteMap | 62 | User Interface | Site Map | +| ConnectionRole | 63 | Configuration | Connection Role | +| HierarchyRule | 65 | Data Model | Hierarchy Rule | +| CustomControl | 66 | User Interface | Custom Control | +| FieldSecurityProfile | 70 | Security | Field Security | +| ModelDrivenApp | 80 | Apps | Model-driven App | +| PluginAssembly | 91 | Code | Plugin Assembly | +| SDKMessageProcessingStep | 92 | Code | Plugin Step | +| CanvasApp | 300 | Apps | Canvas App | +| ConnectionReference | 372 | Configuration | Connection Reference | +| EnvironmentVariableDefinition | 380 | Configuration | Environment Variable | +| EnvironmentVariableValue | 381 | Configuration | Environment Variable Value | +| Dataflow | 418 | Data Model | Dataflow | +| ConnectionRoleObjectTypeCode | 3233 | Configuration | Connection Role Object Type | +| CustomAPI | 10240 | Code | Custom API | +| CustomAPIRequestParameter | 10241 | Code | Custom API Request Parameter | +| CustomAPIResponseProperty | 10242 | Code | Custom API Response Property | +| PluginPackage | 10639 | Code | Plugin Package | +| OrganizationSetting | 10563 | Configuration | Organization Setting | +| AppAction | 10645 | Apps | App Action | +| AppActionRule | 10948 | Apps | App Action Rule | +| FxExpression | 11492 | Code | Fx Expression | +| DVFileSearch | 11723 | Data Model | DV File Search | +| DVFileSearchAttribute | 11724 | Data Model | DV File Search Attribute | +| DVFileSearchEntity | 11725 | Data Model | DV File Search Entity | +| AISkillConfig | 12075 | Configuration | AI Skill Config | + +**Note**: High-numbered types (10000+) are environment-specific and may not exist in all Dataverse environments. The Generator will handle missing types gracefully. + +--- + +## Architecture Decision + +**New Data Structure**: Create a separate `SolutionComponents` export in Data.ts rather than extending entity-based structure. This allows non-entity components (apps, plugins, flows) to be tracked. + +**Data Flow**: +``` +Dataverse solutioncomponent table + → Generator SolutionComponentExtractor (NEW) + → Data.ts SolutionComponents export (NEW) + → Website DatamodelDataContext + → InsightsSolutionView with filter checkboxes +``` + +### Relationship to Existing SolutionComponentService + +The **existing `SolutionComponentService`** serves a different purpose and will NOT be replaced: + +| Aspect | SolutionComponentService (KEEP) | SolutionComponentExtractor (NEW) | +|--------|--------------------------------|----------------------------------| +| **Purpose** | Determines which entities/attributes/relationships to include in extraction | Provides component list for insights visualization | +| **Used By** | DataverseService for filtering metadata extraction | WebsiteBuilder for Data.ts export | +| **Output** | ComponentInfo with solution mapping for entity metadata | SolutionComponentCollection for insights page | +| **Scope** | Only types 1, 2, 10, 20, 62 (entity-centric) | All 38+ component types | + +The two services are complementary: +- `SolutionComponentService` → "Which entities should we extract metadata for?" +- `SolutionComponentExtractor` → "What components exist in each solution for visualization?" + +--- + +## Implementation Steps + +### Phase 1: Generator Changes + +#### 1.1 Create/Update DTOs +**File:** `Generator/DTO/SolutionComponent.cs` + +- Extend `SolutionComponentType` enum with all 38+ types +- Add `SolutionComponentData` record (Name, SchemaName, ComponentType, ObjectId, IsExplicit) +- Add `SolutionComponentCollection` record (SolutionId, SolutionName, Components[]) + +#### 1.2 Create SolutionComponentExtractor Service +**File:** `Generator/Services/SolutionComponentExtractor.cs` (NEW) + +- Query solutioncomponent table for all supported component types +- Group results by solution +- Resolve display names by querying respective tables + +#### 1.3 Update WebsiteBuilder +**File:** `Generator/WebsiteBuilder.cs` + +- Accept `SolutionComponentCollection[]` parameter +- Add new export: `export let SolutionComponents: SolutionComponentCollectionType[] = [...]` + +#### 1.4 Update DataverseService +**File:** `Generator/DataverseService.cs` + +- Inject and call SolutionComponentExtractor +- Pass results to WebsiteBuilder + +--- + +### Phase 2: Website Changes + +#### 2.1 Update Types +**File:** `Website/lib/Types.ts` + +- Extend `SolutionComponentTypeEnum` with all types (matching Dataverse codes) +- Add `SolutionComponentDataType` and `SolutionComponentCollectionType` +- Add `ComponentTypeCategories` constant (groups types by category for UI) +- Add `ComponentTypeLabels` constant (human-readable names) + +#### 2.2 Update Data Loading +**Files:** +- `Website/components/datamodelview/dataLoaderWorker.ts` +- `Website/contexts/DatamodelDataContext.tsx` + +- Import and expose `SolutionComponents` from Data.ts +- Add to context state and dispatch + +#### 2.3 Update InsightsSolutionView +**File:** `Website/components/insightsview/solutions/InsightsSolutionView.tsx` + +- Add `enabledComponentTypes` state (Set of enabled types) +- Add collapsible filter panel with checkboxes grouped by category +- Add "Select All" / "Select None" buttons +- Update `solutions` useMemo to filter by enabled types +- Update summary panel to group components by type (TREE VIEW - see follow-up task) + +--- + +## Files to Modify + +| File | Change Type | +|------|-------------| +| `Generator/DTO/SolutionComponent.cs` | Extend | +| `Generator/Services/SolutionComponentExtractor.cs` | Create | +| `Generator/WebsiteBuilder.cs` | Extend | +| `Generator/DataverseService.cs` | Extend | +| `Generator/Program.cs` | Register new service | +| `Website/lib/Types.ts` | Extend | +| `Website/components/datamodelview/dataLoaderWorker.ts` | Extend | +| `Website/contexts/DatamodelDataContext.tsx` | Extend | +| `Website/components/insightsview/solutions/InsightsSolutionView.tsx` | Major update | + +--- + +## UI Design + +### Filter Panel (collapsible, above heatmap) +``` +┌─────────────────────────────────────────────────────────────┐ +│ Component Type Filters [Select All] [None] [▼] │ +│ 6 component type(s) selected │ +├─────────────────────────────────────────────────────────────┤ +│ Data Model User Interface Apps │ +│ ☑ Table ☐ Form ☐ Model-driven App │ +│ ☑ Column ☐ View ☐ Canvas App │ +│ ☑ Relationship ☐ Site Map │ +│ ☐ Choice ☐ Custom Control Code │ +│ ☐ Key ☐ Cloud Flow │ +│ Security ☐ Plugin Assembly │ +│ Configuration ☐ Security Role ☐ Plugin Step │ +│ ☐ Env Variable ☐ Field Security ☐ Web Resource │ +│ ☐ Connection Ref │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Summary Panel - Tree View Structure (Follow-up Task #5823) +``` +Shared Components: (42) + +▼ Table (5) + Account + Contact + Lead + Opportunity + Case + +▼ Column (30) + firstname + lastname + emailaddress1 + ... + +▼ Cloud Flow (7) + When Contact Created + Sync to ERP + ... +``` + +--- + +## Notes + +- **Breaking Change**: `SolutionComponentTypeEnum.Relationship` changes from 3 to 10 to match Dataverse. Only affects InsightsSolutionView (isolated change). +- **Performance**: Filter updates trigger useMemo recalculation - should remain fast with memoization. +- **Fallback**: If a component type query fails (e.g., Canvas Apps in older environments), skip gracefully with logging. +- **SolutionComponentService**: NOT replaced - serves different purpose (entity filtering vs insights visualization). + +--- + +## Implementation Phases + +### Phase 1: Generator (Do First) +1. Extend `SolutionComponentType` enum in `Generator/DTO/SolutionComponent.cs` +2. Create `SolutionComponentExtractor` service in `Generator/Services/` +3. Update `WebsiteBuilder.cs` to export new `SolutionComponents` array +4. Update `DataverseService.cs` to call extractor +5. Register service in `Program.cs` +6. **TEST**: Run Generator against test environment, verify Data.ts output + +### Phase 2: Website (Do Second) +1. Update `Website/lib/Types.ts` with new types and labels +2. Update `dataLoaderWorker.ts` to load SolutionComponents +3. Update `DatamodelDataContext.tsx` to expose solutionComponents +4. Update `InsightsSolutionView.tsx` with filter UI and logic +5. **TEST**: Run website, verify heatmap works with filtering + +--- + +## Related Follow-up Tasks + +- **Task #5823**: Solution summary → treeview of all component types "enabled" + - The summary panel will be structured as a tree view grouped by component type + - This is built into this implementation - summary panel will show components grouped by type + +--- + +## Name Resolution Tables (for Generator) + +Each component type needs its display name resolved from different Dataverse tables: + +| ComponentType | Query Table | Name Column | +|---------------|-------------|-------------| +| Entity (1) | entity (via metadata API) | LogicalName/DisplayName | +| Attribute (2) | attribute (via metadata API) | LogicalName/DisplayName | +| OptionSet (9) | optionset (via metadata API) | Name | +| Relationship (10) | relationship (via metadata API) | SchemaName | +| EntityKey (14) | entitykey (via metadata API) | LogicalName | +| SecurityRole (20) | role | name | +| SavedQuery (26) | savedquery | name | +| Workflow (29) | workflow | name | +| RibbonCustomization (50) | ribboncustomization | entity | +| SavedQueryVisualization (59) | savedqueryvisualization | name | +| SystemForm (60) | systemform | name | +| WebResource (61) | webresource | name | +| SiteMap (62) | sitemap | sitemapname | +| ConnectionRole (63) | connectionrole | name | +| HierarchyRule (65) | hierarchyrule | name | +| CustomControl (66) | customcontrol | name | +| FieldSecurityProfile (70) | fieldsecurityprofile | name | +| ModelDrivenApp (80) | appmodule | name | +| PluginAssembly (91) | pluginassembly | name | +| SDKMessageProcessingStep (92) | sdkmessageprocessingstep | name | +| CanvasApp (300) | canvasapp | name | +| ConnectionReference (372) | connectionreference | connectionreferencedisplayname | +| EnvironmentVariableDefinition (380) | environmentvariabledefinition | displayname | +| EnvironmentVariableValue (381) | environmentvariablevalue | schemaname | +| Dataflow (418) | workflow (category=6) | name | +| CustomAPI (10240) | customapi | name | +| CustomAPIRequestParameter (10241) | customapirequestparameter | name | +| CustomAPIResponseProperty (10242) | customapiresponseproperty | name | +| PluginPackage (10639) | pluginpackage | name | + +**Fallback Strategy**: If name resolution fails for a component, use ObjectId as fallback name. \ No newline at end of file From 49417e87f12fa97b69b77e31631adf9519e6eb00 Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:13:05 +0100 Subject: [PATCH 04/14] Initial extension of solution insights page. Needs further pruning and verification before final approval. --- Generator/DTO/SolutionComponent.cs | 59 ++- Generator/DataverseService.cs | 56 ++- Generator/Program.cs | 5 +- .../Services/SolutionComponentExtractor.cs | 321 +++++++++++++ Generator/WebsiteBuilder.cs | 20 +- .../datamodelview/dataLoaderWorker.ts | 10 +- .../solutions/InsightsSolutionView.tsx | 420 +++++++++++------- Website/contexts/DatamodelDataContext.tsx | 7 +- Website/lib/Types.ts | 151 ++++++- 9 files changed, 882 insertions(+), 167 deletions(-) create mode 100644 Generator/Services/SolutionComponentExtractor.cs diff --git a/Generator/DTO/SolutionComponent.cs b/Generator/DTO/SolutionComponent.cs index 1f5f279..1ef4abd 100644 --- a/Generator/DTO/SolutionComponent.cs +++ b/Generator/DTO/SolutionComponent.cs @@ -1,10 +1,49 @@ namespace Generator.DTO; +/// +/// Solution component types from Dataverse. +/// See: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/entities/solutioncomponent +/// public enum SolutionComponentType { Entity = 1, Attribute = 2, - Relationship = 3, + OptionSet = 9, + Relationship = 10, + EntityKey = 14, + SecurityRole = 20, + SavedQuery = 26, + Workflow = 29, + RibbonCustomization = 50, + SavedQueryVisualization = 59, + SystemForm = 60, + WebResource = 61, + SiteMap = 62, + ConnectionRole = 63, + HierarchyRule = 65, + CustomControl = 66, + FieldSecurityProfile = 70, + ModelDrivenApp = 80, + PluginAssembly = 91, + SDKMessageProcessingStep = 92, + CanvasApp = 300, + ConnectionReference = 372, + EnvironmentVariableDefinition = 380, + EnvironmentVariableValue = 381, + Dataflow = 418, + ConnectionRoleObjectTypeCode = 3233, + CustomAPI = 10240, + CustomAPIRequestParameter = 10241, + CustomAPIResponseProperty = 10242, + PluginPackage = 10639, + OrganizationSetting = 10563, + AppAction = 10645, + AppActionRule = 10948, + FxExpression = 11492, + DVFileSearch = 11723, + DVFileSearchAttribute = 11724, + DVFileSearchEntity = 11725, + AISkillConfig = 12075, } public record SolutionComponent( @@ -14,3 +53,21 @@ public record SolutionComponent( SolutionComponentType ComponentType, string PublisherName, string PublisherPrefix); + +/// +/// Represents a solution component with its solution membership info for the insights view. +/// +public record SolutionComponentData( + string Name, + string SchemaName, + SolutionComponentType ComponentType, + Guid ObjectId, + bool IsExplicit); + +/// +/// Collection of solution components grouped by solution. +/// +public record SolutionComponentCollection( + Guid SolutionId, + string SolutionName, + List Components); diff --git a/Generator/DataverseService.cs b/Generator/DataverseService.cs index fed21bd..4955658 100644 --- a/Generator/DataverseService.cs +++ b/Generator/DataverseService.cs @@ -24,6 +24,7 @@ internal class DataverseService private readonly EntityIconService entityIconService; private readonly RecordMappingService recordMappingService; private readonly SolutionComponentService solutionComponentService; + private readonly SolutionComponentExtractor solutionComponentExtractor; private readonly WorkflowService workflowService; private readonly RelationshipService relationshipService; @@ -38,6 +39,7 @@ public DataverseService( EntityIconService entityIconService, RecordMappingService recordMappingService, SolutionComponentService solutionComponentService, + SolutionComponentExtractor solutionComponentExtractor, WorkflowService workflowService, RelationshipService relationshipService) { @@ -49,6 +51,7 @@ public DataverseService( this.recordMappingService = recordMappingService; this.workflowService = workflowService; this.relationshipService = relationshipService; + this.solutionComponentExtractor = solutionComponentExtractor; // Register all analyzers with their query functions analyzerRegistrations = new List @@ -69,7 +72,7 @@ public DataverseService( this.solutionComponentService = solutionComponentService; } - public async Task<(IEnumerable, IEnumerable)> GetFilteredMetadata() + public async Task<(IEnumerable, IEnumerable, IEnumerable)> GetFilteredMetadata() { // used to collect warnings for the insights dashboard var warnings = new List(); @@ -275,8 +278,55 @@ public DataverseService( }) .ToList(); - logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed - returning empty results"); - return (records, warnings); + /// SOLUTION COMPONENTS FOR INSIGHTS + List solutionComponentCollections; + try + { + logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracting solution components for insights view"); + + // Build name lookups from entity metadata for the extractor + var entityNameLookup = entitiesInSolutionMetadata.ToDictionary( + e => e.MetadataId!.Value, + e => e.DisplayName.ToLabelString() ?? e.SchemaName); + + var attributeNameLookup = entitiesInSolutionMetadata + .SelectMany(e => e.Attributes.Where(a => a.MetadataId.HasValue)) + .ToDictionary( + a => a.MetadataId!.Value, + a => a.DisplayName.ToLabelString() ?? a.SchemaName); + + var relationshipNameLookup = entitiesInSolutionMetadata + .SelectMany(e => e.ManyToManyRelationships.Cast() + .Concat(e.OneToManyRelationships) + .Concat(e.ManyToOneRelationships)) + .Where(r => r.MetadataId.HasValue) + .DistinctBy(r => r.MetadataId!.Value) + .ToDictionary( + r => r.MetadataId!.Value, + r => r.SchemaName); + + // Build solution name lookup + var solutionNameLookup = solutionLookup.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.Name); + + solutionComponentCollections = await solutionComponentExtractor.ExtractSolutionComponentsAsync( + solutionIds, + solutionNameLookup, + entityNameLookup, + attributeNameLookup, + relationshipNameLookup); + + logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracted components for {solutionComponentCollections.Count} solutions"); + } + catch (Exception ex) + { + logger.LogWarning(ex, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Failed to extract solution components for insights, continuing without them"); + solutionComponentCollections = new List(); + } + + logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed"); + return (records, warnings, solutionComponentCollections); } } diff --git a/Generator/Program.cs b/Generator/Program.cs index 1672cf7..03d5cb1 100644 --- a/Generator/Program.cs +++ b/Generator/Program.cs @@ -59,15 +59,16 @@ services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); +services.AddSingleton(); // Build service provider var serviceProvider = services.BuildServiceProvider(); // Resolve and use DataverseService var dataverseService = serviceProvider.GetRequiredService(); -var (entities, warnings) = await dataverseService.GetFilteredMetadata(); +var (entities, warnings, solutionComponents) = await dataverseService.GetFilteredMetadata(); -var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings); +var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings, solutionComponents); websiteBuilder.AddData(); // Token provider function diff --git a/Generator/Services/SolutionComponentExtractor.cs b/Generator/Services/SolutionComponentExtractor.cs new file mode 100644 index 0000000..47f46f2 --- /dev/null +++ b/Generator/Services/SolutionComponentExtractor.cs @@ -0,0 +1,321 @@ +using Generator.DTO; +using Microsoft.Extensions.Logging; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace Generator.Services; + +/// +/// Extracts all solution components for the insights visualization. +/// This is separate from SolutionComponentService which filters entity metadata extraction. +/// +public class SolutionComponentExtractor +{ + private readonly ServiceClient _client; + private readonly ILogger _logger; + + /// + /// All component types we want to extract for insights. + /// + private static readonly int[] SupportedComponentTypes = new[] + { + 1, // Entity + 2, // Attribute + 9, // OptionSet + 10, // Relationship + 14, // EntityKey + 20, // SecurityRole + 26, // SavedQuery + 29, // Workflow + 50, // RibbonCustomization + 59, // SavedQueryVisualization + 60, // SystemForm + 61, // WebResource + 62, // SiteMap + 63, // ConnectionRole + 65, // HierarchyRule + 66, // CustomControl + 70, // FieldSecurityProfile + 80, // ModelDrivenApp + 91, // PluginAssembly + 92, // SDKMessageProcessingStep + 300, // CanvasApp + 372, // ConnectionReference + 380, // EnvironmentVariableDefinition + 381, // EnvironmentVariableValue + 418, // Dataflow + }; + + /// + /// Maps component type codes to their Dataverse table, name column, and primary key column for name resolution. + /// Primary key is optional - if null, defaults to tablename + "id". + /// + private static readonly Dictionary ComponentTableMap = new() + { + { 20, ("role", "name", null) }, + { 26, ("savedquery", "name", null) }, + { 29, ("workflow", "name", null) }, + { 50, ("ribboncustomization", "entity", null) }, + { 59, ("savedqueryvisualization", "name", null) }, + { 60, ("systemform", "name", "formid") }, // systemform uses formid, not systemformid + { 61, ("webresource", "name", null) }, + { 62, ("sitemap", "sitemapname", null) }, + { 63, ("connectionrole", "name", null) }, + { 65, ("hierarchyrule", "name", null) }, + { 66, ("customcontrol", "name", null) }, + { 70, ("fieldsecurityprofile", "name", null) }, + { 80, ("appmodule", "name", "appmoduleid") }, // appmodule uses appmoduleid + { 91, ("pluginassembly", "name", null) }, + { 92, ("sdkmessageprocessingstep", "name", null) }, + { 300, ("canvasapp", "name", null) }, + { 372, ("connectionreference", "connectionreferencedisplayname", null) }, + { 380, ("environmentvariabledefinition", "displayname", null) }, + { 381, ("environmentvariablevalue", "schemaname", null) }, + { 418, ("workflow", "name", null) }, // Dataflows are stored in workflow table with category=6 + }; + + public SolutionComponentExtractor(ServiceClient client, ILogger logger) + { + _client = client; + _logger = logger; + } + + /// + /// Extracts all solution components grouped by solution for the insights view. + /// + public async Task> ExtractSolutionComponentsAsync( + List solutionIds, + Dictionary solutionNameLookup, + Dictionary? entityNameLookup = null, + Dictionary? attributeNameLookup = null, + Dictionary? relationshipNameLookup = null) + { + _logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracting solution components for {solutionIds.Count} solutions"); + + if (solutionIds == null || !solutionIds.Any()) + { + _logger.LogWarning("No solution IDs provided for component extraction"); + return new List(); + } + + // Query all solution components + var rawComponents = await QuerySolutionComponentsAsync(solutionIds); + _logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Found {rawComponents.Count} raw solution components"); + + // Group by solution + var componentsBySolution = rawComponents + .GroupBy(c => c.SolutionId) + .ToDictionary(g => g.Key, g => g.ToList()); + + // Resolve names for each component type + var nameCache = await BuildNameCacheAsync(rawComponents, entityNameLookup, attributeNameLookup, relationshipNameLookup); + + // Build the result collections + var result = new List(); + foreach (var solutionId in solutionIds) + { + var solutionName = solutionNameLookup.GetValueOrDefault(solutionId, solutionId.ToString()); + + if (!componentsBySolution.TryGetValue(solutionId, out var components)) + { + result.Add(new SolutionComponentCollection(solutionId, solutionName, new List())); + continue; + } + + var componentDataList = components + .Select(c => new SolutionComponentData( + Name: ResolveComponentName(c, nameCache), + SchemaName: ResolveComponentSchemaName(c, nameCache), + ComponentType: (SolutionComponentType)c.ComponentType, + ObjectId: c.ObjectId, + IsExplicit: c.IsExplicit)) + .OrderBy(c => c.ComponentType) + .ThenBy(c => c.Name) + .ToList(); + + result.Add(new SolutionComponentCollection(solutionId, solutionName, componentDataList)); + _logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Solution '{solutionName}': {componentDataList.Count} components"); + } + + return result; + } + + private async Task> QuerySolutionComponentsAsync(List solutionIds) + { + var results = new List(); + + var query = new QueryExpression("solutioncomponent") + { + ColumnSet = new ColumnSet("objectid", "componenttype", "solutionid", "rootcomponentbehavior"), + Criteria = new FilterExpression(LogicalOperator.And) + { + Conditions = + { + new ConditionExpression("componenttype", ConditionOperator.In, SupportedComponentTypes), + new ConditionExpression("solutionid", ConditionOperator.In, solutionIds) + } + } + }; + + try + { + var response = await _client.RetrieveMultipleAsync(query); + foreach (var entity in response.Entities) + { + var componentType = entity.GetAttributeValue("componenttype")?.Value ?? 0; + var objectId = entity.GetAttributeValue("objectid"); + var solutionId = entity.GetAttributeValue("solutionid")?.Id ?? Guid.Empty; + var rootBehavior = entity.Contains("rootcomponentbehavior") + ? entity.GetAttributeValue("rootcomponentbehavior")?.Value ?? -1 + : -1; + + // RootComponentBehaviour: 0, 1, 2 = explicit, other = implicit + var isExplicit = rootBehavior >= 0 && rootBehavior <= 2; + + results.Add(new RawComponentInfo(componentType, objectId, solutionId, isExplicit)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to query solution components"); + } + + return results; + } + + private async Task> BuildNameCacheAsync( + List components, + Dictionary? entityNameLookup, + Dictionary? attributeNameLookup, + Dictionary? relationshipNameLookup) + { + var cache = new Dictionary<(int, Guid), (string Name, string SchemaName)>(); + + // Group components by type for batch queries + var componentsByType = components + .GroupBy(c => c.ComponentType) + .ToDictionary(g => g.Key, g => g.Select(c => c.ObjectId).Distinct().ToList()); + + foreach (var (componentType, objectIds) in componentsByType) + { + // Use provided lookups for metadata-based types + if (componentType == 1 && entityNameLookup != null) // Entity + { + foreach (var objectId in objectIds) + { + if (entityNameLookup.TryGetValue(objectId, out var name)) + { + cache[(componentType, objectId)] = (name, name); + } + } + continue; + } + + if (componentType == 2 && attributeNameLookup != null) // Attribute + { + foreach (var objectId in objectIds) + { + if (attributeNameLookup.TryGetValue(objectId, out var name)) + { + cache[(componentType, objectId)] = (name, name); + } + } + continue; + } + + if (componentType == 10 && relationshipNameLookup != null) // Relationship + { + foreach (var objectId in objectIds) + { + if (relationshipNameLookup.TryGetValue(objectId, out var name)) + { + cache[(componentType, objectId)] = (name, name); + } + } + continue; + } + + // Skip types that need metadata API (9=OptionSet, 14=EntityKey) - use ObjectId as fallback + if (componentType == 9 || componentType == 14) + { + foreach (var objectId in objectIds) + { + cache[(componentType, objectId)] = (objectId.ToString(), objectId.ToString()); + } + continue; + } + + // Query Dataverse tables for other types + if (ComponentTableMap.TryGetValue(componentType, out var tableInfo)) + { + var primaryKey = tableInfo.PrimaryKey ?? tableInfo.TableName + "id"; + var names = await QueryComponentNamesAsync(tableInfo.TableName, tableInfo.NameColumn, primaryKey, objectIds); + foreach (var (objectId, name) in names) + { + cache[(componentType, objectId)] = (name, name); + } + } + } + + return cache; + } + + private async Task> QueryComponentNamesAsync(string tableName, string nameColumn, string primaryKey, List objectIds) + { + var result = new Dictionary(); + + if (!objectIds.Any()) + return result; + + try + { + var query = new QueryExpression(tableName) + { + ColumnSet = new ColumnSet(primaryKey, nameColumn), + Criteria = new FilterExpression(LogicalOperator.And) + { + Conditions = + { + new ConditionExpression(primaryKey, ConditionOperator.In, objectIds) + } + } + }; + + var response = await _client.RetrieveMultipleAsync(query); + foreach (var entity in response.Entities) + { + var id = entity.GetAttributeValue(primaryKey); + var name = entity.GetAttributeValue(nameColumn) ?? id.ToString(); + result[id] = name; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"Failed to query names from {tableName}. Using ObjectId as fallback."); + // Return empty - fallback will use ObjectId + } + + return result; + } + + private string ResolveComponentName(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName)> cache) + { + if (cache.TryGetValue((component.ComponentType, component.ObjectId), out var names)) + { + return names.Name; + } + return component.ObjectId.ToString(); + } + + private string ResolveComponentSchemaName(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName)> cache) + { + if (cache.TryGetValue((component.ComponentType, component.ObjectId), out var names)) + { + return names.SchemaName; + } + return component.ObjectId.ToString(); + } + + private record RawComponentInfo(int ComponentType, Guid ObjectId, Guid SolutionId, bool IsExplicit); +} diff --git a/Generator/WebsiteBuilder.cs b/Generator/WebsiteBuilder.cs index 0661ebd..2b10158 100644 --- a/Generator/WebsiteBuilder.cs +++ b/Generator/WebsiteBuilder.cs @@ -11,14 +11,19 @@ internal class WebsiteBuilder private readonly IConfiguration configuration; private readonly IEnumerable records; private readonly IEnumerable warnings; - private readonly IEnumerable solutions; + private readonly IEnumerable solutionComponents; private readonly string OutputFolder; - public WebsiteBuilder(IConfiguration configuration, IEnumerable records, IEnumerable warnings) + public WebsiteBuilder( + IConfiguration configuration, + IEnumerable records, + IEnumerable warnings, + IEnumerable? solutionComponents = null) { this.configuration = configuration; this.records = records; this.warnings = warnings; + this.solutionComponents = solutionComponents ?? Enumerable.Empty(); // Assuming execution in bin/xxx/net8.0 OutputFolder = configuration["OutputFolder"] ?? Path.Combine(System.Reflection.Assembly.GetExecutingAssembly().Location, "../../../../../Website/generated"); @@ -27,7 +32,7 @@ public WebsiteBuilder(IConfiguration configuration, IEnumerable records, internal void AddData() { var sb = new StringBuilder(); - sb.AppendLine("import { GroupType, SolutionWarningType } from \"@/lib/Types\";"); + sb.AppendLine("import { GroupType, SolutionWarningType, SolutionComponentCollectionType } 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); @@ -66,6 +71,15 @@ internal void AddData() } sb.AppendLine("]"); + // SOLUTION COMPONENTS (for insights) + sb.AppendLine(""); + sb.AppendLine("export let SolutionComponents: SolutionComponentCollectionType[] = ["); + foreach (var collection in solutionComponents) + { + sb.AppendLine($" {JsonConvert.SerializeObject(collection)},"); + } + sb.AppendLine("]"); + File.WriteAllText(Path.Combine(OutputFolder, "Data.ts"), sb.ToString()); } } diff --git a/Website/components/datamodelview/dataLoaderWorker.ts b/Website/components/datamodelview/dataLoaderWorker.ts index b4fc209..a36fbdf 100644 --- a/Website/components/datamodelview/dataLoaderWorker.ts +++ b/Website/components/datamodelview/dataLoaderWorker.ts @@ -1,5 +1,5 @@ import { EntityType } from '@/lib/Types'; -import { Groups, SolutionWarnings, SolutionCount } from '../../generated/Data'; +import { Groups, SolutionWarnings, SolutionCount, SolutionComponents } from '../../generated/Data'; self.onmessage = function () { const entityMap = new Map(); @@ -8,5 +8,11 @@ self.onmessage = function () { entityMap.set(entity.SchemaName, entity); }); }); - self.postMessage({ groups: Groups, entityMap: entityMap, warnings: SolutionWarnings, solutionCount: SolutionCount }); + self.postMessage({ + groups: Groups, + entityMap: entityMap, + warnings: SolutionWarnings, + solutionCount: SolutionCount, + solutionComponents: SolutionComponents + }); }; \ No newline at end of file diff --git a/Website/components/insightsview/solutions/InsightsSolutionView.tsx b/Website/components/insightsview/solutions/InsightsSolutionView.tsx index f280215..0b3994f 100644 --- a/Website/components/insightsview/solutions/InsightsSolutionView.tsx +++ b/Website/components/insightsview/solutions/InsightsSolutionView.tsx @@ -1,10 +1,12 @@ import { useDatamodelData } from '@/contexts/DatamodelDataContext' -import { Paper, Typography, Box, Grid, useTheme, Tooltip, IconButton } from '@mui/material' +import { Paper, Typography, Box, Grid, useTheme, Tooltip, IconButton, Button, Collapse, FormControlLabel, Checkbox } from '@mui/material' import React, { useMemo, useState } from 'react' import { ResponsiveHeatMap } from '@nivo/heatmap' -import { SolutionComponentTypeEnum } from '@/lib/Types' +import { SolutionComponentTypeEnum, SolutionComponentDataType, ComponentTypeCategories, ComponentTypeLabels } from '@/lib/Types' import { generateEnvelopeSVG } from '@/lib/svgart' import { InfoIcon } from '@/lib/icons' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import ExpandLessIcon from '@mui/icons-material/ExpandLess' interface InsightsSolutionViewProps { @@ -20,71 +22,74 @@ interface HeatMapCell { } const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { - const { groups } = useDatamodelData(); + const { solutionComponents } = useDatamodelData(); const theme = useTheme(); - const [selectedSolution, setSelectedSolution] = useState<{ Solution1: string, Solution2: string, Components: { Name: string; SchemaName: string; ComponentType: SolutionComponentTypeEnum }[] } | undefined>(undefined); - - const solutions = useMemo(() => { - const solutionMap: Map = new Map(); - groups.forEach(group => { - group.Entities.forEach(entity => { + // Filter state - default to Entity, Attribute, Relationship for backwards compatibility + const [enabledComponentTypes, setEnabledComponentTypes] = useState>( + new Set([ + SolutionComponentTypeEnum.Entity, + SolutionComponentTypeEnum.Attribute, + SolutionComponentTypeEnum.Relationship, + ]) + ); + + const [filtersExpanded, setFiltersExpanded] = useState(false); + + const [selectedSolution, setSelectedSolution] = useState<{ + Solution1: string; + Solution2: string; + Components: SolutionComponentDataType[]; + } | undefined>(undefined); + + // Handle toggle of individual component type + const handleToggleType = (type: SolutionComponentTypeEnum, checked: boolean) => { + setEnabledComponentTypes(prev => { + const newSet = new Set(prev); + if (checked) { + newSet.add(type); + } else { + newSet.delete(type); + } + return newSet; + }); + }; - if (!entity.Solutions || entity.Solutions.length === 0) { - console.log(`Entity ${entity.DisplayName} has no solutions.`); - } + // Select all component types + const handleSelectAll = () => { + const allTypes = Object.values(ComponentTypeCategories).flat(); + setEnabledComponentTypes(new Set(allTypes)); + }; - entity.Solutions.forEach(solution => { - if (!solutionMap.has(solution.Name)) { - solutionMap.set(solution.Name, [{ Name: entity.DisplayName, SchemaName: entity.SchemaName, ComponentType: SolutionComponentTypeEnum.Entity }]); - } else { - solutionMap.get(solution.Name)!.push({ Name: entity.DisplayName, SchemaName: entity.SchemaName, ComponentType: SolutionComponentTypeEnum.Entity }); - } + // Clear all component types + const handleSelectNone = () => { + setEnabledComponentTypes(new Set()); + }; - entity.Attributes.forEach(attribute => { - if (!attribute.Solutions || attribute.Solutions.length === 0) { - console.log(`Attr ${attribute.DisplayName} has no solutions.`); - } - - attribute.Solutions.forEach(attrSolution => { - if (!solutionMap.has(attrSolution.Name)) { - solutionMap.set(attrSolution.Name, [{ Name: attribute.DisplayName, SchemaName: attribute.SchemaName, ComponentType: SolutionComponentTypeEnum.Attribute }]); - } else { - solutionMap.get(attrSolution.Name)!.push({ Name: attribute.DisplayName, SchemaName: attribute.SchemaName, ComponentType: SolutionComponentTypeEnum.Attribute }); - } - }); - }); + // Build filtered solution map from solutionComponents + const solutions = useMemo(() => { + const solutionMap: Map = new Map(); - entity.Relationships.forEach(relationship => { - if (!relationship.Solutions || relationship.Solutions.length === 0) { - console.log(`Relationship ${relationship.Name} has no solutions.`); - } - - relationship.Solutions.forEach(relSolution => { - if (!solutionMap.has(relSolution.Name)) { - solutionMap.set(relSolution.Name, [{ Name: relationship.Name, SchemaName: relationship.RelationshipSchema, ComponentType: SolutionComponentTypeEnum.Relationship }]); - } else { - solutionMap.get(relSolution.Name)!.push({ Name: relationship.Name, SchemaName: relationship.RelationshipSchema, ComponentType: SolutionComponentTypeEnum.Relationship }); - } - }); - }); - }); - }); + solutionComponents.forEach(collection => { + const filteredComponents = collection.Components.filter( + comp => enabledComponentTypes.has(comp.ComponentType) + ); + solutionMap.set(collection.SolutionName, filteredComponents); }); return solutionMap; - }, [groups]); + }, [solutionComponents, enabledComponentTypes]); const solutionMatrix = useMemo(() => { const solutionNames = Array.from(solutions.keys()); // Create a cache for symmetric calculations - const cache = new Map(); + const cache = new Map(); const matrix: Array<{ solution1: string; solution2: string; - sharedComponents: { Name: string; SchemaName: string; ComponentType: SolutionComponentTypeEnum }[]; + sharedComponents: SolutionComponentDataType[]; count: number; }> = []; @@ -113,7 +118,7 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { // Find true intersection: components that exist in BOTH solutions const sharedComponents = components1.filter(c1 => - components2.some(c2 => c2.SchemaName === c1.SchemaName && c2.ComponentType === c1.ComponentType) + components2.some(c2 => c2.ObjectId === c1.ObjectId && c2.ComponentType === c1.ComponentType) ); result = { @@ -158,6 +163,25 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { }; }, [solutions]); + // Group components by type for summary panel tree view + const groupedComponents = useMemo(() => { + if (!selectedSolution) return null; + + const grouped: Record = {}; + selectedSolution.Components.forEach(comp => { + const label = ComponentTypeLabels[comp.ComponentType] || 'Unknown'; + if (!grouped[label]) grouped[label] = []; + grouped[label].push(comp); + }); + + // Sort each group by name + Object.keys(grouped).forEach(key => { + grouped[key].sort((a, b) => a.Name.localeCompare(b.Name)); + }); + + return grouped; + }, [selectedSolution]); + const onCellSelect = (cellData: HeatMapCell) => { const solution1 = cellData.serieId as string; const solution2 = cellData.data.x as string; @@ -180,6 +204,17 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { } } + // Get all available component types from the data + const availableTypes = useMemo(() => { + const types = new Set(); + solutionComponents.forEach(collection => { + collection.Components.forEach(comp => { + types.add(comp.ComponentType); + }); + }); + return types; + }, [solutionComponents]); + return ( @@ -200,6 +235,73 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { + + {/* Filter Panel */} + + + + + + Component Type Filters + + + ({enabledComponentTypes.size} selected) + + + + + + setFiltersExpanded(!filtersExpanded)} + size="small" + > + {filtersExpanded ? : } + + + + + + + + {Object.entries(ComponentTypeCategories).map(([category, types]) => { + // Only show categories that have available types + const availableInCategory = types.filter(t => availableTypes.has(t)); + if (availableInCategory.length === 0) return null; + + return ( + + + {category} + + + {availableInCategory.map(type => ( + handleToggleType(type, e.target.checked)} + /> + } + label={ + + {ComponentTypeLabels[type]} + + } + sx={{ marginY: -0.5 }} + /> + ))} + + + ); + })} + + + + + + @@ -216,91 +318,99 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { Click on any cell to see the shared components between two solutions. - - onCellSelect(cell)} - hoverTarget="cell" - tooltip={({ cell }: { cell: HeatMapCell }) => ( - - - {cell.serieId} × {cell.data.x} - - - {cell.serieId === cell.data.x ? 'Same solution' : `${cell.value} shared components`} - - - )} - theme={{ - text: { - fill: theme.palette.text.primary - }, - tooltip: { - container: { + {solutionMatrix.solutionNames.length === 0 ? ( + + + No solution data available. Run the Generator to extract solution components. + + + ) : ( + + onCellSelect(cell)} + hoverTarget="cell" + tooltip={({ cell }: { cell: HeatMapCell }) => ( + + + {cell.serieId} × {cell.data.x} + + + {cell.serieId === cell.data.x ? 'Same solution' : `${cell.value} shared components`} + + + )} + theme={{ + text: { + fill: theme.palette.text.primary + }, + tooltip: { + container: { + background: theme.palette.background.paper, + color: theme.palette.text.primary + } } - } - }} - /> - + }} + /> + + )} @@ -311,29 +421,31 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { {selectedSolution ? ( - - {selectedSolution.Components.length > 0 ? ( + + {selectedSolution.Solution1} ∩ {selectedSolution.Solution2} + + + {selectedSolution.Components.length > 0 && groupedComponents ? ( - - Shared Components: ({selectedSolution.Components.length}) + + Shared Components: {selectedSolution.Components.length} -
    - {selectedSolution.Components.map(component => ( -
  • - - {component.Name} ({ - component.ComponentType === SolutionComponentTypeEnum.Entity - ? 'Table' - : component.ComponentType === SolutionComponentTypeEnum.Attribute - ? 'Column' - : component.ComponentType === SolutionComponentTypeEnum.Relationship - ? 'Relationship' - : 'Unknown' - }) - -
  • - ))} -
+ {Object.entries(groupedComponents).map(([typeLabel, comps]) => ( + + + {typeLabel} ({comps.length}) + + + {comps.map(comp => ( +
  • + + {comp.Name} + +
  • + ))} +
    +
    + ))}
    ) : ( diff --git a/Website/contexts/DatamodelDataContext.tsx b/Website/contexts/DatamodelDataContext.tsx index a495391..c663609 100644 --- a/Website/contexts/DatamodelDataContext.tsx +++ b/Website/contexts/DatamodelDataContext.tsx @@ -1,7 +1,7 @@ 'use client' import React, { createContext, useContext, useReducer, ReactNode } from "react"; -import { AttributeType, EntityType, GroupType, RelationshipType, SolutionWarningType } from "@/lib/Types"; +import { AttributeType, EntityType, GroupType, RelationshipType, SolutionWarningType, SolutionComponentCollectionType } from "@/lib/Types"; import { useSearchParams } from "next/navigation"; import { SearchScope } from "@/components/datamodelview/TimeSlicedSearch"; @@ -14,6 +14,7 @@ interface DatamodelDataState extends DataModelAction { entityMap?: Map; warnings: SolutionWarningType[]; solutionCount: number; + solutionComponents: SolutionComponentCollectionType[]; search: string; searchScope: SearchScope; filtered: Array< @@ -28,6 +29,7 @@ const initialState: DatamodelDataState = { groups: [], warnings: [], solutionCount: 0, + solutionComponents: [], search: "", searchScope: { columnNames: true, @@ -54,6 +56,8 @@ const datamodelDataReducer = (state: DatamodelDataState, action: any): Datamodel return { ...state, warnings: action.payload }; case "SET_SOLUTION_COUNT": return { ...state, solutionCount: action.payload }; + case "SET_SOLUTION_COMPONENTS": + return { ...state, solutionComponents: action.payload }; case "SET_SEARCH": return { ...state, search: action.payload }; case "SET_SEARCH_SCOPE": @@ -83,6 +87,7 @@ export const DatamodelDataProvider = ({ children }: { children: ReactNode }) => dispatch({ type: "SET_ENTITIES", payload: e.data.entityMap || new Map() }); dispatch({ type: "SET_WARNINGS", payload: e.data.warnings || [] }); dispatch({ type: "SET_SOLUTION_COUNT", payload: e.data.solutionCount || 0 }); + dispatch({ type: "SET_SOLUTION_COMPONENTS", payload: e.data.solutionComponents || [] }); worker.terminate(); }; worker.postMessage({}); diff --git a/Website/lib/Types.ts b/Website/lib/Types.ts index bc1ffba..7fb00ed 100644 --- a/Website/lib/Types.ts +++ b/Website/lib/Types.ts @@ -49,12 +49,161 @@ export type EntityType = { } +/// Solution component types matching Dataverse codes +/// See: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/entities/solutioncomponent export const enum SolutionComponentTypeEnum { Entity = 1, Attribute = 2, - Relationship = 3, + OptionSet = 9, + Relationship = 10, + EntityKey = 14, + SecurityRole = 20, + SavedQuery = 26, + Workflow = 29, + RibbonCustomization = 50, + SavedQueryVisualization = 59, + SystemForm = 60, + WebResource = 61, + SiteMap = 62, + ConnectionRole = 63, + HierarchyRule = 65, + CustomControl = 66, + FieldSecurityProfile = 70, + ModelDrivenApp = 80, + PluginAssembly = 91, + SDKMessageProcessingStep = 92, + CanvasApp = 300, + ConnectionReference = 372, + EnvironmentVariableDefinition = 380, + EnvironmentVariableValue = 381, + Dataflow = 418, + ConnectionRoleObjectTypeCode = 3233, + CustomAPI = 10240, + CustomAPIRequestParameter = 10241, + CustomAPIResponseProperty = 10242, + PluginPackage = 10639, + OrganizationSetting = 10563, + AppAction = 10645, + AppActionRule = 10948, + FxExpression = 11492, + DVFileSearch = 11723, + DVFileSearchAttribute = 11724, + DVFileSearchEntity = 11725, + AISkillConfig = 12075, } +/// Solution component data for insights view +export type SolutionComponentDataType = { + Name: string; + SchemaName: string; + ComponentType: SolutionComponentTypeEnum; + ObjectId: string; + IsExplicit: boolean; +} + +/// Collection of solution components grouped by solution +export type SolutionComponentCollectionType = { + SolutionId: string; + SolutionName: string; + Components: SolutionComponentDataType[]; +} + +/// Component type categories for UI grouping +export const ComponentTypeCategories: Record = { + 'Data Model': [ + SolutionComponentTypeEnum.Entity, + SolutionComponentTypeEnum.Attribute, + SolutionComponentTypeEnum.Relationship, + SolutionComponentTypeEnum.OptionSet, + SolutionComponentTypeEnum.EntityKey, + SolutionComponentTypeEnum.HierarchyRule, + SolutionComponentTypeEnum.Dataflow, + SolutionComponentTypeEnum.DVFileSearch, + SolutionComponentTypeEnum.DVFileSearchAttribute, + SolutionComponentTypeEnum.DVFileSearchEntity, + ], + 'User Interface': [ + SolutionComponentTypeEnum.SystemForm, + SolutionComponentTypeEnum.SavedQuery, + SolutionComponentTypeEnum.SavedQueryVisualization, + SolutionComponentTypeEnum.SiteMap, + SolutionComponentTypeEnum.CustomControl, + SolutionComponentTypeEnum.RibbonCustomization, + ], + 'Apps': [ + SolutionComponentTypeEnum.ModelDrivenApp, + SolutionComponentTypeEnum.CanvasApp, + SolutionComponentTypeEnum.AppAction, + SolutionComponentTypeEnum.AppActionRule, + ], + 'Code': [ + SolutionComponentTypeEnum.Workflow, + SolutionComponentTypeEnum.PluginAssembly, + SolutionComponentTypeEnum.SDKMessageProcessingStep, + SolutionComponentTypeEnum.WebResource, + SolutionComponentTypeEnum.CustomAPI, + SolutionComponentTypeEnum.CustomAPIRequestParameter, + SolutionComponentTypeEnum.CustomAPIResponseProperty, + SolutionComponentTypeEnum.PluginPackage, + SolutionComponentTypeEnum.FxExpression, + ], + 'Security': [ + SolutionComponentTypeEnum.SecurityRole, + SolutionComponentTypeEnum.FieldSecurityProfile, + ], + 'Configuration': [ + SolutionComponentTypeEnum.EnvironmentVariableDefinition, + SolutionComponentTypeEnum.EnvironmentVariableValue, + SolutionComponentTypeEnum.ConnectionReference, + SolutionComponentTypeEnum.ConnectionRole, + SolutionComponentTypeEnum.ConnectionRoleObjectTypeCode, + SolutionComponentTypeEnum.OrganizationSetting, + SolutionComponentTypeEnum.AISkillConfig, + ], +}; + +/// Human-readable labels for component types +export const ComponentTypeLabels: Record = { + [SolutionComponentTypeEnum.Entity]: 'Table', + [SolutionComponentTypeEnum.Attribute]: 'Column', + [SolutionComponentTypeEnum.OptionSet]: 'Choice', + [SolutionComponentTypeEnum.Relationship]: 'Relationship', + [SolutionComponentTypeEnum.EntityKey]: 'Key', + [SolutionComponentTypeEnum.SecurityRole]: 'Security Role', + [SolutionComponentTypeEnum.SavedQuery]: 'View', + [SolutionComponentTypeEnum.Workflow]: 'Cloud Flow', + [SolutionComponentTypeEnum.RibbonCustomization]: 'Ribbon', + [SolutionComponentTypeEnum.SavedQueryVisualization]: 'Chart', + [SolutionComponentTypeEnum.SystemForm]: 'Form', + [SolutionComponentTypeEnum.WebResource]: 'Web Resource', + [SolutionComponentTypeEnum.SiteMap]: 'Site Map', + [SolutionComponentTypeEnum.ConnectionRole]: 'Connection Role', + [SolutionComponentTypeEnum.HierarchyRule]: 'Hierarchy Rule', + [SolutionComponentTypeEnum.CustomControl]: 'Custom Control', + [SolutionComponentTypeEnum.FieldSecurityProfile]: 'Field Security', + [SolutionComponentTypeEnum.ModelDrivenApp]: 'Model-driven App', + [SolutionComponentTypeEnum.PluginAssembly]: 'Plugin Assembly', + [SolutionComponentTypeEnum.SDKMessageProcessingStep]: 'Plugin Step', + [SolutionComponentTypeEnum.CanvasApp]: 'Canvas App', + [SolutionComponentTypeEnum.ConnectionReference]: 'Connection Reference', + [SolutionComponentTypeEnum.EnvironmentVariableDefinition]: 'Environment Variable', + [SolutionComponentTypeEnum.EnvironmentVariableValue]: 'Env Variable Value', + [SolutionComponentTypeEnum.Dataflow]: 'Dataflow', + [SolutionComponentTypeEnum.ConnectionRoleObjectTypeCode]: 'Connection Role Type', + [SolutionComponentTypeEnum.CustomAPI]: 'Custom API', + [SolutionComponentTypeEnum.CustomAPIRequestParameter]: 'Custom API Parameter', + [SolutionComponentTypeEnum.CustomAPIResponseProperty]: 'Custom API Response', + [SolutionComponentTypeEnum.PluginPackage]: 'Plugin Package', + [SolutionComponentTypeEnum.OrganizationSetting]: 'Org Setting', + [SolutionComponentTypeEnum.AppAction]: 'App Action', + [SolutionComponentTypeEnum.AppActionRule]: 'App Action Rule', + [SolutionComponentTypeEnum.FxExpression]: 'Fx Expression', + [SolutionComponentTypeEnum.DVFileSearch]: 'DV File Search', + [SolutionComponentTypeEnum.DVFileSearchAttribute]: 'DV File Search Attr', + [SolutionComponentTypeEnum.DVFileSearchEntity]: 'DV File Search Entity', + [SolutionComponentTypeEnum.AISkillConfig]: 'AI Skill Config', +}; + export const enum RequiredLevel { None = 0, SystemRequired = 1, From 2abade1f94a6e5c49fb6c7f9b2a9a216790a7d77 Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:52:10 +0100 Subject: [PATCH 05/14] fold functionality on solution summary --- .../solutions/InsightsSolutionView.tsx | 108 +++++++++++++++--- 1 file changed, 92 insertions(+), 16 deletions(-) diff --git a/Website/components/insightsview/solutions/InsightsSolutionView.tsx b/Website/components/insightsview/solutions/InsightsSolutionView.tsx index 0b3994f..097ee3d 100644 --- a/Website/components/insightsview/solutions/InsightsSolutionView.tsx +++ b/Website/components/insightsview/solutions/InsightsSolutionView.tsx @@ -163,6 +163,9 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { }; }, [solutions]); + // Collapsed state for component groups in summary panel + const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); + // Group components by type for summary panel tree view const groupedComponents = useMemo(() => { if (!selectedSolution) return null; @@ -182,6 +185,45 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { return grouped; }, [selectedSolution]); + // Reset collapsed state when selecting a new cell, with smart expand logic + React.useEffect(() => { + if (selectedSolution && groupedComponents) { + const totalComponents = selectedSolution.Components.length; + if (totalComponents <= 10) { + // Expand all if small number of components + setCollapsedGroups(new Set()); + } else { + // Collapse all by default for larger sets + setCollapsedGroups(new Set(Object.keys(groupedComponents))); + } + } + }, [selectedSolution?.Solution1, selectedSolution?.Solution2]); + + // Toggle individual group collapse state + const handleToggleGroup = (label: string) => { + setCollapsedGroups(prev => { + const newSet = new Set(prev); + if (newSet.has(label)) { + newSet.delete(label); + } else { + newSet.add(label); + } + return newSet; + }); + }; + + // Expand all groups + const handleExpandAllGroups = () => { + setCollapsedGroups(new Set()); + }; + + // Collapse all groups + const handleCollapseAllGroups = () => { + if (groupedComponents) { + setCollapsedGroups(new Set(Object.keys(groupedComponents))); + } + }; + const onCellSelect = (cellData: HeatMapCell) => { const solution1 = cellData.serieId as string; const solution2 = cellData.data.x as string; @@ -427,23 +469,57 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { {selectedSolution.Components.length > 0 && groupedComponents ? ( - - Shared Components: {selectedSolution.Components.length} - - {Object.entries(groupedComponents).map(([typeLabel, comps]) => ( - - - {typeLabel} ({comps.length}) - - - {comps.map(comp => ( -
  • - - {comp.Name} - -
  • - ))} + + + {Object.keys(groupedComponents).length} types, {selectedSolution.Components.length} components + + + + + + + {Object.entries(groupedComponents) + .sort((a, b) => a[1].length - b[1].length) + .map(([typeLabel, comps]) => ( + + handleToggleGroup(typeLabel)} + sx={{ + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + py: 0.5, + px: 0.5, + borderRadius: 1, + '&:hover': { + backgroundColor: 'action.hover' + } + }} + > + {collapsedGroups.has(typeLabel) ? ( + + ) : ( + + )} + + {typeLabel} ({comps.length}) + + + + {comps.map(comp => ( +
  • + + {comp.Name} + +
  • + ))} +
    +
    ))}
    From 742d8016c57b625102815b756d77d8d895e39d53 Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:12:31 +0100 Subject: [PATCH 06/14] added wildcard functionality --- Tasks/Todo/SolutionRelationsFullOverview.md | 2 + .../solutions/InsightsSolutionView.tsx | 68 +++++++++++++++++-- Website/lib/Types.ts | 11 +++ 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/Tasks/Todo/SolutionRelationsFullOverview.md b/Tasks/Todo/SolutionRelationsFullOverview.md index c8511e6..47c5cde 100644 --- a/Tasks/Todo/SolutionRelationsFullOverview.md +++ b/Tasks/Todo/SolutionRelationsFullOverview.md @@ -12,6 +12,8 @@ Known Elements: Code snippet from other program to reference various component types. +MIGHT BE INCOMPLETE LIST BELOW + ``` private static readonly Dictionary ComponentTypeNames = new() { diff --git a/Website/components/insightsview/solutions/InsightsSolutionView.tsx b/Website/components/insightsview/solutions/InsightsSolutionView.tsx index 097ee3d..ce83d73 100644 --- a/Website/components/insightsview/solutions/InsightsSolutionView.tsx +++ b/Website/components/insightsview/solutions/InsightsSolutionView.tsx @@ -21,6 +21,20 @@ interface HeatMapCell { value: number | null; } +// Helper to get label for component type, with fallback for unmapped types +const getComponentTypeLabel = (type: SolutionComponentTypeEnum): string => { + return ComponentTypeLabels[type] || `Unknown (${type})`; +}; + +// Get all types that are in any category (known/mapped types) +const getAllCategorizedTypes = (): Set => { + const allTypes = new Set(); + Object.values(ComponentTypeCategories).forEach(types => { + types.forEach(t => allTypes.add(t)); + }); + return allTypes; +}; + const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { const { solutionComponents } = useDatamodelData(); const theme = useTheme(); @@ -55,10 +69,10 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { }); }; - // Select all component types + // Select all component types (including unmapped ones) const handleSelectAll = () => { - const allTypes = Object.values(ComponentTypeCategories).flat(); - setEnabledComponentTypes(new Set(allTypes)); + // Include all available types from the data (both categorized and unmapped) + setEnabledComponentTypes(new Set(availableTypes)); }; // Clear all component types @@ -172,7 +186,7 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { const grouped: Record = {}; selectedSolution.Components.forEach(comp => { - const label = ComponentTypeLabels[comp.ComponentType] || 'Unknown'; + const label = getComponentTypeLabel(comp.ComponentType); if (!grouped[label]) grouped[label] = []; grouped[label].push(comp); }); @@ -246,15 +260,27 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { } } - // Get all available component types from the data - const availableTypes = useMemo(() => { + // Get all available component types from the data, and identify unmapped ones + const { availableTypes, unmappedTypes } = useMemo(() => { const types = new Set(); solutionComponents.forEach(collection => { collection.Components.forEach(comp => { types.add(comp.ComponentType); }); }); - return types; + + // Find types that exist in data but aren't in any category + const categorizedTypes = getAllCategorizedTypes(); + const unmapped: SolutionComponentTypeEnum[] = []; + types.forEach(t => { + if (!categorizedTypes.has(t)) { + unmapped.push(t); + } + }); + // Sort unmapped by numeric value for consistent display + unmapped.sort((a, b) => a - b); + + return { availableTypes: types, unmappedTypes: unmapped }; }, [solutionComponents]); return ( @@ -338,6 +364,34 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => {
    ); })} + {/* Show unmapped/unknown component types in "Other" category */} + {unmappedTypes.length > 0 && ( + + + Other + + + {unmappedTypes.map(type => ( + handleToggleType(type, e.target.checked)} + /> + } + label={ + + {getComponentTypeLabel(type)} + + } + sx={{ marginY: -0.5 }} + /> + ))} + + + )} diff --git a/Website/lib/Types.ts b/Website/lib/Types.ts index 7fb00ed..9583434 100644 --- a/Website/lib/Types.ts +++ b/Website/lib/Types.ts @@ -81,6 +81,9 @@ export const enum SolutionComponentTypeEnum { CustomAPI = 10240, CustomAPIRequestParameter = 10241, CustomAPIResponseProperty = 10242, + RequirementResourcePreference = 10019, + RequirementStatus = 10020, + SchedulingParameter = 10025, PluginPackage = 10639, OrganizationSetting = 10563, AppAction = 10645, @@ -160,6 +163,11 @@ export const ComponentTypeCategories: Record = { [SolutionComponentTypeEnum.DVFileSearchAttribute]: 'DV File Search Attr', [SolutionComponentTypeEnum.DVFileSearchEntity]: 'DV File Search Entity', [SolutionComponentTypeEnum.AISkillConfig]: 'AI Skill Config', + [SolutionComponentTypeEnum.RequirementResourcePreference]: 'Resource Preference', + [SolutionComponentTypeEnum.RequirementStatus]: 'Requirement Status', + [SolutionComponentTypeEnum.SchedulingParameter]: 'Scheduling Parameter', }; export const enum RequiredLevel { From c63cfb9bdaba748b88a445e794360a1d6c443b04 Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:20:59 +0100 Subject: [PATCH 07/14] types shared on hover --- .../solutions/InsightsSolutionView.tsx | 62 ++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/Website/components/insightsview/solutions/InsightsSolutionView.tsx b/Website/components/insightsview/solutions/InsightsSolutionView.tsx index ce83d73..0a2ded8 100644 --- a/Website/components/insightsview/solutions/InsightsSolutionView.tsx +++ b/Website/components/insightsview/solutions/InsightsSolutionView.tsx @@ -478,21 +478,53 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { ]} onClick={(cell: HeatMapCell) => onCellSelect(cell)} hoverTarget="cell" - tooltip={({ cell }: { cell: HeatMapCell }) => ( - - - {cell.serieId} × {cell.data.x} - - - {cell.serieId === cell.data.x ? 'Same solution' : `${cell.value} shared components`} - - - )} + tooltip={({ cell }: { cell: HeatMapCell }) => { + // Get the shared components for this cell to show type breakdown + const solution1 = cell.serieId; + const solution2 = cell.data.x; + const i = solutionMatrix.solutionNames.indexOf(solution1); + const j = solutionMatrix.solutionNames.indexOf(solution2); + + let typeBreakdown: Record = {}; + if (solution1 !== solution2 && i !== -1 && j !== -1) { + const matrixIndex = i * solutionMatrix.solutionNames.length + j; + const sharedComponents = solutionMatrix.matrix[matrixIndex].sharedComponents; + + // Group by component type + sharedComponents.forEach(comp => { + const label = getComponentTypeLabel(comp.ComponentType); + typeBreakdown[label] = (typeBreakdown[label] || 0) + 1; + }); + } + + const sortedTypes = Object.entries(typeBreakdown).sort((a, b) => b[1] - a[1]); + + return ( + + + {cell.serieId} × {cell.data.x} + + + {cell.serieId === cell.data.x ? 'Same solution' : `${cell.value} shared components`} + + {sortedTypes.length > 0 && ( + + {sortedTypes.map(([label, count]) => ( + + {label}: {count} + + ))} + + )} + + ); + }} theme={{ text: { fill: theme.palette.text.primary From 60c7b47f6c1772bce29a7db150272d636f3cecb9 Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:45:26 +0100 Subject: [PATCH 08/14] new feature plan and plans cleanup --- .../SolutionRelationsFullOverview-Plan.md | 0 .../SolutionRelationsFullOverview.md | 0 .../SolutionRelationsTypesSharedHover.md | 0 .../SolutionSummaryTreeView.md | 10 + Tasks/Plans/TypesToSolutionsOverview-Plan.md | 326 ++++++++++++++++++ Tasks/Todo/PerformanceImprovements.md | 1 - Tasks/Todo/SolutionSummaryTreeView.md | 4 - 7 files changed, 336 insertions(+), 5 deletions(-) rename Tasks/{Plans => Done/SolutionInsightsImprovements/MatrixImprovements}/SolutionRelationsFullOverview-Plan.md (100%) rename Tasks/{Todo => Done/SolutionInsightsImprovements/MatrixImprovements}/SolutionRelationsFullOverview.md (100%) rename Tasks/{Todo => Done/SolutionInsightsImprovements/MatrixImprovements}/SolutionRelationsTypesSharedHover.md (100%) create mode 100644 Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionSummaryTreeView.md create mode 100644 Tasks/Plans/TypesToSolutionsOverview-Plan.md delete mode 100644 Tasks/Todo/PerformanceImprovements.md delete mode 100644 Tasks/Todo/SolutionSummaryTreeView.md diff --git a/Tasks/Plans/SolutionRelationsFullOverview-Plan.md b/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview-Plan.md similarity index 100% rename from Tasks/Plans/SolutionRelationsFullOverview-Plan.md rename to Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview-Plan.md diff --git a/Tasks/Todo/SolutionRelationsFullOverview.md b/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview.md similarity index 100% rename from Tasks/Todo/SolutionRelationsFullOverview.md rename to Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview.md diff --git a/Tasks/Todo/SolutionRelationsTypesSharedHover.md b/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsTypesSharedHover.md similarity index 100% rename from Tasks/Todo/SolutionRelationsTypesSharedHover.md rename to Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsTypesSharedHover.md diff --git a/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionSummaryTreeView.md b/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionSummaryTreeView.md new file mode 100644 index 0000000..d566ac2 --- /dev/null +++ b/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionSummaryTreeView.md @@ -0,0 +1,10 @@ +Tasks: +- 5823: Solution summary -> treeview of all component types "enabled" + Attributes + |- form, + my-form, + form-a, + form-b + |- view, + view-a, + main-view, \ No newline at end of file diff --git a/Tasks/Plans/TypesToSolutionsOverview-Plan.md b/Tasks/Plans/TypesToSolutionsOverview-Plan.md new file mode 100644 index 0000000..e2db3e6 --- /dev/null +++ b/Tasks/Plans/TypesToSolutionsOverview-Plan.md @@ -0,0 +1,326 @@ +# Plan: Types to Solutions Overview + +## Summary + +Add a new section to the Solution Insights page (`/insights?view=solutions`) that answers the question: **"Which solutions contain each component type, and what specific components are in each?"** + +This is the inverse of the existing heatmap view: +- **Heatmap (existing)**: Solution → Solution intersection of components +- **Types Overview (new)**: Component Type → Solutions → Specific Components + +Example use case: "Which solutions have plugin assemblies, and which plugins are in each solution?" + +--- + +## User Story + +As a Dataverse administrator, I want to see: +1. A list of all component types that exist in my solutions +2. For each component type, which solutions contain that type +3. For each solution, what specific components of that type it contains + +**Visual hierarchy**: `Component Type → Solutions → Component Names` + +--- + +## UI Design + +### Layout + +New section below the existing heatmap and summary panel, occupying full width. + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Component Types Overview [▼ / ▲] │ +│ See which solutions contain each component type │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ▼ Plugin Assembly (3 solutions, 12 components) │ +│ ├─ ▼ CRM Core Solution (5) │ +│ │ • AccountPlugin │ +│ │ • ContactPlugin │ +│ │ • LeadPlugin │ +│ │ • OpportunityPlugin │ +│ │ • CasePlugin │ +│ │ │ +│ ├─ ▼ Integration Solution (4) │ +│ │ • ERPSyncPlugin │ +│ │ • WebhookPlugin │ +│ │ • DataExportPlugin │ +│ │ • ImportPlugin │ +│ │ │ +│ └─ ▶ Marketing Solution (3) [collapsed] │ +│ │ +│ ▶ Cloud Flow (5 solutions, 47 components) [collapsed] │ +│ │ +│ ▼ Canvas App (2 solutions, 4 components) │ +│ ├─ ▶ Sales Portal (2) [collapsed] │ +│ └─ ▶ Service Desk (2) [collapsed] │ +│ │ +│ ▶ Model-driven App (4 solutions, 8 components) [collapsed] │ +│ │ +│ ▶ Custom API (1 solution, 3 components) [collapsed] │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Interaction + +1. **Component Type Level**: Click to expand/collapse all solutions for that type +2. **Solution Level**: Click to expand/collapse specific components in that solution +3. **Sorting**: Types sorted by total component count (descending) - most used types first +4. **Empty Types**: Only show types that have at least one component in the data + +### Features + +- **Section collapsible**: Entire section can be collapsed to reduce visual clutter +- **Expand/Collapse All**: Buttons to expand or collapse all types and solutions +- **Search/Filter** (optional enhancement): Filter by type name or solution name +- **Respects filter panel**: Uses same `enabledComponentTypes` filter as the heatmap + +--- + +## Implementation Steps + +### Step 1: Add Collapsed State for Section + +Add state to track if the entire "Types Overview" section is expanded/collapsed. + +```typescript +const [typesOverviewExpanded, setTypesOverviewExpanded] = useState(true); +``` + +### Step 2: Create Data Structure + +Build a hierarchical data structure from the existing `solutionComponents`: + +```typescript +type TypeToSolutionsData = { + componentType: SolutionComponentTypeEnum; + typeLabel: string; + totalCount: number; + solutions: { + solutionName: string; + components: SolutionComponentDataType[]; + }[]; +}[]; +``` + +### Step 3: Create `typesToSolutions` useMemo + +```typescript +const typesToSolutions = useMemo(() => { + // 1. Group all components by type + // 2. For each type, group by solution + // 3. Filter by enabledComponentTypes + // 4. Sort by total count descending +}, [solutionComponents, enabledComponentTypes]); +``` + +### Step 4: Create Collapse State for Types and Solutions + +```typescript +// Track which types are collapsed +const [collapsedTypes, setCollapsedTypes] = useState>(new Set()); + +// Track which solutions within each type are collapsed (key: "type-solutionName") +const [collapsedTypeSolutions, setCollapsedTypeSolutions] = useState>(new Set()); +``` + +### Step 5: Implement Smart Defaults + +- If a type has ≤3 solutions, expand all by default +- If a solution has ≤5 components, expand by default +- Otherwise, start collapsed + +### Step 6: Render UI + +Use MUI components: +- `Paper` for container +- `Collapse` for expand/collapse animation +- `Box` with `onClick` for clickable headers +- `ExpandMore/ExpandLess` icons for visual indicators +- Nested `Box` for indentation + +--- + +## Files to Modify + +| File | Change | +|------|--------| +| `InsightsSolutionView.tsx` | Add new section with types overview UI | + +No Generator changes needed - uses existing `solutionComponents` data. + +--- + +## Code Structure + +### New useMemo Hook + +```typescript +const typesToSolutions = useMemo(() => { + // Build map: componentType -> solutionName -> components[] + const typeMap = new Map>(); + + solutionComponents.forEach(collection => { + collection.Components.forEach(comp => { + // Only include enabled types + if (!enabledComponentTypes.has(comp.ComponentType)) return; + + if (!typeMap.has(comp.ComponentType)) { + typeMap.set(comp.ComponentType, new Map()); + } + const solutionMap = typeMap.get(comp.ComponentType)!; + + if (!solutionMap.has(collection.SolutionName)) { + solutionMap.set(collection.SolutionName, []); + } + solutionMap.get(collection.SolutionName)!.push(comp); + }); + }); + + // Convert to array and sort + const result = Array.from(typeMap.entries()) + .map(([type, solutions]) => { + const solutionsArray = Array.from(solutions.entries()) + .map(([name, comps]) => ({ + solutionName: name, + components: comps.sort((a, b) => a.Name.localeCompare(b.Name)) + })) + .sort((a, b) => b.components.length - a.components.length); + + return { + componentType: type, + typeLabel: getComponentTypeLabel(type), + totalCount: solutionsArray.reduce((sum, s) => sum + s.components.length, 0), + solutions: solutionsArray + }; + }) + .sort((a, b) => b.totalCount - a.totalCount); + + return result; +}, [solutionComponents, enabledComponentTypes]); +``` + +### UI Rendering (Pseudocode) + +```tsx + + + {/* Header with expand/collapse */} + + Component Types Overview + + + + setTypesOverviewExpanded(!typesOverviewExpanded)}> + {typesOverviewExpanded ? : } + + + + + + {typesToSolutions.map(typeData => ( + + {/* Type header - clickable */} + toggleType(typeData.componentType)}> + {typeData.typeLabel} + ({typeData.solutions.length} solutions, {typeData.totalCount} components) + + + + {typeData.solutions.map(solution => ( + + {/* Solution header - clickable */} + toggleSolution(typeData.componentType, solution.solutionName)}> + {solution.solutionName} ({solution.components.length}) + + + + + {solution.components.map(comp => ( +
  • {comp.Name}
  • + ))} +
    +
    +
    + ))} +
    +
    + ))} +
    +
    +
    +``` + +--- + +## Smart Expand/Collapse Logic + +### On Initial Load + +```typescript +useEffect(() => { + // Auto-expand types with few solutions + const autoExpanded = new Set(); + const autoCollapsedSolutions = new Set(); + + typesToSolutions.forEach(typeData => { + if (typeData.solutions.length > 3) { + // Collapse types with many solutions + autoExpanded.add(typeData.componentType); + } + + typeData.solutions.forEach(solution => { + if (solution.components.length > 5) { + // Collapse solutions with many components + autoCollapsedSolutions.add(`${typeData.componentType}-${solution.solutionName}`); + } + }); + }); + + setCollapsedTypes(autoExpanded); + setCollapsedTypeSolutions(autoCollapsedSolutions); +}, [typesToSolutions]); +``` + +--- + +## Styling Notes + +- Use consistent spacing and indentation with existing UI +- Use `text.secondary` color for counts +- Use `primary.main` color for type and solution names +- Tree-view indentation: 24px per level +- Hover effect on clickable rows + +--- + +## Edge Cases + +1. **No data**: Show "No component data available" message +2. **No enabled types**: Show "Select component types in the filter panel above" +3. **Empty after filter**: Show "No components match the selected filters" +4. **Very long lists**: Consider virtualization if performance becomes an issue (future enhancement) + +--- + +## Testing + +1. Verify types are correctly grouped +2. Verify solutions are correctly nested under types +3. Verify component names appear under solutions +4. Verify expand/collapse works at all levels +5. Verify filter panel affects the overview +6. Verify sorting (most components first) +7. Verify smart expand logic works correctly + +--- + +## Future Enhancements (Out of Scope) + +- Search/filter within the types overview +- Export to CSV/Excel +- Click to navigate to component details +- Highlight shared components (appear in multiple solutions) diff --git a/Tasks/Todo/PerformanceImprovements.md b/Tasks/Todo/PerformanceImprovements.md deleted file mode 100644 index 74eaf0a..0000000 --- a/Tasks/Todo/PerformanceImprovements.md +++ /dev/null @@ -1 +0,0 @@ -NPM install takes forever, generally slow on compile and other areas. \ No newline at end of file diff --git a/Tasks/Todo/SolutionSummaryTreeView.md b/Tasks/Todo/SolutionSummaryTreeView.md deleted file mode 100644 index ebf9ae8..0000000 --- a/Tasks/Todo/SolutionSummaryTreeView.md +++ /dev/null @@ -1,4 +0,0 @@ -Tasks: -- 5823: Solution summary -> treeview of all component types "enabled" - Attributes - |- a, b, c \ No newline at end of file From 716b79a82fc2b021ce6bb06d99c11c625db0a2cd Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:16:54 +0100 Subject: [PATCH 09/14] final feature change on component types overview --- .../solutions/InsightsSolutionView.tsx | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) diff --git a/Website/components/insightsview/solutions/InsightsSolutionView.tsx b/Website/components/insightsview/solutions/InsightsSolutionView.tsx index 0a2ded8..0d74b2f 100644 --- a/Website/components/insightsview/solutions/InsightsSolutionView.tsx +++ b/Website/components/insightsview/solutions/InsightsSolutionView.tsx @@ -283,6 +283,100 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { return { availableTypes: types, unmappedTypes: unmapped }; }, [solutionComponents]); + // ===== TYPES TO SOLUTIONS OVERVIEW ===== + + // State for section expansion + const [typesOverviewExpanded, setTypesOverviewExpanded] = useState(true); + + // State for collapsed types and components within types + const [collapsedTypes, setCollapsedTypes] = useState>(new Set()); + + // State for "shared only" filter - default to true (only show components in multiple solutions) + const [showSharedOnly, setShowSharedOnly] = useState(true); + + // Build hierarchical data: Component Type → Specific Component → Solutions it appears in + const typesToComponents = useMemo(() => { + // Build map: componentType -> objectId -> { component, solutions[] } + const typeMap = new Map>(); + + solutionComponents.forEach(collection => { + collection.Components.forEach(comp => { + // Only include enabled types + if (!enabledComponentTypes.has(comp.ComponentType)) return; + + if (!typeMap.has(comp.ComponentType)) { + typeMap.set(comp.ComponentType, new Map()); + } + const componentMap = typeMap.get(comp.ComponentType)!; + + if (!componentMap.has(comp.ObjectId)) { + componentMap.set(comp.ObjectId, { component: comp, solutions: [] }); + } + componentMap.get(comp.ObjectId)!.solutions.push(collection.SolutionName); + }); + }); + + // Convert to array and sort + const result = Array.from(typeMap.entries()) + .map(([type, components]) => { + let componentsArray = Array.from(components.values()); + + // Apply "shared only" filter if enabled + if (showSharedOnly) { + componentsArray = componentsArray.filter(c => c.solutions.length > 1); + } + + // Sort alphabetically by component name + componentsArray.sort((a, b) => a.component.Name.localeCompare(b.component.Name)); + + const sharedCount = componentsArray.filter(c => c.solutions.length > 1).length; + + return { + componentType: type, + typeLabel: getComponentTypeLabel(type), + totalCount: componentsArray.length, + sharedCount: sharedCount, + components: componentsArray + }; + }) + // Filter out types with no components (when showSharedOnly and no shared components) + .filter(t => t.components.length > 0) + // Sort alphabetically by type label + .sort((a, b) => a.typeLabel.localeCompare(b.typeLabel)); + + return result; + }, [solutionComponents, enabledComponentTypes, showSharedOnly]); + + // Collapse all types when data/filters change + React.useEffect(() => { + const allTypes = new Set(typesToComponents.map(t => t.componentType)); + setCollapsedTypes(allTypes); + }, [typesToComponents]); + + // Toggle type collapse + const handleToggleTypeCollapse = (type: SolutionComponentTypeEnum) => { + setCollapsedTypes(prev => { + const newSet = new Set(prev); + if (newSet.has(type)) { + newSet.delete(type); + } else { + newSet.add(type); + } + return newSet; + }); + }; + + // Expand all types + const handleExpandAllTypesOverview = () => { + setCollapsedTypes(new Set()); + }; + + // Collapse all types + const handleCollapseAllTypesOverview = () => { + const allTypes = new Set(typesToComponents.map(t => t.componentType)); + setCollapsedTypes(allTypes); + }; + return ( @@ -623,6 +717,138 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { )} + + {/* Component Types Overview Section */} + + + + + + Component Types Overview + + + + {InfoIcon} + + + + + + setShowSharedOnly(e.target.checked)} + /> + } + label={ + + Shared only + + } + sx={{ mr: 1 }} + /> + + + + setTypesOverviewExpanded(!typesOverviewExpanded)} + size="small" + > + {typesOverviewExpanded ? : } + + + + + + {typesToComponents.length === 0 ? ( + + + {enabledComponentTypes.size === 0 + ? 'Select component types in the filter panel above to see the overview.' + : 'No components match the selected filters.'} + + + ) : ( + + {typesToComponents.map(typeData => ( + + {/* Type header - clickable */} + handleToggleTypeCollapse(typeData.componentType)} + sx={{ + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + py: 1, + px: 1, + borderRadius: 1, + backgroundColor: 'action.hover', + '&:hover': { + backgroundColor: 'action.selected' + } + }} + > + {collapsedTypes.has(typeData.componentType) ? ( + + ) : ( + + )} + + {typeData.typeLabel} + + + ({typeData.totalCount} {typeData.totalCount === 1 ? 'component' : 'components'}{typeData.sharedCount > 0 && `, ${typeData.sharedCount} shared`}) + + + + {/* Components under this type */} + + + {typeData.components.map(({ component, solutions }) => ( + + + + {component.Name} + + + → + + {solutions.map((sol) => ( + + {sol} + + ))} + + + ))} + + + + ))} + + )} + + + ) } From 7e85d250431f435a433dbc335c3ccd42e535423f Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:34:27 +0100 Subject: [PATCH 10/14] plans updates --- .../TypesPerSolutionOverview.md | 0 .../TypesToSolutionsOverview-Plan.md | 0 Tasks/overview.md | 9 --------- 3 files changed, 9 deletions(-) rename Tasks/{Todo => Done/InsightsComponentsOverview}/TypesPerSolutionOverview.md (100%) rename Tasks/{Plans => Done/InsightsComponentsOverview}/TypesToSolutionsOverview-Plan.md (100%) delete mode 100644 Tasks/overview.md diff --git a/Tasks/Todo/TypesPerSolutionOverview.md b/Tasks/Done/InsightsComponentsOverview/TypesPerSolutionOverview.md similarity index 100% rename from Tasks/Todo/TypesPerSolutionOverview.md rename to Tasks/Done/InsightsComponentsOverview/TypesPerSolutionOverview.md diff --git a/Tasks/Plans/TypesToSolutionsOverview-Plan.md b/Tasks/Done/InsightsComponentsOverview/TypesToSolutionsOverview-Plan.md similarity index 100% rename from Tasks/Plans/TypesToSolutionsOverview-Plan.md rename to Tasks/Done/InsightsComponentsOverview/TypesToSolutionsOverview-Plan.md diff --git a/Tasks/overview.md b/Tasks/overview.md deleted file mode 100644 index 139a8a7..0000000 --- a/Tasks/overview.md +++ /dev/null @@ -1,9 +0,0 @@ -*gør hvad der er lækkert* - -Prioritized: -[FullOverView](Todo\SolutionRelationsFullOverview.md) - -Todo: -[SolutionRelationsTypesSharedHover](Todo\SolutionRelationsTypesSharedHover.md) -[SolutionSummaryTreeView](Todo\SolutionSummaryTreeView.md) -[TypesPerSolutionOverview](Todo\TypesPerSolutionOverview.md) From d668afb9a9b519e08c860b9436b4fb65d93fb657 Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:01:23 +0100 Subject: [PATCH 11/14] updated summaries to show specific tables on relevant componentTypes --- Generator/DTO/SolutionComponent.cs | 3 +- Generator/DataverseService.cs | 26 ++- .../Services/SolutionComponentExtractor.cs | 151 +++++++++++++----- .../solutions/InsightsSolutionView.tsx | 40 ++++- Website/lib/Types.ts | 1 + 5 files changed, 172 insertions(+), 49 deletions(-) diff --git a/Generator/DTO/SolutionComponent.cs b/Generator/DTO/SolutionComponent.cs index 1ef4abd..e474f55 100644 --- a/Generator/DTO/SolutionComponent.cs +++ b/Generator/DTO/SolutionComponent.cs @@ -62,7 +62,8 @@ public record SolutionComponentData( string SchemaName, SolutionComponentType ComponentType, Guid ObjectId, - bool IsExplicit); + bool IsExplicit, + string? RelatedTable = null); /// /// Collection of solution components grouped by solution. diff --git a/Generator/DataverseService.cs b/Generator/DataverseService.cs index 4955658..f5b3f59 100644 --- a/Generator/DataverseService.cs +++ b/Generator/DataverseService.cs @@ -305,6 +305,27 @@ public DataverseService( r => r.MetadataId!.Value, r => r.SchemaName); + // Build entity lookups for attributes, relationships, and keys (maps component ID to parent entity name) + var attributeEntityLookup = entitiesInSolutionMetadata + .SelectMany(e => e.Attributes.Where(a => a.MetadataId.HasValue) + .Select(a => (AttributeId: a.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName))) + .ToDictionary(x => x.AttributeId, x => x.EntityName); + + var relationshipEntityLookup = entitiesInSolutionMetadata + .SelectMany(e => e.ManyToManyRelationships.Cast() + .Concat(e.OneToManyRelationships) + .Concat(e.ManyToOneRelationships) + .Where(r => r.MetadataId.HasValue) + .Select(r => (RelationshipId: r.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName))) + .DistinctBy(x => x.RelationshipId) + .ToDictionary(x => x.RelationshipId, x => x.EntityName); + + var keyEntityLookup = entitiesInSolutionMetadata + .SelectMany(e => (e.Keys ?? Array.Empty()) + .Where(k => k.MetadataId.HasValue) + .Select(k => (KeyId: k.MetadataId!.Value, EntityName: e.DisplayName.ToLabelString() ?? e.LogicalName))) + .ToDictionary(x => x.KeyId, x => x.EntityName); + // Build solution name lookup var solutionNameLookup = solutionLookup.ToDictionary( kvp => kvp.Key, @@ -315,7 +336,10 @@ public DataverseService( solutionNameLookup, entityNameLookup, attributeNameLookup, - relationshipNameLookup); + relationshipNameLookup, + attributeEntityLookup, + relationshipEntityLookup, + keyEntityLookup); logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracted components for {solutionComponentCollections.Count} solutions"); } diff --git a/Generator/Services/SolutionComponentExtractor.cs b/Generator/Services/SolutionComponentExtractor.cs index 47f46f2..5d4fc66 100644 --- a/Generator/Services/SolutionComponentExtractor.cs +++ b/Generator/Services/SolutionComponentExtractor.cs @@ -48,33 +48,39 @@ public class SolutionComponentExtractor }; /// - /// Maps component type codes to their Dataverse table, name column, and primary key column for name resolution. + /// Maps component type codes to their Dataverse table, name column, primary key column, and optional entity column for name resolution. /// Primary key is optional - if null, defaults to tablename + "id". + /// EntityColumn is used to get the related table for components like forms and views. /// - private static readonly Dictionary ComponentTableMap = new() + private static readonly Dictionary ComponentTableMap = new() { - { 20, ("role", "name", null) }, - { 26, ("savedquery", "name", null) }, - { 29, ("workflow", "name", null) }, - { 50, ("ribboncustomization", "entity", null) }, - { 59, ("savedqueryvisualization", "name", null) }, - { 60, ("systemform", "name", "formid") }, // systemform uses formid, not systemformid - { 61, ("webresource", "name", null) }, - { 62, ("sitemap", "sitemapname", null) }, - { 63, ("connectionrole", "name", null) }, - { 65, ("hierarchyrule", "name", null) }, - { 66, ("customcontrol", "name", null) }, - { 70, ("fieldsecurityprofile", "name", null) }, - { 80, ("appmodule", "name", "appmoduleid") }, // appmodule uses appmoduleid - { 91, ("pluginassembly", "name", null) }, - { 92, ("sdkmessageprocessingstep", "name", null) }, - { 300, ("canvasapp", "name", null) }, - { 372, ("connectionreference", "connectionreferencedisplayname", null) }, - { 380, ("environmentvariabledefinition", "displayname", null) }, - { 381, ("environmentvariablevalue", "schemaname", null) }, - { 418, ("workflow", "name", null) }, // Dataflows are stored in workflow table with category=6 + { 20, ("role", "name", null, null) }, + { 26, ("savedquery", "name", null, "returnedtypecode") }, // Views have returnedtypecode for the entity + { 29, ("workflow", "name", null, null) }, + { 50, ("ribboncustomization", "entity", null, null) }, + { 59, ("savedqueryvisualization", "name", null, null) }, + { 60, ("systemform", "name", "formid", "objecttypecode") }, // Forms have objecttypecode for the entity + { 61, ("webresource", "name", null, null) }, + { 62, ("sitemap", "sitemapname", null, null) }, + { 63, ("connectionrole", "name", null, null) }, + { 65, ("hierarchyrule", "name", null, null) }, + { 66, ("customcontrol", "name", null, null) }, + { 70, ("fieldsecurityprofile", "name", null, null) }, + { 80, ("appmodule", "name", "appmoduleid", null) }, // appmodule uses appmoduleid + { 91, ("pluginassembly", "name", null, null) }, + { 92, ("sdkmessageprocessingstep", "name", null, null) }, + { 300, ("canvasapp", "name", null, null) }, + { 372, ("connectionreference", "connectionreferencedisplayname", null, null) }, + { 380, ("environmentvariabledefinition", "displayname", null, null) }, + { 381, ("environmentvariablevalue", "schemaname", null, null) }, + { 418, ("workflow", "name", null, null) }, // Dataflows are stored in workflow table with category=6 }; + /// + /// Component types that should have a related table displayed. + /// + private static readonly HashSet ComponentTypesWithRelatedTable = new() { 2, 10, 14, 26, 60 }; // Attribute, Relationship, EntityKey, SavedQuery (View), SystemForm + public SolutionComponentExtractor(ServiceClient client, ILogger logger) { _client = client; @@ -89,7 +95,10 @@ public async Task> ExtractSolutionComponentsAs Dictionary solutionNameLookup, Dictionary? entityNameLookup = null, Dictionary? attributeNameLookup = null, - Dictionary? relationshipNameLookup = null) + Dictionary? relationshipNameLookup = null, + Dictionary? attributeEntityLookup = null, + Dictionary? relationshipEntityLookup = null, + Dictionary? keyEntityLookup = null) { _logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] Extracting solution components for {solutionIds.Count} solutions"); @@ -109,7 +118,7 @@ public async Task> ExtractSolutionComponentsAs .ToDictionary(g => g.Key, g => g.ToList()); // Resolve names for each component type - var nameCache = await BuildNameCacheAsync(rawComponents, entityNameLookup, attributeNameLookup, relationshipNameLookup); + var nameCache = await BuildNameCacheAsync(rawComponents, entityNameLookup, attributeNameLookup, relationshipNameLookup, attributeEntityLookup, relationshipEntityLookup, keyEntityLookup); // Build the result collections var result = new List(); @@ -129,7 +138,8 @@ public async Task> ExtractSolutionComponentsAs SchemaName: ResolveComponentSchemaName(c, nameCache), ComponentType: (SolutionComponentType)c.ComponentType, ObjectId: c.ObjectId, - IsExplicit: c.IsExplicit)) + IsExplicit: c.IsExplicit, + RelatedTable: ResolveRelatedTable(c, nameCache))) .OrderBy(c => c.ComponentType) .ThenBy(c => c.Name) .ToList(); @@ -184,13 +194,16 @@ private async Task> QuerySolutionComponentsAsync(List> BuildNameCacheAsync( + private async Task> BuildNameCacheAsync( List components, Dictionary? entityNameLookup, Dictionary? attributeNameLookup, - Dictionary? relationshipNameLookup) + Dictionary? relationshipNameLookup, + Dictionary? attributeEntityLookup, + Dictionary? relationshipEntityLookup, + Dictionary? keyEntityLookup) { - var cache = new Dictionary<(int, Guid), (string Name, string SchemaName)>(); + var cache = new Dictionary<(int, Guid), (string Name, string SchemaName, string? RelatedTable)>(); // Group components by type for batch queries var componentsByType = components @@ -206,7 +219,7 @@ private async Task> QuerySolutionComponentsAsync(List> QuerySolutionComponentsAsync(List> QuerySolutionComponentsAsync(List> QuerySolutionComponentsAsync(List> QuerySolutionComponentsAsync(List> QueryComponentNamesAsync(string tableName, string nameColumn, string primaryKey, List objectIds) + private async Task> QueryComponentNamesWithEntityAsync( + string tableName, string nameColumn, string primaryKey, string? entityColumn, List objectIds) { - var result = new Dictionary(); + var result = new List<(Guid, string, string?)>(); if (!objectIds.Any()) return result; try { + var columns = new List { primaryKey, nameColumn }; + if (!string.IsNullOrEmpty(entityColumn)) + { + columns.Add(entityColumn); + } + var query = new QueryExpression(tableName) { - ColumnSet = new ColumnSet(primaryKey, nameColumn), + ColumnSet = new ColumnSet(columns.ToArray()), Criteria = new FilterExpression(LogicalOperator.And) { Conditions = @@ -287,7 +320,25 @@ private async Task> QueryComponentNamesAsync(string tab { var id = entity.GetAttributeValue(primaryKey); var name = entity.GetAttributeValue(nameColumn) ?? id.ToString(); - result[id] = name; + string? relatedTable = null; + + if (!string.IsNullOrEmpty(entityColumn) && entity.Contains(entityColumn)) + { + // The entity column can be a string (logical name) or an int (object type code) + var entityValue = entity[entityColumn]; + if (entityValue is string strValue) + { + relatedTable = strValue; + } + else if (entityValue is int intValue) + { + // Object type code - we'd need entity metadata to resolve this + // For now, just store the numeric value as string + relatedTable = intValue.ToString(); + } + } + + result.Add((id, name, relatedTable)); } } catch (Exception ex) @@ -299,7 +350,7 @@ private async Task> QueryComponentNamesAsync(string tab return result; } - private string ResolveComponentName(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName)> cache) + private string ResolveComponentName(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName, string? RelatedTable)> cache) { if (cache.TryGetValue((component.ComponentType, component.ObjectId), out var names)) { @@ -308,7 +359,7 @@ private string ResolveComponentName(RawComponentInfo component, Dictionary<(int, return component.ObjectId.ToString(); } - private string ResolveComponentSchemaName(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName)> cache) + private string ResolveComponentSchemaName(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName, string? RelatedTable)> cache) { if (cache.TryGetValue((component.ComponentType, component.ObjectId), out var names)) { @@ -317,5 +368,19 @@ private string ResolveComponentSchemaName(RawComponentInfo component, Dictionary return component.ObjectId.ToString(); } + private string? ResolveRelatedTable(RawComponentInfo component, Dictionary<(int, Guid), (string Name, string SchemaName, string? RelatedTable)> cache) + { + if (!ComponentTypesWithRelatedTable.Contains(component.ComponentType)) + { + return null; + } + + if (cache.TryGetValue((component.ComponentType, component.ObjectId), out var names)) + { + return names.RelatedTable; + } + return null; + } + private record RawComponentInfo(int ComponentType, Guid ObjectId, Guid SolutionId, bool IsExplicit); } diff --git a/Website/components/insightsview/solutions/InsightsSolutionView.tsx b/Website/components/insightsview/solutions/InsightsSolutionView.tsx index 0d74b2f..c384a66 100644 --- a/Website/components/insightsview/solutions/InsightsSolutionView.tsx +++ b/Website/components/insightsview/solutions/InsightsSolutionView.tsx @@ -26,6 +26,28 @@ const getComponentTypeLabel = (type: SolutionComponentTypeEnum): string => { return ComponentTypeLabels[type] || `Unknown (${type})`; }; +// Component types that should show the related table as a prefix +const ComponentTypesWithRelatedTable = new Set([ + SolutionComponentTypeEnum.Attribute, // Column + SolutionComponentTypeEnum.Relationship, // Relationship + SolutionComponentTypeEnum.SystemForm, // Form + SolutionComponentTypeEnum.EntityKey, // Key + SolutionComponentTypeEnum.SavedQuery, // View +]); + +// Helper to check if component has related table info +const hasRelatedTable = (comp: SolutionComponentDataType): boolean => { + return ComponentTypesWithRelatedTable.has(comp.ComponentType) && !!comp.RelatedTable; +}; + +// Helper to get sort key for components (related table + name for applicable types) +const getComponentSortKey = (comp: SolutionComponentDataType): string => { + if (ComponentTypesWithRelatedTable.has(comp.ComponentType) && comp.RelatedTable) { + return `${comp.RelatedTable}\0${comp.Name}`; + } + return comp.Name; +}; + // Get all types that are in any category (known/mapped types) const getAllCategorizedTypes = (): Set => { const allTypes = new Set(); @@ -191,9 +213,9 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { grouped[label].push(comp); }); - // Sort each group by name + // Sort each group by related table (if applicable) then by name Object.keys(grouped).forEach(key => { - grouped[key].sort((a, b) => a.Name.localeCompare(b.Name)); + grouped[key].sort((a, b) => getComponentSortKey(a).localeCompare(getComponentSortKey(b))); }); return grouped; @@ -326,8 +348,8 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { componentsArray = componentsArray.filter(c => c.solutions.length > 1); } - // Sort alphabetically by component name - componentsArray.sort((a, b) => a.component.Name.localeCompare(b.component.Name)); + // Sort by related table (if applicable) then by component name + componentsArray.sort((a, b) => getComponentSortKey(a.component).localeCompare(getComponentSortKey(b.component))); const sharedCount = componentsArray.filter(c => c.solutions.length > 1).length; @@ -694,6 +716,11 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { {comps.map(comp => (
  • + {hasRelatedTable(comp) && ( + + {comp.RelatedTable}: + + )} {comp.Name}
  • @@ -817,6 +844,11 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { > + {hasRelatedTable(component) && ( + + {component.RelatedTable}: + + )} {component.Name} diff --git a/Website/lib/Types.ts b/Website/lib/Types.ts index 9583434..07952a4 100644 --- a/Website/lib/Types.ts +++ b/Website/lib/Types.ts @@ -102,6 +102,7 @@ export type SolutionComponentDataType = { ComponentType: SolutionComponentTypeEnum; ObjectId: string; IsExplicit: boolean; + RelatedTable?: string | null; } /// Collection of solution components grouped by solution From b2e519c6225794402dcbeca9b1fddb5467a62a62 Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:38:10 +0100 Subject: [PATCH 12/14] fix(website): use const for non-reassigned variable Co-Authored-By: Claude Opus 4.5 --- .../components/insightsview/solutions/InsightsSolutionView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Website/components/insightsview/solutions/InsightsSolutionView.tsx b/Website/components/insightsview/solutions/InsightsSolutionView.tsx index c384a66..95fff94 100644 --- a/Website/components/insightsview/solutions/InsightsSolutionView.tsx +++ b/Website/components/insightsview/solutions/InsightsSolutionView.tsx @@ -601,7 +601,7 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { const i = solutionMatrix.solutionNames.indexOf(solution1); const j = solutionMatrix.solutionNames.indexOf(solution2); - let typeBreakdown: Record = {}; + const typeBreakdown: Record = {}; if (solution1 !== solution2 && i !== -1 && j !== -1) { const matrixIndex = i * solutionMatrix.solutionNames.length + j; const sharedComponents = solutionMatrix.matrix[matrixIndex].sharedComponents; From bc196d663871bc5da07df7570c1736f1b294f652 Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:47:13 +0100 Subject: [PATCH 13/14] removed tasks for development --- .../TypesPerSolutionOverview.md | 3 - .../TypesToSolutionsOverview-Plan.md | 326 ------------------ .../SolutionRelationsFullOverview-Plan.md | 287 --------------- .../SolutionRelationsFullOverview.md | 62 ---- .../SolutionRelationsTypesSharedHover.md | 2 - .../SolutionSummaryTreeView.md | 10 - 6 files changed, 690 deletions(-) delete mode 100644 Tasks/Done/InsightsComponentsOverview/TypesPerSolutionOverview.md delete mode 100644 Tasks/Done/InsightsComponentsOverview/TypesToSolutionsOverview-Plan.md delete mode 100644 Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview-Plan.md delete mode 100644 Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview.md delete mode 100644 Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsTypesSharedHover.md delete mode 100644 Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionSummaryTreeView.md diff --git a/Tasks/Done/InsightsComponentsOverview/TypesPerSolutionOverview.md b/Tasks/Done/InsightsComponentsOverview/TypesPerSolutionOverview.md deleted file mode 100644 index ef83b20..0000000 --- a/Tasks/Done/InsightsComponentsOverview/TypesPerSolutionOverview.md +++ /dev/null @@ -1,3 +0,0 @@ -- 5231: Types To Solutions Overview - - Must answer the question "Which solutions have plugin assemblies, and which plugins are in each solution" - - each type -> what solutions they are in -> the specific names. \ No newline at end of file diff --git a/Tasks/Done/InsightsComponentsOverview/TypesToSolutionsOverview-Plan.md b/Tasks/Done/InsightsComponentsOverview/TypesToSolutionsOverview-Plan.md deleted file mode 100644 index e2db3e6..0000000 --- a/Tasks/Done/InsightsComponentsOverview/TypesToSolutionsOverview-Plan.md +++ /dev/null @@ -1,326 +0,0 @@ -# Plan: Types to Solutions Overview - -## Summary - -Add a new section to the Solution Insights page (`/insights?view=solutions`) that answers the question: **"Which solutions contain each component type, and what specific components are in each?"** - -This is the inverse of the existing heatmap view: -- **Heatmap (existing)**: Solution → Solution intersection of components -- **Types Overview (new)**: Component Type → Solutions → Specific Components - -Example use case: "Which solutions have plugin assemblies, and which plugins are in each solution?" - ---- - -## User Story - -As a Dataverse administrator, I want to see: -1. A list of all component types that exist in my solutions -2. For each component type, which solutions contain that type -3. For each solution, what specific components of that type it contains - -**Visual hierarchy**: `Component Type → Solutions → Component Names` - ---- - -## UI Design - -### Layout - -New section below the existing heatmap and summary panel, occupying full width. - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Component Types Overview [▼ / ▲] │ -│ See which solutions contain each component type │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ▼ Plugin Assembly (3 solutions, 12 components) │ -│ ├─ ▼ CRM Core Solution (5) │ -│ │ • AccountPlugin │ -│ │ • ContactPlugin │ -│ │ • LeadPlugin │ -│ │ • OpportunityPlugin │ -│ │ • CasePlugin │ -│ │ │ -│ ├─ ▼ Integration Solution (4) │ -│ │ • ERPSyncPlugin │ -│ │ • WebhookPlugin │ -│ │ • DataExportPlugin │ -│ │ • ImportPlugin │ -│ │ │ -│ └─ ▶ Marketing Solution (3) [collapsed] │ -│ │ -│ ▶ Cloud Flow (5 solutions, 47 components) [collapsed] │ -│ │ -│ ▼ Canvas App (2 solutions, 4 components) │ -│ ├─ ▶ Sales Portal (2) [collapsed] │ -│ └─ ▶ Service Desk (2) [collapsed] │ -│ │ -│ ▶ Model-driven App (4 solutions, 8 components) [collapsed] │ -│ │ -│ ▶ Custom API (1 solution, 3 components) [collapsed] │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### Interaction - -1. **Component Type Level**: Click to expand/collapse all solutions for that type -2. **Solution Level**: Click to expand/collapse specific components in that solution -3. **Sorting**: Types sorted by total component count (descending) - most used types first -4. **Empty Types**: Only show types that have at least one component in the data - -### Features - -- **Section collapsible**: Entire section can be collapsed to reduce visual clutter -- **Expand/Collapse All**: Buttons to expand or collapse all types and solutions -- **Search/Filter** (optional enhancement): Filter by type name or solution name -- **Respects filter panel**: Uses same `enabledComponentTypes` filter as the heatmap - ---- - -## Implementation Steps - -### Step 1: Add Collapsed State for Section - -Add state to track if the entire "Types Overview" section is expanded/collapsed. - -```typescript -const [typesOverviewExpanded, setTypesOverviewExpanded] = useState(true); -``` - -### Step 2: Create Data Structure - -Build a hierarchical data structure from the existing `solutionComponents`: - -```typescript -type TypeToSolutionsData = { - componentType: SolutionComponentTypeEnum; - typeLabel: string; - totalCount: number; - solutions: { - solutionName: string; - components: SolutionComponentDataType[]; - }[]; -}[]; -``` - -### Step 3: Create `typesToSolutions` useMemo - -```typescript -const typesToSolutions = useMemo(() => { - // 1. Group all components by type - // 2. For each type, group by solution - // 3. Filter by enabledComponentTypes - // 4. Sort by total count descending -}, [solutionComponents, enabledComponentTypes]); -``` - -### Step 4: Create Collapse State for Types and Solutions - -```typescript -// Track which types are collapsed -const [collapsedTypes, setCollapsedTypes] = useState>(new Set()); - -// Track which solutions within each type are collapsed (key: "type-solutionName") -const [collapsedTypeSolutions, setCollapsedTypeSolutions] = useState>(new Set()); -``` - -### Step 5: Implement Smart Defaults - -- If a type has ≤3 solutions, expand all by default -- If a solution has ≤5 components, expand by default -- Otherwise, start collapsed - -### Step 6: Render UI - -Use MUI components: -- `Paper` for container -- `Collapse` for expand/collapse animation -- `Box` with `onClick` for clickable headers -- `ExpandMore/ExpandLess` icons for visual indicators -- Nested `Box` for indentation - ---- - -## Files to Modify - -| File | Change | -|------|--------| -| `InsightsSolutionView.tsx` | Add new section with types overview UI | - -No Generator changes needed - uses existing `solutionComponents` data. - ---- - -## Code Structure - -### New useMemo Hook - -```typescript -const typesToSolutions = useMemo(() => { - // Build map: componentType -> solutionName -> components[] - const typeMap = new Map>(); - - solutionComponents.forEach(collection => { - collection.Components.forEach(comp => { - // Only include enabled types - if (!enabledComponentTypes.has(comp.ComponentType)) return; - - if (!typeMap.has(comp.ComponentType)) { - typeMap.set(comp.ComponentType, new Map()); - } - const solutionMap = typeMap.get(comp.ComponentType)!; - - if (!solutionMap.has(collection.SolutionName)) { - solutionMap.set(collection.SolutionName, []); - } - solutionMap.get(collection.SolutionName)!.push(comp); - }); - }); - - // Convert to array and sort - const result = Array.from(typeMap.entries()) - .map(([type, solutions]) => { - const solutionsArray = Array.from(solutions.entries()) - .map(([name, comps]) => ({ - solutionName: name, - components: comps.sort((a, b) => a.Name.localeCompare(b.Name)) - })) - .sort((a, b) => b.components.length - a.components.length); - - return { - componentType: type, - typeLabel: getComponentTypeLabel(type), - totalCount: solutionsArray.reduce((sum, s) => sum + s.components.length, 0), - solutions: solutionsArray - }; - }) - .sort((a, b) => b.totalCount - a.totalCount); - - return result; -}, [solutionComponents, enabledComponentTypes]); -``` - -### UI Rendering (Pseudocode) - -```tsx - - - {/* Header with expand/collapse */} - - Component Types Overview - - - - setTypesOverviewExpanded(!typesOverviewExpanded)}> - {typesOverviewExpanded ? : } - - - - - - {typesToSolutions.map(typeData => ( - - {/* Type header - clickable */} - toggleType(typeData.componentType)}> - {typeData.typeLabel} - ({typeData.solutions.length} solutions, {typeData.totalCount} components) - - - - {typeData.solutions.map(solution => ( - - {/* Solution header - clickable */} - toggleSolution(typeData.componentType, solution.solutionName)}> - {solution.solutionName} ({solution.components.length}) - - - - - {solution.components.map(comp => ( -
  • {comp.Name}
  • - ))} -
    -
    -
    - ))} -
    -
    - ))} -
    -
    -
    -``` - ---- - -## Smart Expand/Collapse Logic - -### On Initial Load - -```typescript -useEffect(() => { - // Auto-expand types with few solutions - const autoExpanded = new Set(); - const autoCollapsedSolutions = new Set(); - - typesToSolutions.forEach(typeData => { - if (typeData.solutions.length > 3) { - // Collapse types with many solutions - autoExpanded.add(typeData.componentType); - } - - typeData.solutions.forEach(solution => { - if (solution.components.length > 5) { - // Collapse solutions with many components - autoCollapsedSolutions.add(`${typeData.componentType}-${solution.solutionName}`); - } - }); - }); - - setCollapsedTypes(autoExpanded); - setCollapsedTypeSolutions(autoCollapsedSolutions); -}, [typesToSolutions]); -``` - ---- - -## Styling Notes - -- Use consistent spacing and indentation with existing UI -- Use `text.secondary` color for counts -- Use `primary.main` color for type and solution names -- Tree-view indentation: 24px per level -- Hover effect on clickable rows - ---- - -## Edge Cases - -1. **No data**: Show "No component data available" message -2. **No enabled types**: Show "Select component types in the filter panel above" -3. **Empty after filter**: Show "No components match the selected filters" -4. **Very long lists**: Consider virtualization if performance becomes an issue (future enhancement) - ---- - -## Testing - -1. Verify types are correctly grouped -2. Verify solutions are correctly nested under types -3. Verify component names appear under solutions -4. Verify expand/collapse works at all levels -5. Verify filter panel affects the overview -6. Verify sorting (most components first) -7. Verify smart expand logic works correctly - ---- - -## Future Enhancements (Out of Scope) - -- Search/filter within the types overview -- Export to CSV/Excel -- Click to navigate to component details -- Highlight shared components (appear in multiple solutions) diff --git a/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview-Plan.md b/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview-Plan.md deleted file mode 100644 index 470ec53..0000000 --- a/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview-Plan.md +++ /dev/null @@ -1,287 +0,0 @@ -# Plan: Extended Solution Insights with Component Type Filtering - -## Summary - -Extend the Solutions Insights page (`/insights?view=solutions`) to show all Dataverse solution component types with toggleable checkbox filtering. Currently only shows Entity, Attribute, and Relationship - will expand to 40+ component types. - -## User Decisions - -- **Default Filter State**: Only Entity, Attribute, Relationship enabled on load (backwards compatible) -- **Filter Panel**: Collapsed by default (user expands to see all options) -- **Component Types**: Include ALL types from the reference list - ---- - -## Component Types to Support (Full List) - -| Type | Code | Category | Label | -|------|------|----------|-------| -| Entity | 1 | Data Model | Table | -| Attribute | 2 | Data Model | Column | -| OptionSet | 9 | Data Model | Choice | -| Relationship | 10 | Data Model | Relationship | -| EntityKey | 14 | Data Model | Key | -| SecurityRole | 20 | Security | Security Role | -| SavedQuery | 26 | User Interface | View | -| Workflow | 29 | Code | Cloud Flow | -| RibbonCustomization | 50 | User Interface | Ribbon | -| SavedQueryVisualization | 59 | User Interface | Chart | -| SystemForm | 60 | User Interface | Form | -| WebResource | 61 | Code | Web Resource | -| SiteMap | 62 | User Interface | Site Map | -| ConnectionRole | 63 | Configuration | Connection Role | -| HierarchyRule | 65 | Data Model | Hierarchy Rule | -| CustomControl | 66 | User Interface | Custom Control | -| FieldSecurityProfile | 70 | Security | Field Security | -| ModelDrivenApp | 80 | Apps | Model-driven App | -| PluginAssembly | 91 | Code | Plugin Assembly | -| SDKMessageProcessingStep | 92 | Code | Plugin Step | -| CanvasApp | 300 | Apps | Canvas App | -| ConnectionReference | 372 | Configuration | Connection Reference | -| EnvironmentVariableDefinition | 380 | Configuration | Environment Variable | -| EnvironmentVariableValue | 381 | Configuration | Environment Variable Value | -| Dataflow | 418 | Data Model | Dataflow | -| ConnectionRoleObjectTypeCode | 3233 | Configuration | Connection Role Object Type | -| CustomAPI | 10240 | Code | Custom API | -| CustomAPIRequestParameter | 10241 | Code | Custom API Request Parameter | -| CustomAPIResponseProperty | 10242 | Code | Custom API Response Property | -| PluginPackage | 10639 | Code | Plugin Package | -| OrganizationSetting | 10563 | Configuration | Organization Setting | -| AppAction | 10645 | Apps | App Action | -| AppActionRule | 10948 | Apps | App Action Rule | -| FxExpression | 11492 | Code | Fx Expression | -| DVFileSearch | 11723 | Data Model | DV File Search | -| DVFileSearchAttribute | 11724 | Data Model | DV File Search Attribute | -| DVFileSearchEntity | 11725 | Data Model | DV File Search Entity | -| AISkillConfig | 12075 | Configuration | AI Skill Config | - -**Note**: High-numbered types (10000+) are environment-specific and may not exist in all Dataverse environments. The Generator will handle missing types gracefully. - ---- - -## Architecture Decision - -**New Data Structure**: Create a separate `SolutionComponents` export in Data.ts rather than extending entity-based structure. This allows non-entity components (apps, plugins, flows) to be tracked. - -**Data Flow**: -``` -Dataverse solutioncomponent table - → Generator SolutionComponentExtractor (NEW) - → Data.ts SolutionComponents export (NEW) - → Website DatamodelDataContext - → InsightsSolutionView with filter checkboxes -``` - -### Relationship to Existing SolutionComponentService - -The **existing `SolutionComponentService`** serves a different purpose and will NOT be replaced: - -| Aspect | SolutionComponentService (KEEP) | SolutionComponentExtractor (NEW) | -|--------|--------------------------------|----------------------------------| -| **Purpose** | Determines which entities/attributes/relationships to include in extraction | Provides component list for insights visualization | -| **Used By** | DataverseService for filtering metadata extraction | WebsiteBuilder for Data.ts export | -| **Output** | ComponentInfo with solution mapping for entity metadata | SolutionComponentCollection for insights page | -| **Scope** | Only types 1, 2, 10, 20, 62 (entity-centric) | All 38+ component types | - -The two services are complementary: -- `SolutionComponentService` → "Which entities should we extract metadata for?" -- `SolutionComponentExtractor` → "What components exist in each solution for visualization?" - ---- - -## Implementation Steps - -### Phase 1: Generator Changes - -#### 1.1 Create/Update DTOs -**File:** `Generator/DTO/SolutionComponent.cs` - -- Extend `SolutionComponentType` enum with all 38+ types -- Add `SolutionComponentData` record (Name, SchemaName, ComponentType, ObjectId, IsExplicit) -- Add `SolutionComponentCollection` record (SolutionId, SolutionName, Components[]) - -#### 1.2 Create SolutionComponentExtractor Service -**File:** `Generator/Services/SolutionComponentExtractor.cs` (NEW) - -- Query solutioncomponent table for all supported component types -- Group results by solution -- Resolve display names by querying respective tables - -#### 1.3 Update WebsiteBuilder -**File:** `Generator/WebsiteBuilder.cs` - -- Accept `SolutionComponentCollection[]` parameter -- Add new export: `export let SolutionComponents: SolutionComponentCollectionType[] = [...]` - -#### 1.4 Update DataverseService -**File:** `Generator/DataverseService.cs` - -- Inject and call SolutionComponentExtractor -- Pass results to WebsiteBuilder - ---- - -### Phase 2: Website Changes - -#### 2.1 Update Types -**File:** `Website/lib/Types.ts` - -- Extend `SolutionComponentTypeEnum` with all types (matching Dataverse codes) -- Add `SolutionComponentDataType` and `SolutionComponentCollectionType` -- Add `ComponentTypeCategories` constant (groups types by category for UI) -- Add `ComponentTypeLabels` constant (human-readable names) - -#### 2.2 Update Data Loading -**Files:** -- `Website/components/datamodelview/dataLoaderWorker.ts` -- `Website/contexts/DatamodelDataContext.tsx` - -- Import and expose `SolutionComponents` from Data.ts -- Add to context state and dispatch - -#### 2.3 Update InsightsSolutionView -**File:** `Website/components/insightsview/solutions/InsightsSolutionView.tsx` - -- Add `enabledComponentTypes` state (Set of enabled types) -- Add collapsible filter panel with checkboxes grouped by category -- Add "Select All" / "Select None" buttons -- Update `solutions` useMemo to filter by enabled types -- Update summary panel to group components by type (TREE VIEW - see follow-up task) - ---- - -## Files to Modify - -| File | Change Type | -|------|-------------| -| `Generator/DTO/SolutionComponent.cs` | Extend | -| `Generator/Services/SolutionComponentExtractor.cs` | Create | -| `Generator/WebsiteBuilder.cs` | Extend | -| `Generator/DataverseService.cs` | Extend | -| `Generator/Program.cs` | Register new service | -| `Website/lib/Types.ts` | Extend | -| `Website/components/datamodelview/dataLoaderWorker.ts` | Extend | -| `Website/contexts/DatamodelDataContext.tsx` | Extend | -| `Website/components/insightsview/solutions/InsightsSolutionView.tsx` | Major update | - ---- - -## UI Design - -### Filter Panel (collapsible, above heatmap) -``` -┌─────────────────────────────────────────────────────────────┐ -│ Component Type Filters [Select All] [None] [▼] │ -│ 6 component type(s) selected │ -├─────────────────────────────────────────────────────────────┤ -│ Data Model User Interface Apps │ -│ ☑ Table ☐ Form ☐ Model-driven App │ -│ ☑ Column ☐ View ☐ Canvas App │ -│ ☑ Relationship ☐ Site Map │ -│ ☐ Choice ☐ Custom Control Code │ -│ ☐ Key ☐ Cloud Flow │ -│ Security ☐ Plugin Assembly │ -│ Configuration ☐ Security Role ☐ Plugin Step │ -│ ☐ Env Variable ☐ Field Security ☐ Web Resource │ -│ ☐ Connection Ref │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Summary Panel - Tree View Structure (Follow-up Task #5823) -``` -Shared Components: (42) - -▼ Table (5) - Account - Contact - Lead - Opportunity - Case - -▼ Column (30) - firstname - lastname - emailaddress1 - ... - -▼ Cloud Flow (7) - When Contact Created - Sync to ERP - ... -``` - ---- - -## Notes - -- **Breaking Change**: `SolutionComponentTypeEnum.Relationship` changes from 3 to 10 to match Dataverse. Only affects InsightsSolutionView (isolated change). -- **Performance**: Filter updates trigger useMemo recalculation - should remain fast with memoization. -- **Fallback**: If a component type query fails (e.g., Canvas Apps in older environments), skip gracefully with logging. -- **SolutionComponentService**: NOT replaced - serves different purpose (entity filtering vs insights visualization). - ---- - -## Implementation Phases - -### Phase 1: Generator (Do First) -1. Extend `SolutionComponentType` enum in `Generator/DTO/SolutionComponent.cs` -2. Create `SolutionComponentExtractor` service in `Generator/Services/` -3. Update `WebsiteBuilder.cs` to export new `SolutionComponents` array -4. Update `DataverseService.cs` to call extractor -5. Register service in `Program.cs` -6. **TEST**: Run Generator against test environment, verify Data.ts output - -### Phase 2: Website (Do Second) -1. Update `Website/lib/Types.ts` with new types and labels -2. Update `dataLoaderWorker.ts` to load SolutionComponents -3. Update `DatamodelDataContext.tsx` to expose solutionComponents -4. Update `InsightsSolutionView.tsx` with filter UI and logic -5. **TEST**: Run website, verify heatmap works with filtering - ---- - -## Related Follow-up Tasks - -- **Task #5823**: Solution summary → treeview of all component types "enabled" - - The summary panel will be structured as a tree view grouped by component type - - This is built into this implementation - summary panel will show components grouped by type - ---- - -## Name Resolution Tables (for Generator) - -Each component type needs its display name resolved from different Dataverse tables: - -| ComponentType | Query Table | Name Column | -|---------------|-------------|-------------| -| Entity (1) | entity (via metadata API) | LogicalName/DisplayName | -| Attribute (2) | attribute (via metadata API) | LogicalName/DisplayName | -| OptionSet (9) | optionset (via metadata API) | Name | -| Relationship (10) | relationship (via metadata API) | SchemaName | -| EntityKey (14) | entitykey (via metadata API) | LogicalName | -| SecurityRole (20) | role | name | -| SavedQuery (26) | savedquery | name | -| Workflow (29) | workflow | name | -| RibbonCustomization (50) | ribboncustomization | entity | -| SavedQueryVisualization (59) | savedqueryvisualization | name | -| SystemForm (60) | systemform | name | -| WebResource (61) | webresource | name | -| SiteMap (62) | sitemap | sitemapname | -| ConnectionRole (63) | connectionrole | name | -| HierarchyRule (65) | hierarchyrule | name | -| CustomControl (66) | customcontrol | name | -| FieldSecurityProfile (70) | fieldsecurityprofile | name | -| ModelDrivenApp (80) | appmodule | name | -| PluginAssembly (91) | pluginassembly | name | -| SDKMessageProcessingStep (92) | sdkmessageprocessingstep | name | -| CanvasApp (300) | canvasapp | name | -| ConnectionReference (372) | connectionreference | connectionreferencedisplayname | -| EnvironmentVariableDefinition (380) | environmentvariabledefinition | displayname | -| EnvironmentVariableValue (381) | environmentvariablevalue | schemaname | -| Dataflow (418) | workflow (category=6) | name | -| CustomAPI (10240) | customapi | name | -| CustomAPIRequestParameter (10241) | customapirequestparameter | name | -| CustomAPIResponseProperty (10242) | customapiresponseproperty | name | -| PluginPackage (10639) | pluginpackage | name | - -**Fallback Strategy**: If name resolution fails for a component, use ObjectId as fallback name. \ No newline at end of file diff --git a/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview.md b/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview.md deleted file mode 100644 index 47c5cde..0000000 --- a/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsFullOverview.md +++ /dev/null @@ -1,62 +0,0 @@ -Extend the SolutionsInsights in insights Section. Add the capability to see all the related or enabled entity types from all the enabled solutions and make it toggleable which component types to see. - -Tasks: -- analyze requirements and needs to solve problem -- Implement in a clean and effecient manner - -Known Elements: -- New handling of types based on list in bottom of this file. -- checkboxes for which different component types to compare - - all possible types -- Extension of generator and website to handle the new and added types - -Code snippet from other program to reference various component types. - -MIGHT BE INCOMPLETE LIST BELOW - -``` -private static readonly Dictionary ComponentTypeNames = new() - { - { 1, "Entity" }, - { 2, "Attribute" }, - { 9, "OptionSet" }, - { 10, "Relationship" }, - { 14, "Entity Key" }, - { 20, "Security Role" }, - { 26, "SavedQuery (View)" }, - { 29, "Workflow" }, - { 50, "Ribbon Customization" }, - { 59, "Saved Query Visualization" }, - { 60, "SystemForm (Form)" }, - { 61, "Web Resource" }, - { 62, "SiteMap" }, - { 63, "Connection Role" }, - { 65, "Hierarchy Rule" }, - { 66, "Custom Control" }, - { 70, "Field Security Profile" }, - { 80, "Model-driven App" }, - { 91, "Plugin Assembly" }, - { 92, "SDK Message Processing Step" }, - { 300, "Canvas App" }, - { 372, "Connection Reference" }, - { 380, "Environment Variable Definition" }, - { 381, "Environment Variable Value" }, - { 418, "Dataflow" }, - { 3233, "Connection Role Object Type Code" }, - { 10019, "Requirement Resource Preference" }, - { 10020, "Requirement Status" }, - { 10025, "Scheduling Parameter" }, - { 10240, "Custom API" }, - { 10241, "Custom API Request Parameter" }, - { 10242, "Custom API Response Property" }, - { 10639, "Plugin Package" }, - { 10563, "Organization Setting" }, - { 10645, "App Action" }, - { 10948, "App Action Rule" }, - { 11492, "Fx Expression" }, - { 11723, "DV File Search" }, - { 11724, "DV File Search Attribute" }, - { 11725, "DV File Search Entity" }, - { 12075, "AI Skill Config" } - }; -``` \ No newline at end of file diff --git a/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsTypesSharedHover.md b/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsTypesSharedHover.md deleted file mode 100644 index 6dd5416..0000000 --- a/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionRelationsTypesSharedHover.md +++ /dev/null @@ -1,2 +0,0 @@ -- 5412: *mouse over - solution relations datamatrix - - types shared in dialog box \ No newline at end of file diff --git a/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionSummaryTreeView.md b/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionSummaryTreeView.md deleted file mode 100644 index d566ac2..0000000 --- a/Tasks/Done/SolutionInsightsImprovements/MatrixImprovements/SolutionSummaryTreeView.md +++ /dev/null @@ -1,10 +0,0 @@ -Tasks: -- 5823: Solution summary -> treeview of all component types "enabled" - Attributes - |- form, - my-form, - form-a, - form-b - |- view, - view-a, - main-view, \ No newline at end of file From 338c83339a3d8a1cf33188ca6a8690c089b66c4c Mon Sep 17 00:00:00 2001 From: Bircck <55695195+Bircck@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:28:48 +0100 Subject: [PATCH 14/14] fixed error on mising element from data.ts --- Website/stubs/Data.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Website/stubs/Data.ts b/Website/stubs/Data.ts index 367957e..cf66eb0 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, SolutionWarningType } from "@/lib/Types"; +import { GroupType, SolutionWarningType, SolutionComponentCollectionType } from "@/lib/Types"; export const LastSynched: Date = new Date(); export const Logo: string | null = null; @@ -113,4 +113,6 @@ export let Groups: GroupType[] = [ } ]; -export let SolutionWarnings: SolutionWarningType[] = []; \ No newline at end of file +export let SolutionWarnings: SolutionWarningType[] = []; + +export let SolutionComponents: SolutionComponentCollectionType[] = []; \ No newline at end of file