diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..6f280e2 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run dev:*)", + "Bash(dotnet build:*)", + "Bash(npm run build:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/Generator/DTO/Attributes/Attribute.cs b/Generator/DTO/Attributes/Attribute.cs index fe5ba77..221ce63 100644 --- a/Generator/DTO/Attributes/Attribute.cs +++ b/Generator/DTO/Attributes/Attribute.cs @@ -7,6 +7,7 @@ public abstract class Attribute public bool IsStandardFieldModified { get; set; } public bool IsCustomAttribute { get; set; } public bool IsPrimaryId { get; set; } + public bool IsPrimaryName { get; set; } public List AttributeUsages { get; set; } = new List(); public string DisplayName { get; } public string SchemaName { get; } @@ -20,6 +21,7 @@ public abstract class Attribute protected Attribute(AttributeMetadata metadata) { IsPrimaryId = metadata.IsPrimaryId ?? false; + IsPrimaryName = metadata.IsPrimaryName ?? false; IsCustomAttribute = metadata.IsCustomAttribute ?? false; DisplayName = metadata.DisplayName.UserLocalizedLabel?.Label ?? string.Empty; SchemaName = metadata.SchemaName; diff --git a/Generator/DataverseService.cs b/Generator/DataverseService.cs index 4886709..1032095 100644 --- a/Generator/DataverseService.cs +++ b/Generator/DataverseService.cs @@ -339,7 +339,7 @@ private static Record MakeRecord( .Select(metadata => { var attr = GetAttribute(metadata, entity, logicalToSchema, attributeUsages, logger); - attr.IsStandardFieldModified = MetadataExtensions.StandardFieldHasChanged(metadata, entity.DisplayName.UserLocalizedLabel?.Label ?? string.Empty); + attr.IsStandardFieldModified = MetadataExtensions.StandardFieldHasChanged(metadata, entity.DisplayName.UserLocalizedLabel?.Label ?? string.Empty, entity.IsCustomEntity ?? false); return attr; }) .Where(x => !string.IsNullOrEmpty(x.DisplayName)) diff --git a/Generator/MetadataExtensions.cs b/Generator/MetadataExtensions.cs index 3577e4d..6c081eb 100644 --- a/Generator/MetadataExtensions.cs +++ b/Generator/MetadataExtensions.cs @@ -20,15 +20,25 @@ internal static string PrettyDescription(this string description) => .Replace("\"", @"ā€") .Replace("\n", " "); - public static bool StandardFieldHasChanged(this AttributeMetadata attribute, string entityDisplayName) + public static bool StandardFieldHasChanged(this AttributeMetadata attribute, string entityDisplayName, bool isCustomEntity) { if (attribute.IsCustomAttribute ?? false) return false; var languagecode = attribute.DisplayName.UserLocalizedLabel?.LanguageCode; var fields = GetDefaultFields(entityDisplayName, languagecode); - return fields.StandardDescriptionHasChanged(attribute.LogicalName, attribute.Description.UserLocalizedLabel?.Label ?? string.Empty) + var hasTextChanged = fields.StandardDescriptionHasChanged(attribute.LogicalName, attribute.Description.UserLocalizedLabel?.Label ?? string.Empty) || fields.StandardDisplayNameHasChanged(attribute.LogicalName, attribute.DisplayName.UserLocalizedLabel?.Label ?? string.Empty); + + // Check if options have been added to statecode or statuscode + var hasOptionsChanged = attribute switch + { + StateAttributeMetadata state => StateCodeOptionsHaveChanged(state), + StatusAttributeMetadata status => StatusCodeOptionsHaveChanged(status), + _ => false + }; + + return hasTextChanged || hasOptionsChanged; } private static bool StandardDisplayNameHasChanged(this IEnumerable<(string LogicalName, string DisplayName, string Description)> fields, string logicalName, string displayName) @@ -45,6 +55,16 @@ private static bool StandardDescriptionHasChanged(this IEnumerable<(string Logic .Any(f => description.Equals(f.Description)); } + private static bool StateCodeOptionsHaveChanged(StateAttributeMetadata state) + { + return state.OptionSet?.Options?.Count > 2; + } + + private static bool StatusCodeOptionsHaveChanged(StatusAttributeMetadata status) + { + return status.OptionSet?.Options?.Count > 2; + } + private static IEnumerable<(string LogicalName, string DisplayName, string Description)> GetDefaultFields(string entityDisplayName, int? languageCode) { switch (languageCode) diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md new file mode 100644 index 0000000..b58a170 --- /dev/null +++ b/PROJECT_CONTEXT.md @@ -0,0 +1,734 @@ +# Data Model Viewer - Project Context + +> **Generated**: 2025-11-06 +> **Version**: 2.2.2 +> **Purpose**: Comprehensive reference for understanding the Data Model Viewer codebase + +## šŸ“‹ Quick Summary + +**Data Model Viewer** is an enterprise web application that visualizes Microsoft Dataverse (Dynamics 365) metadata. It provides interactive entity relationship diagrams, comprehensive metadata browsing, and analytics for understanding data models and security configurations. + +**Architecture**: Hybrid C#/.NET generator + Next.js 15 React frontend + +## šŸ—ļø Architecture Overview + +``` +ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” +│ USER INTERACTION │ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ Next.js 15 Frontend │ + │ (React 19 + TypeScript) │ + │ │ + │ • JointJS Diagrams │ + │ • MUI Components │ + │ • Tailwind CSS 4 │ + │ • Web Workers (routing) │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ generated/Data.ts │ + │ (1.5MB metadata) │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ C# .NET 8 Generator │ + │ │ + │ • Dataverse SDK │ + │ • Azure Identity │ + │ • Plugin Analysis │ + │ • Flow Analysis │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + │ + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + │ Microsoft Dataverse │ + │ (Dynamics 365) │ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +## šŸ—‚ļø Directory Structure + +``` +DataModelViewer/ +ā”œā”€ā”€ Generator/ # C# .NET 8 - Metadata extraction & analysis +│ ā”œā”€ā”€ DTO/ # Data Transfer Objects (entities, attributes, etc.) +│ ā”œā”€ā”€ Services/ # Plugin, Flow, WebResource analyzers +│ ā”œā”€ā”€ Queries/ # Dataverse query definitions +│ ā”œā”€ā”€ DataverseService.cs # Main Dataverse interaction +│ ā”œā”€ā”€ WebsiteBuilder.cs # Generates Data.ts +│ └── Program.cs # Entry point +│ +ā”œā”€ā”€ Website/ # Next.js 15 - Frontend application +│ ā”œā”€ā”€ app/ # Next.js App Router +│ │ ā”œā”€ā”€ api/ # API routes (auth, diagram CRUD) +│ │ ā”œā”€ā”€ diagram/ # Diagram editor page +│ │ ā”œā”€ā”€ metadata/ # Entity metadata viewer +│ │ ā”œā”€ā”€ insights/ # Analytics pages +│ │ └── processes/ # Process explorer +│ │ +│ ā”œā”€ā”€ components/ # React components +│ │ ā”œā”€ā”€ diagramview/ # Diagram editor (JointJS integration) +│ │ ā”œā”€ā”€ datamodelview/ # Metadata browser +│ │ ā”œā”€ā”€ insightsview/ # Analytics components +│ │ └── shared/ # Shared UI components +│ │ +│ ā”œā”€ā”€ contexts/ # React Context providers +│ │ ā”œā”€ā”€ AuthContext.tsx # Session management +│ │ ā”œā”€ā”€ DatamodelDataContext.tsx # Metadata loading +│ │ ā”œā”€ā”€ DiagramViewContext.tsx # Diagram state +│ │ └── SettingsContext.tsx # User preferences +│ │ +│ ā”œā”€ā”€ lib/ # Utilities and services +│ │ ā”œā”€ā”€ diagram/ # Diagram serialization, helpers +│ │ ā”œā”€ā”€ Types.ts # Core type definitions +│ │ └── session.ts # JWT session management +│ │ +│ ā”œā”€ā”€ generated/ # Generated by C# Generator +│ │ └── Data.ts # Entity metadata (1.5MB) +│ │ +│ └── middleware.ts # Authentication middleware +│ +ā”œā”€ā”€ Infrastructure/ # Azure Bicep (App Service deployment) +└── azure-pipelines-*.yml # CI/CD pipelines +``` + +## šŸ”‘ Key Technologies + +### Backend (Generator) +- **.NET 8.0** with C# 13 +- **Microsoft.PowerPlatform.Dataverse.Client** (v1.2.2) - Dataverse connectivity +- **Azure.Identity** (v1.13.1) - Authentication via DefaultAzureCredential +- **System.Linq.Dynamic.Core** - Dynamic queries + +### Frontend (Website) +- **Next.js 15** with App Router + **React 19** + **TypeScript 5** +- **JointJS (@joint/core v4.1.3)** - Interactive diagram editor +- **libavoid-js (v0.4.5)** - Orthogonal routing via WebAssembly +- **MUI v7** - Material Design components +- **Tailwind CSS 4** - Utility-first styling (new @layer syntax) +- **Nivo** - Data visualization charts +- **jose** - JWT session management + +### Infrastructure +- **Azure App Service** (Node 24 LTS on Linux) +- **Azure Managed Identity** - Production authentication +- **Azure DevOps Git** - Diagram storage & versioning +- **Azure Bicep** - Infrastructure as Code + +## šŸŽÆ Core Workflows + +### 1. Data Generation Flow + +``` +appsettings.local.json + ↓ +DataverseService (C#) + ↓ [Azure DefaultAzureCredential] +Microsoft Dataverse + ↓ [Metadata queries] +DataverseService.GetFilteredMetadata() + ↓ [Filter by solutions] +PluginAnalyzer, FlowAnalyzer, WebResourceAnalyzer + ↓ +WebsiteBuilder.AddData() + ↓ +Website/generated/Data.ts (TypeScript) +``` + +**Generated Data Structure:** +```typescript +export const EntityGroups: Map +export const Warnings: SolutionWarningsList[] +export const SolutionComponents: SolutionComponentList[] +export const timestamp: string +export const logo: string | null +``` + +### 2. Diagram Editing Flow + +``` +User adds entity + ↓ +DiagramViewContext.addEntity() + ↓ +createEntity() → EntityElement (JointJS) + ↓ +graph.addCell(entityElement) + ↓ +AvoidRouter detects change + ↓ +Web Worker calculates routes (libavoid WASM) + ↓ +Main thread receives route vertices + ↓ +Updates RelationshipLink vertices + ↓ +User saves → Serializes to JSON + ↓ +POST /api/diagram/save + ↓ +AzureDevOpsService.createFile() + ↓ +Azure DevOps Git Commit +``` + +### 3. Authentication Flow + +``` +User visits protected route + ↓ +middleware.ts intercepts + ↓ +Checks JWT cookie (encrypted with jose) + ↓ +If invalid → redirect to /login + ↓ +User enters password + ↓ +POST /api/auth/login + ↓ +Validates against WebsitePassword env var + ↓ +Creates JWT session cookie (httpOnly, secure) + ↓ +Redirects to home +``` + +## šŸ“Š Data Models + +### Core Types ([lib/Types.ts](Website/lib/Types.ts)) + +```typescript +// Main entity structure +EntityType { + DisplayName: string + SchemaName: string + Group: string | null // e.g., "Core", "Custom" + Ownership: "User" | "Organization" | "Team" | "None" + IsActivity: boolean + IsCustom: boolean + Attributes: AttributeType[] // 10+ polymorphic types + Relationships: RelationshipType[] + SecurityRoles: SecurityRole[] + Keys: Key[] +} + +// Polymorphic attribute union +AttributeType = + | ChoiceAttributeType // Picklist with options + | LookupAttributeType // Foreign key (Targets: string[]) + | StringAttributeType // Text (MaxLength, Format) + | DateTimeAttributeType // Date/Time (Behavior, Format) + | IntegerAttributeType // Number (Min, Max, Format) + | DecimalAttributeType // Decimal/Money (Precision) + | BooleanAttributeType // Two-option (TrueLabel, FalseLabel) + | StatusAttributeType // Status with linked State + | FileAttributeType // File/Image (MaxSize) + | GenericAttributeType // Fallback + +// Relationships +RelationshipType { + Name: string // Display name + RelationshipSchema: string // Unique identifier + TableSchema: string // Related entity + IsManyToMany: boolean + CascadeConfiguration: { // Delete behavior + Delete: "Cascade" | "RemoveLink" | "Restrict" | null + Merge: ... + Reparent: ... + } +} + +// Security +SecurityRole { + Name: string + Create/Read/Write/Delete: PrivilegeDepth | null + Append/AppendTo/Assign/Share: PrivilegeDepth | null +} + +PrivilegeDepth = "None" | "Basic" | "Local" | "Deep" | "Global" +``` + +### Diagram Models ([lib/diagram/models/](Website/lib/diagram/models/)) + +```typescript +// Persisted diagram format +SerializedDiagram { + name: string + entities: SerializedEntity[] + excludedLinks: ExcludedLinkMetadata[] + zoom: number + translate: { x: number, y: number } +} + +SerializedEntity { + entitySchemaName: string + position: { x: number, y: number } + size: { width: number, height: number } + label?: string // Custom display name +} + +// Relationship metadata stored on links +RelationshipInformation { + relationshipSchemaName: string + lookupDisplayName: string + isManyToMany: boolean + isSourceEntity: boolean // Direction indicator +} +``` + +## šŸŽØ Styling System + +**Tailwind CSS 4** with modern syntax: + +```css +/* globals.css */ +@layer theme, base, mui, components, utilities; +@import 'tailwindcss'; + +@theme { + --breakpoint-md: 56.25rem; + --animate-data-flow: data-flow 2s linear infinite; +} + +@layer theme { + :root { + --layout-sidebar-desktop-width: 72px; + --layout-header-desktop-height: 72px; + } +} +``` + +**MUI Integration**: +- Uses AppRouterCacheProvider for Next.js 15 +- Custom theme in [theme.ts](Website/theme.ts) +- Combines MUI semantic components with Tailwind utilities + +```tsx +// Component pattern + + +``` + +## šŸ”§ Build & Deployment + +### Local Development + +```bash +# Generator +cd Generator +dotnet restore +dotnet build --configuration Release +dotnet run --OutputFolder ../Website/generated + +# Website +cd Website +npm install +npm run dev # Next.js dev server on port 3000 +``` + +### Production Build + +```bash +# Full pipeline +dotnet build --configuration Release +dotnet run --project Generator/Generator.csproj --OutputFolder Website/generated +cd Website +npm install +npm run prepipeline # Copy stub files if needed +npm run build # Next.js standalone build +npm run postbuild # Prepare deployment bundle +``` + +**Output**: [Website/.next/standalone/](Website/.next/standalone/) → Deploy to Azure App Service + +### CI/CD Pipeline + +1. **Build** ([azure-pipelines-build-jobs.yml](azure-pipelines-build-jobs.yml)) + - Build Generator (.NET 8) + - Run Generator → create Data.ts + - Download Wiki content (optional) + - Build Next.js app + - Create WebApp.zip artifact + +2. **Deploy** ([azure-pipelines-deploy-jobs.yml](azure-pipelines-deploy-jobs.yml)) + - Deploy Bicep template → App Service + Managed Identity + - Deploy WebApp.zip + - Set startup command: `node server.js` + +## šŸ” Configuration + +### Generator ([Generator/appsettings.local.json](Generator/appsettings.local.json)) + +```json +{ + "DataverseUrl": "https://org.crm4.dynamics.com", + "DataverseSolutionNames": "Solution1,Solution2" +} +``` + +### Website ([Website/.env.local](Website/.env.local)) + +```env +# Authentication +WebsitePassword= +WebsiteSessionSecret=<32-byte-secret> + +# Azure DevOps (diagram storage) +ADO_ORGANIZATION_URL=https://dev.azure.com/org +ADO_PROJECT_NAME=ProjectName +ADO_REPOSITORY_NAME=RepoName +ADO_PAT= # Dev only + +# Azure +AZURE_CLI_AUTHENTICATION_ENABLED=true # Dev only +``` + +## 🧩 Critical Components + +### DiagramViewContext ([components/diagramview/DiagramViewContext.tsx](Website/components/diagramview/DiagramViewContext.tsx)) + +**Responsibility**: Central state manager for diagram canvas + +**Key State**: +```typescript +{ + graph: dia.Graph | null // JointJS graph model + paper: dia.Paper | null // JointJS paper (canvas) + entitiesInDiagram: Map + excludedLinks: Set + paperMatrix: DOMMatrix // Canvas transform (zoom/pan) +} +``` + +**Key Actions**: +- `addEntity(entity, position)` - Add entity to canvas +- `removeEntity(schemaName)` - Remove entity and cleanup +- `selectEntity(schemaName)` - Update selection state +- `applySmartLayout()` - Auto-arrange entities +- `setExcludedLinks(set)` - Filter relationships + +### DiagramEventBridge ([lib/diagram/DiagramEventBridge.ts](Website/lib/diagram/DiagramEventBridge.ts)) + +**Pattern**: Event bus connecting JointJS (non-React) to React components + +```typescript +class DiagramEventBridge { + private static instance: DiagramEventBridge + + dispatch(eventName: string, detail: any) { + window.dispatchEvent(new CustomEvent(eventName, { detail })) + } + + on(eventName: string, callback: (detail: any) => void) { + window.addEventListener(eventName, (e) => callback(e.detail)) + } +} + +// Usage in JointJS view +DiagramEventBridge.getInstance().dispatch('selectObject', { + type: 'Entity', + name: entitySchemaName +}) + +// Usage in React component +useEffect(() => { + const bridge = DiagramEventBridge.getInstance() + const handler = (detail) => { /* handle event */ } + bridge.on('selectObject', handler) + return () => bridge.off('selectObject', handler) +}, []) +``` + +### AvoidRouter ([components/diagramview/avoid-router/](Website/components/diagramview/avoid-router/)) + +**Pattern**: WebAssembly routing in Web Worker + +**Files**: +- [worker-thread/worker.ts](Website/components/diagramview/avoid-router/worker-thread/worker.ts) - Worker entry point +- [shared/avoidrouter.ts](Website/components/diagramview/avoid-router/shared/avoidrouter.ts) - Main thread interface +- [shared/initialization.ts](Website/components/diagramview/avoid-router/shared/initialization.ts) - Setup logic + +**Flow**: +``` +Main Thread: Entity moved + ↓ +AvoidRouter.updateShapes() (debounced) + ↓ +Worker message: { type: 'updateShapes', shapes: [...] } + ↓ +Worker: libavoid calculates routes + ↓ +Worker message: { type: 'routesUpdated', routes: [...] } + ↓ +Main Thread: Update link vertices +``` + +### EntityElement ([components/diagramview/diagram-elements/EntityElement.ts](Website/components/diagramview/diagram-elements/EntityElement.ts)) + +**Custom JointJS Element** for Dataverse entities + +```typescript +class EntityElement extends dia.Element { + defaults() { + return { + type: 'custom.Entity', + size: { width: 120, height: 60 }, + attrs: { + body: { /* rectangle styling */ }, + icon: { /* SVG icon */ }, + label: { /* entity name */ } + }, + ports: { + groups: { + top: { position: 'top' }, + right: { position: 'right' }, + // ... 8 connection points + } + } + } + } +} + +// Custom view for DOM interactions +class EntityElementView extends dia.ElementView { + events: { + 'contextmenu': 'onContextMenu' + } + + onContextMenu(evt) { + DiagramEventBridge.getInstance().dispatch('entityContextMenu', { + entitySchemaName: this.model.get('entitySchemaName'), + position: { x: evt.clientX, y: evt.clientY } + }) + } +} +``` + +### DatamodelDataContext ([contexts/DatamodelDataContext.tsx](Website/contexts/DatamodelDataContext.tsx)) + +**Responsibility**: Load and provide entity metadata to entire app + +**Loading Strategy**: Web Worker to avoid blocking main thread + +```typescript +useEffect(() => { + const worker = new Worker('/workers/dataLoader.worker.js') + worker.postMessage({ type: 'LOAD_DATA' }) + + worker.onmessage = (e) => { + if (e.data.type === 'DATA_LOADED') { + const data = deserializeData(e.data.data) + setEntityGroups(data.EntityGroups) + setWarnings(data.Warnings) + // ... + } + } +}, []) +``` + +## šŸ” Search & Navigation + +### Metadata Browser ([app/metadata/page.tsx](Website/app/metadata/page.tsx)) + +**Features**: +- Virtual scrolling via @tanstack/react-virtual (handles 1000+ entities) +- Search by entity name/schema name +- Filter by group (Core, Custom, Activity, etc.) +- Detail view with tabs: Attributes, Relationships, Keys, Security + +### Process Explorer ([app/processes/page.tsx](Website/app/processes/page.tsx)) + +**Shows attribute usage in**: +- **Plugins**: Scanned from PluginStep.Configuration and images +- **Power Automate Flows**: Parsed from flow definition JSON +- **Web Resources**: JavaScript code parsed for attribute references + +**Use Case**: "Which plugins/flows will break if I delete this attribute?" + +## šŸ“ˆ Insights & Analytics ([app/insights/](Website/app/insights/)) + +### Compliance Insights ([insights/compliance/page.tsx](Website/app/insights/compliance/page.tsx)) + +**Charts** (using Nivo): +- **Audit Coverage**: Pie chart of IsAuditEnabled entities +- **Notes Coverage**: Entities with notes enabled +- **Ownership Distribution**: User vs Organization vs Team +- **Custom vs System**: Ratio of custom entities + +### Solution Insights ([insights/solutions/page.tsx](Website/app/insights/solutions/page.tsx)) + +**Features**: +- Solution component breakdown +- Dependency visualization (Chord diagram) +- Warning detection (missing dependencies, circular refs) + +## šŸš€ Deployment + +### Azure Resources (Created by [Infrastructure/main.bicep](Infrastructure/main.bicep)) + +1. **App Service Plan** (F1 Free or scalable SKU) +2. **App Service** with: + - System Assigned Managed Identity + - Node 24 LTS runtime + - Environment variables from pipeline + +### Required Azure Configuration + +**Managed Identity Permissions**: +- Must have "Basic" read access to Dataverse environment +- Must have Contributor access to ADO repository (for diagram storage) + +**Environment Variables** (set in pipeline): +- `WebsitePassword` - Login password +- `WebsiteSessionSecret` - JWT encryption key +- `ADO_ORGANIZATION_URL`, `ADO_PROJECT_NAME`, `ADO_REPOSITORY_NAME` +- (No PAT in production - uses Managed Identity) + +## šŸ› ļø Common Tasks + +### Add New Attribute Type + +1. **Generator**: Add DTO in [Generator/DTO/Attributes/](Generator/DTO/Attributes/) +2. **Generator**: Update [DataverseService.cs](Generator/DataverseService.cs) mapping logic +3. **Website**: Add type to [lib/Types.ts](Website/lib/Types.ts) AttributeType union +4. **Website**: Update renderer in [components/datamodelview/attributes/](Website/components/datamodelview/attributes/) + +### Add New Diagram Feature + +1. **Model**: Update [SerializedDiagram](Website/lib/diagram/models/SerializedDiagram.ts) +2. **Serializer**: Update [DiagramSerializer.ts](Website/lib/diagram/services/DiagramSerializer.ts) +3. **Context**: Add state to [DiagramViewContext.tsx](Website/components/diagramview/DiagramViewContext.tsx) +4. **UI**: Add controls in [components/diagramview/panes/](Website/components/diagramview/panes/) + +### Add New API Endpoint + +1. Create route in [app/api/](Website/app/api/) (e.g., `app/api/export/route.ts`) +2. Add session check if auth required: + ```typescript + const session = await getSession() + if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + ``` +3. Import types from [lib/Types.ts](Website/lib/Types.ts) +4. Return NextResponse with JSON + +### Update Generator Query + +1. Find query in [Generator/Queries/](Generator/Queries/) +2. Update FetchXML or QueryExpression +3. Run generator locally to test: + ```bash + dotnet run --project Generator/Generator.csproj --OutputFolder Website/generated + ``` +4. Verify [Website/generated/Data.ts](Website/generated/Data.ts) output + +## šŸ” Customization Detection + +### Standard Field Detection ([MetadataExtensions.cs](Generator/MetadataExtensions.cs)) + +The application identifies whether attributes are standard (out-of-box) or customized using multiple checks: + +**Detection Logic**: + +1. **IsCustomAttribute Check**: If `attribute.IsCustomAttribute = true`, it's entirely custom +2. **Display Name/Description Check**: Compares against language-specific defaults (English, Danish) +3. **Choice Options Check** (NEW for statecode/statuscode): + - Checks if any option in the OptionSet has `IsManaged = false` + - Unmanaged options indicate custom choices have been added + - Works reliably for both custom and standard entities + +**Implementation**: +```csharp +public static bool StandardFieldHasChanged( + AttributeMetadata attribute, + string entityDisplayName, + bool isCustomEntity) +{ + // Check text changes + var hasTextChanged = fields.StandardDescriptionHasChanged(...) + || fields.StandardDisplayNameHasChanged(...); + + // Check option changes for statecode/statuscode + var hasOptionsChanged = attribute switch + { + StateAttributeMetadata state => StateCodeOptionsHaveChanged(state), + StatusAttributeMetadata status => StatusCodeOptionsHaveChanged(status), + _ => false + }; + + return hasTextChanged || hasOptionsChanged; +} + +private static bool StateCodeOptionsHaveChanged(StateAttributeMetadata state) +{ + // Check if any options are unmanaged (custom) + return state.OptionSet?.Options?.Any(opt => opt.IsManaged == false) ?? false; +} +``` + +**Frontend Usage**: +- Fields marked with `IsStandardFieldModified = true` are shown when "Hide standard fields" is OFF +- Enables users to see which entities have custom status/state options added + +## šŸ› Debugging Tips + +### Generator Issues + +**Connection failures**: +- Check `DataverseUrl` in appsettings.local.json +- Verify Azure CLI logged in: `az login` +- Check DefaultAzureCredential order: Azure CLI → Environment → Managed Identity + +**Missing entities**: +- Verify solution names in `DataverseSolutionNames` +- Check if entity is in specified solutions +- Review console output for warnings + +### Website Issues + +**Diagram not rendering**: +- Check browser console for JointJS errors +- Verify EntityElement registered: `dia.Cell.define('custom.Entity')` +- Check DiagramViewContext initialized (graph, paper not null) + +**Routing not working**: +- Check Web Worker loaded: Network tab for `avoidrouter.worker.js` +- Verify libavoid WASM loaded: Console for initialization logs +- Check AvoidRouter state: `routingEnabled` should be true + +**Data not loading**: +- Verify [generated/Data.ts](Website/generated/Data.ts) exists (run generator) +- Check DatamodelDataContext state: `isLoading` should eventually be false +- Check Web Worker: Console for worker errors + +### Performance Issues + +**Slow diagram with many entities**: +- Check entity count (>100 may be slow) +- Disable routing temporarily: AvoidRouter.setRoutingEnabled(false) +- Use Smart Layout sparingly (O(n²) complexity) + +**Large Data.ts file**: +- Filter solutions more specifically in Generator +- Check for excessive attributes or relationships +- Consider pagination for metadata browser + +## šŸ“š Related Documentation + +- **Next.js 15 Docs**: https://nextjs.org/docs +- **JointJS Docs**: https://resources.jointjs.com/docs/jointjs +- **libavoid**: https://www.adaptagrams.org/documentation/libavoid.html +- **Dataverse SDK**: https://docs.microsoft.com/power-apps/developer/data-platform/ +- **Azure Managed Identity**: https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/ + +## šŸ”„ Version History + +- **2.2.2** (2025-11-05): Current version +- **Branch**: `patches/12187-diverse` +- **Main Branch**: `main` + +--- + +**Last Updated**: 2025-11-06 +**Generated By**: Claude Code Analysis diff --git a/Website/components/datamodelview/Attributes.tsx b/Website/components/datamodelview/Attributes.tsx index e4e424e..c17156a 100644 --- a/Website/components/datamodelview/Attributes.tsx +++ b/Website/components/datamodelview/Attributes.tsx @@ -411,7 +411,7 @@ export const Attributes = ({ entity, search = "", onVisibleCountChange }: IAttri {highlightMatch(attribute.SchemaName, highlightTerm)} {getAttributeComponent(entity, attribute, highlightMatch, highlightTerm)} - + {highlightMatch(attribute.Description ?? "", highlightTerm)} diff --git a/Website/components/datamodelview/Section.tsx b/Website/components/datamodelview/Section.tsx index 77ce2b8..0ae723e 100644 --- a/Website/components/datamodelview/Section.tsx +++ b/Website/components/datamodelview/Section.tsx @@ -41,7 +41,7 @@ export const Section = React.memo( {entity.SecurityRoles.length > 0 && ( -
+
)} diff --git a/Website/components/datamodelview/entity/AttributeDetails.tsx b/Website/components/datamodelview/entity/AttributeDetails.tsx index 0720010..e6f394c 100644 --- a/Website/components/datamodelview/entity/AttributeDetails.tsx +++ b/Website/components/datamodelview/entity/AttributeDetails.tsx @@ -1,12 +1,20 @@ 'use client' import { AttributeType, CalculationMethods, RequiredLevel } from "@/lib/Types"; -import { AddCircleOutlineRounded, CalculateRounded, ElectricBoltRounded, ErrorRounded, FunctionsRounded, LockRounded, VisibilityRounded } from "@mui/icons-material"; +import { AddCircleOutlineRounded, BadgeRounded, CalculateRounded, ElectricBoltRounded, ErrorRounded, FunctionsRounded, KeyRounded, LockRounded, VisibilityRounded } from "@mui/icons-material"; import { Box, Link, Tooltip, Typography } from "@mui/material"; -export function AttributeDetails({ entityName, attribute }: { entityName: string, attribute: AttributeType }) { +export function AttributeDetails({ entityName, attribute, isEntityAuditEnabled }: { entityName: string, attribute: AttributeType, isEntityAuditEnabled: boolean }) { const details = []; + if (attribute.IsPrimaryId) { + details.push({ icon: , tooltip: "Primary ID" }); + } + + if (attribute.IsPrimaryName) { + details.push({ icon: , tooltip: "Primary Name" }); + } + switch (attribute.RequiredLevel) { case RequiredLevel.SystemRequired: case RequiredLevel.ApplicationRequired: @@ -27,7 +35,16 @@ export function AttributeDetails({ entityName, attribute }: { entityName: string } if (attribute.IsAuditEnabled) { - details.push({ icon: , tooltip: "Audit Enabled" }); + const hasAuditConflict = !isEntityAuditEnabled; + const iconColor = hasAuditConflict ? "error" : "inherit"; + const tooltipText = hasAuditConflict + ? "Audit enabled on this column but not on the table, so it won't be in effect" + : "Audit Enabled"; + + details.push({ + icon: , + tooltip: tooltipText + }); } if (attribute.IsColumnSecured) { diff --git a/Website/components/datamodelview/entity/SecurityRoles.tsx b/Website/components/datamodelview/entity/SecurityRoles.tsx index de771bc..a4285da 100644 --- a/Website/components/datamodelview/entity/SecurityRoles.tsx +++ b/Website/components/datamodelview/entity/SecurityRoles.tsx @@ -53,33 +53,33 @@ function SecurityRoleRow({ role }: { role: SecurityRole }) { alignItems: 'flex-end' }} > - - - - - - - - + + + + + + + + ); } -function PrivilegeIcon({ name, depth }: { name: string, depth: PrivilegeDepth | null }) { +function PrivilegeIcon({ privilege, name, depth }: { privilege: string, name: string, depth: PrivilegeDepth | null }) { return ( - - {name} - + ); } -function GetDepthIcon({ depth }: { depth: PrivilegeDepth | null }) { +function GetDepthIcon({ privilege, depth }: { privilege: string, depth: PrivilegeDepth | null }) { const theme = useTheme(); - + let icon = null; let tooltip = ""; + // Generate context-aware tooltip based on privilege type and depth + const getTooltipText = (priv: string, d: PrivilegeDepth): string => { + const depthDescriptions: Record = { + [PrivilegeDepth.None]: "No access", + [PrivilegeDepth.Basic]: "User - Only records owned by the user", + [PrivilegeDepth.Local]: "Business Unit - Records owned by the user's business unit", + [PrivilegeDepth.Deep]: "Parent: Child Business Units - Records owned by the user's business unit and all child business units", + [PrivilegeDepth.Global]: "Organization - All records in the organization" + }; + + const privilegeDescriptions: Record = { + "Create": "Create new records", + "Read": "View records", + "Write": "Modify existing records", + "Delete": "Remove records", + "Append": "Attach other records to this record (e.g., add notes, activities)", + "AppendTo": "Attach this record to other records (e.g., be selected in a lookup)", + "Assign": "Change the owner of records", + "Share": "Share records with other users or teams" + }; + + if (d === PrivilegeDepth.None) { + return `${privilegeDescriptions[priv] || priv}: ${depthDescriptions[d]}`; + } + + return `${privilegeDescriptions[priv] || priv}\n${depthDescriptions[d]}`; + }; + if (depth === null || depth === undefined) { icon = ; - tooltip = "Unavailable"; + tooltip = "This privilege is not available for this entity"; } else { switch (depth) { case PrivilegeDepth.None: icon = ; - tooltip = "None"; + tooltip = getTooltipText(privilege, depth); break; case PrivilegeDepth.Basic: icon = ; - tooltip = "User"; + tooltip = getTooltipText(privilege, depth); break; case PrivilegeDepth.Local: icon = ; - tooltip = "Business Unit"; + tooltip = getTooltipText(privilege, depth); break; case PrivilegeDepth.Deep: icon = ; - tooltip = "Parent: Child Business Units"; + tooltip = getTooltipText(privilege, depth); break; case PrivilegeDepth.Global: icon = ; - tooltip = "Organization"; + tooltip = getTooltipText(privilege, depth); break; default: return null; @@ -133,7 +161,19 @@ function GetDepthIcon({ depth }: { depth: PrivilegeDepth | null }) { } return ( - + {icon} diff --git a/Website/components/homeview/HomeView.tsx b/Website/components/homeview/HomeView.tsx index b7d47d0..07e6b69 100644 --- a/Website/components/homeview/HomeView.tsx +++ b/Website/components/homeview/HomeView.tsx @@ -57,14 +57,14 @@ export const HomeView = ({ }: IHomeViewProps) => { const goToPrevious = () => { setSlideDirection('left'); - setCurrentCarouselIndex((prevIndex) => + setCurrentCarouselIndex((prevIndex) => prevIndex === 0 ? carouselItems.length - 1 : prevIndex - 1 ); }; const goToNext = () => { setSlideDirection('right'); - setCurrentCarouselIndex((prevIndex) => + setCurrentCarouselIndex((prevIndex) => prevIndex === carouselItems.length - 1 ? 0 : prevIndex + 1 ); }; @@ -90,10 +90,10 @@ export const HomeView = ({ }: IHomeViewProps) => { }, []); return ( - - + + - { url(/welcomeback-data-stockimage.webp) ` }}> - - Welcome back! - Explore your metadata model with ease. If this is your first time using Data Model Viewer, make sure to check out the documentation on Git. - - + + Welcome back! + Explore your metadata model with ease. If this is your first time using Data Model Viewer, make sure to check out the documentation on Git. + + - - + @@ -123,7 +123,7 @@ export const HomeView = ({ }: IHomeViewProps) => { backgroundImage={carouselItems[currentCarouselIndex]?.image} className='h-96' > -