diff --git a/Generator/DTO/AttributeUsage.cs b/Generator/DTO/AttributeUsage.cs index 5d2dba8..fe36de0 100644 --- a/Generator/DTO/AttributeUsage.cs +++ b/Generator/DTO/AttributeUsage.cs @@ -27,4 +27,20 @@ public record AttributeUsage( OperationType OperationType, ComponentType ComponentType, bool IsFromDependencyAnalysis -); +) +{ + public static OperationType MapSdkMessageToOperationType(string sdkMessageName) + { + return sdkMessageName?.ToLowerInvariant() switch + { + "create" => OperationType.Create, + "update" => OperationType.Update, + "delete" => OperationType.Delete, + "retrieve" => OperationType.Read, + "retrievemultiple" => OperationType.List, + "upsert" => OperationType.Update, + "merge" => OperationType.Update, + _ => OperationType.Other + }; + } +}; diff --git a/Generator/DTO/SDKStep.cs b/Generator/DTO/SDKStep.cs index b1a0a48..ba0ee4b 100644 --- a/Generator/DTO/SDKStep.cs +++ b/Generator/DTO/SDKStep.cs @@ -7,4 +7,5 @@ public record SDKStep( string Name, string FilteringAttributes, string PrimaryObjectTypeCode, + string SdkMessageName, OptionSetValue State) : Analyzeable(); diff --git a/Generator/Queries/PluginQueries.cs b/Generator/Queries/PluginQueries.cs index bc6e5d2..1955a99 100644 --- a/Generator/Queries/PluginQueries.cs +++ b/Generator/Queries/PluginQueries.cs @@ -44,6 +44,16 @@ public static async Task> GetSDKMessageProcessingStepsAsync { Columns = new ColumnSet("primaryobjecttypecode"), EntityAlias = "filter" + }, + new LinkEntity( + "sdkmessageprocessingstep", + "sdkmessage", + "sdkmessageid", + "sdkmessageid", + JoinOperator.Inner) + { + Columns = new ColumnSet("name"), + EntityAlias = "message" } //new LinkEntity //{ @@ -66,17 +76,19 @@ public static async Task> GetSDKMessageProcessingStepsAsync var steps = result.Entities.Select(e => { var sdkMessageId = e.GetAttributeValue("step.sdkmessageid")?.Value as EntityReference; - var sdkMessageName = e.GetAttributeValue("step.name")?.Value as string; + var sdkStepName = e.GetAttributeValue("step.name")?.Value as string; var sdkFilterAttributes = e.GetAttributeValue("step.filteringattributes")?.Value as string; var sdkState = e.GetAttributeValue("step.statecode")?.Value as OptionSetValue; var filterTypeCode = e.GetAttributeValue("filter.primaryobjecttypecode")?.Value as string; + var sdkMessageName = e.GetAttributeValue("message.name")?.Value as string; return new SDKStep( - sdkMessageId.Id.ToString(), - sdkMessageName ?? "Unknown Name", + sdkMessageId?.Id.ToString() ?? "Unknown", + sdkStepName ?? "Unknown Name", sdkFilterAttributes ?? "", - filterTypeCode, - sdkState + filterTypeCode ?? "Unknown", + sdkMessageName ?? "Unknown", + sdkState ?? new OptionSetValue(0) ); }); diff --git a/Generator/Services/Plugins/PluginAnalyzer.cs b/Generator/Services/Plugins/PluginAnalyzer.cs index 1f07b99..0b6f90d 100644 --- a/Generator/Services/Plugins/PluginAnalyzer.cs +++ b/Generator/Services/Plugins/PluginAnalyzer.cs @@ -22,9 +22,12 @@ public override async Task AnalyzeComponentAsync(SDKStep sdkStep, Dictionary attributeMatchesSearch(attr, query)) } - if (hideStandardFields) filteredAttributes = filteredAttributes.filter(attr => attr.IsCustomAttribute || attr.IsStandardFieldModified); + if (hideStandardFields) filteredAttributes = filteredAttributes.filter(attr => (attr.IsCustomAttribute || attr.IsStandardFieldModified) && !attr.SchemaName.endsWith("Base")); if (!sortColumn || !sortDirection) return filteredAttributes diff --git a/Website/components/datamodelview/entity/AttributeDetails.tsx b/Website/components/datamodelview/entity/AttributeDetails.tsx index e6f394c..bffe1f3 100644 --- a/Website/components/datamodelview/entity/AttributeDetails.tsx +++ b/Website/components/datamodelview/entity/AttributeDetails.tsx @@ -12,7 +12,7 @@ export function AttributeDetails({ entityName, attribute, isEntityAuditEnabled } } if (attribute.IsPrimaryName) { - details.push({ icon: , tooltip: "Primary Name" }); + details.push({ icon: , tooltip: "Primary column: Its value is shown in the header of forms for this table, and as the display value of lookup-fields pointing to this table" }); } switch (attribute.RequiredLevel) { diff --git a/Website/components/datamodelview/entity/SecurityRoles.tsx b/Website/components/datamodelview/entity/SecurityRoles.tsx index a4285da..d704d0a 100644 --- a/Website/components/datamodelview/entity/SecurityRoles.tsx +++ b/Website/components/datamodelview/entity/SecurityRoles.tsx @@ -16,7 +16,7 @@ export function SecurityRoles({ roles }: { roles: SecurityRole[] }) { function SecurityRoleRow({ role }: { role: SecurityRole }) { const theme = useTheme(); - + return ( - {role.Name} - @@ -106,7 +106,7 @@ function GetDepthIcon({ privilege, depth }: { privilege: string, depth: Privileg const getTooltipText = (priv: string, d: PrivilegeDepth): string => { const depthDescriptions: Record = { [PrivilegeDepth.None]: "No access", - [PrivilegeDepth.Basic]: "User - Only records owned by the user", + [PrivilegeDepth.Basic]: "User (or team) - Only records owned by the user themselves or owned by teams the user is a member of. This doesn't give access to rows owned by other members of those teams.", [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" @@ -117,8 +117,8 @@ function GetDepthIcon({ privilege, depth }: { privilege: string, depth: Privileg "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)", + "Append": "Access to attach other tables to me. (e.g. fill a lookup on table record with other table record)", + "AppendTo": "Access to attach me to other tables. (e.g. this table can be selected in a lookup from another table)", "Assign": "Change the owner of records", "Share": "Share records with other users or teams" }; diff --git a/Website/components/insightsview/overview/InsightsOverviewView.tsx b/Website/components/insightsview/overview/InsightsOverviewView.tsx index 3010f0b..dce8194 100644 --- a/Website/components/insightsview/overview/InsightsOverviewView.tsx +++ b/Website/components/insightsview/overview/InsightsOverviewView.tsx @@ -54,12 +54,12 @@ const InsightsOverviewView = ({ }: InsightsOverviewViewProps) => { return [ { - category: 'Entities', + category: 'Tables', standard: standardEntities.length, custom: customEntities.length, }, { - category: 'Attributes', + category: 'Columns', standard: standardAttributes.length, custom: customAttributes.length, }, @@ -251,21 +251,21 @@ const InsightsOverviewView = ({ }: InsightsOverviewViewProps) => { - {/* Ungrouped Entities */} - entity.SchemaName).join(", ")}> + {/* Ungrouped Tables */} + entity.SchemaName).join(", ")}> {WarningIcon} {ungroupedEntities.length} - Entities ungrouped + Tables ungrouped - {/* No Icon Entities */} - entity.SchemaName).join(", ")}> + {/* No Icon Tables */} + entity.SchemaName).join(", ")}> {WarningIcon} {missingIconEntities.length} - Entities without icons + Tables without icons @@ -302,7 +302,7 @@ const InsightsOverviewView = ({ }: InsightsOverviewViewProps) => { @@ -314,7 +314,7 @@ const InsightsOverviewView = ({ }: InsightsOverviewViewProps) => { Data Model Distribution: Standard vs Custom - + {InfoIcon} @@ -618,9 +618,9 @@ const InsightsOverviewView = ({ }: InsightsOverviewViewProps) => { - Entity Features Distribution + Table Features Distribution - + {InfoIcon} @@ -674,9 +674,9 @@ const InsightsOverviewView = ({ }: InsightsOverviewViewProps) => { - Attribute Types Distribution + Column Types Distribution - + {InfoIcon} @@ -730,9 +730,9 @@ const InsightsOverviewView = ({ }: InsightsOverviewViewProps) => { - Attribute Process Dependencies by Type + Column Process Dependencies by Type - + {InfoIcon} @@ -786,9 +786,9 @@ const InsightsOverviewView = ({ }: InsightsOverviewViewProps) => { - Attribute Process Dependencies by Detection Source + Column Process Dependencies by Detection Source - + {InfoIcon} diff --git a/Website/components/insightsview/solutions/InsightsSolutionView.tsx b/Website/components/insightsview/solutions/InsightsSolutionView.tsx index c3ed7b8..f280215 100644 --- a/Website/components/insightsview/solutions/InsightsSolutionView.tsx +++ b/Website/components/insightsview/solutions/InsightsSolutionView.tsx @@ -323,9 +323,9 @@ const InsightsSolutionView = ({ }: InsightsSolutionViewProps) => { {component.Name} ({ component.ComponentType === SolutionComponentTypeEnum.Entity - ? 'Entity' + ? 'Table' : component.ComponentType === SolutionComponentTypeEnum.Attribute - ? 'Attribute' + ? 'Column' : component.ComponentType === SolutionComponentTypeEnum.Relationship ? 'Relationship' : 'Unknown' diff --git a/Website/components/processesview/ProcessesView.tsx b/Website/components/processesview/ProcessesView.tsx index 93ac534..b308250 100644 --- a/Website/components/processesview/ProcessesView.tsx +++ b/Website/components/processesview/ProcessesView.tsx @@ -3,14 +3,14 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react' import { useSidebar } from '@/contexts/SidebarContext' import { useDatamodelData } from '@/contexts/DatamodelDataContext' -import { Box, Typography, Paper, TextField, InputAdornment, Grid, List, ListItem, ListItemButton, ListItemText, Chip, IconButton, Table, TableHead, TableBody, TableRow, TableCell, useTheme, Alert, Divider, Accordion, AccordionSummary, AccordionDetails } from '@mui/material' -import { AccountTreeRounded, CloseRounded, ExtensionRounded, JavascriptRounded, SearchRounded, WarningRounded, ExpandMoreRounded } from '@mui/icons-material' -import { AttributeType, EntityType, ComponentType, OperationType, WarningType } from '@/lib/Types' +import { Box, Typography, Paper, TextField, InputAdornment, Grid, List, ListItem, ListItemButton, Chip, IconButton, Table, TableHead, TableBody, TableRow, TableCell, useTheme, Divider, ClickAwayListener } from '@mui/material' +import { AccountTreeRounded, CloseRounded, ExtensionRounded, JavascriptRounded, SearchRounded } from '@mui/icons-material' +import { AttributeType, EntityType, ComponentType, OperationType } from '@/lib/Types' import LoadingOverlay from '@/components/shared/LoadingOverlay' -import NotchedBox from '../shared/elements/NotchedBox' import { StatCard } from '../shared/elements/StatCard' import { ResponsivePie } from '@nivo/pie' -import { useSearchParams } from 'next/navigation' +import { ResponsiveBar } from '@nivo/bar' +import { useSearchParams, useRouter, usePathname } from 'next/navigation' interface IProcessesViewProps { } @@ -22,13 +22,18 @@ interface AttributeSearchResult { export const ProcessesView = ({ }: IProcessesViewProps) => { const { setElement, close } = useSidebar() - const { groups, warnings } = useDatamodelData() + const { groups } = useDatamodelData() const theme = useTheme() + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() const [searchTerm, setSearchTerm] = useState('') const [isSearching, setIsSearching] = useState(false) const [selectedAttribute, setSelectedAttribute] = useState(null) - const initialAttribute = useSearchParams().get('attr') || ""; - const initialEntity = useSearchParams().get('ent') || ""; + const [searchAnchorEl, setSearchAnchorEl] = useState(null) + const [showSearchDropdown, setShowSearchDropdown] = useState(false) + const initialAttribute = searchParams.get('attr') || ""; + const initialEntity = searchParams.get('ent') || ""; useEffect(() => { setElement(null); @@ -89,6 +94,28 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { return data; }, [selectedAttribute]) + const operationTypeData = useMemo(() => { + if (!selectedAttribute) return []; + + const data = Object.values( + selectedAttribute.attribute.AttributeUsages.reduce((acc, au) => { + const operationTypeName = OperationType[au.OperationType]; + if (acc[operationTypeName]) { + acc[operationTypeName].count += 1; + } else { + acc[operationTypeName] = { + operation: operationTypeName, + count: 1, + color: 'hsl(210, 70%, 50%)' + }; + } + return acc; + }, {} as Record) + ); + + return data; + }, [selectedAttribute]) + // Search through all attributes across all entities const searchResults = useMemo(() => { if (!searchTerm.trim() || searchTerm.length < 2) { @@ -126,26 +153,45 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { }) }) - return results.slice(0, 50) // Limit results for performance + return results.slice(0, 10) // Limit to 10 results for dropdown }, [searchTerm, groups]) // Simulate search delay for UX useEffect(() => { if (searchTerm.trim() && searchTerm.length >= 2) { setIsSearching(true) + setShowSearchDropdown(true) const timer = setTimeout(() => { setIsSearching(false) }, 300) return () => clearTimeout(timer) } else { setIsSearching(false) + setShowSearchDropdown(false) } }, [searchTerm]) const handleAttributeSelect = useCallback((result: AttributeSearchResult) => { setSelectedAttribute(result); setSearchTerm(''); - }, []) + setShowSearchDropdown(false); + + // Update URL with query params for deeplinking + const params = new URLSearchParams(searchParams.toString()); + params.set('attr', result.attribute.SchemaName); + params.set('ent', result.entity.SchemaName); + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, [searchParams, router, pathname]) + + const handleClearSelection = useCallback(() => { + setSelectedAttribute(null); + + // Remove query params + const params = new URLSearchParams(searchParams.toString()); + params.delete('attr'); + params.delete('ent'); + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, [searchParams, router, pathname]) const getAttributeTypeLabel = (attributeType: string) => { switch (attributeType) { @@ -179,21 +225,15 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { return ( <> - + - {!selectedAttribute && ( - - Welcome to the Processes Explorer. Please search and select an attribute to see related processes. - - )} - { { { { { - {/* Search Bar */} - - setSearchTerm(e.target.value)} - slotProps={{ - input: { - startAdornment: ( - - - - ), - } - }} - sx={{ - '& .MuiOutlinedInput-root': { - backgroundColor: 'background.paper', - borderRadius: '8px', - '& fieldset': { - borderColor: 'divider', - }, - '&:hover fieldset': { - borderColor: 'primary.main', - }, - '&.Mui-focused fieldset': { - borderColor: 'primary.main', - }, - }, - '& .MuiInputBase-input': { - fontSize: '1.1rem', - padding: '14px 16px', - }, - }} - /> - - - {/* Search Results */} - {searchTerm.trim() && searchTerm.length >= 2 && !isSearching && ( - - - Attribute Search Results ({searchResults.length}) - - - {searchResults.length > 0 ? ( - + + {/* Search Bar with Dropdown */} + + { + setSearchTerm(e.target.value); + setSearchAnchorEl(e.currentTarget); + }} + slotProps={{ + input: { + startAdornment: ( + + + + ), + } + }} sx={{ - borderRadius: 2, - backgroundColor: 'background.paper', - maxHeight: '400px', - overflow: 'auto' + '& .MuiOutlinedInput-root': { + backgroundColor: 'background.default', + '& fieldset': { + borderColor: 'divider', + }, + '&:hover fieldset': { + borderColor: 'primary.main', + }, + '&.Mui-focused fieldset': { + borderColor: 'primary.main', + }, + }, }} - > - - {searchResults.map((result, index) => ( - - handleAttributeSelect(result)} - selected={selectedAttribute?.attribute.SchemaName === result.attribute.SchemaName && - selectedAttribute?.entity.SchemaName === result.entity.SchemaName} - > - - - {result.attribute.DisplayName} - - - {result.attribute.IsCustomAttribute && ( - - )} - - } - secondary={ - - - {result.entity.DisplayName} • {result.group} - - - {result.attribute.SchemaName} - - - } - /> - - - ))} - - - ) : ( - - No attributes found matching "{searchTerm}" - - )} - - )} + /> - {searchTerm.trim() && searchTerm.length < 2 && ( - - Enter at least 2 characters to search attributes - - )} + {/* Search Dropdown */} + {showSearchDropdown && searchAnchorEl && ( + setShowSearchDropdown(false)}> + + {searchResults.length > 0 ? ( + + {searchResults.map((result, index) => ( + + handleAttributeSelect(result)} + sx={{ + py: 1.5, + borderBottom: index < searchResults.length - 1 ? 1 : 0, + borderColor: 'divider' + }} + > + + + + + {result.attribute.DisplayName} + + + + + {result.attribute.SchemaName} + + + + {result.entity.DisplayName} + + + + + ))} + + ) : ( + + + No columns found matching "{searchTerm}" + + + )} + + + )} + - {/* GRID WITH SELECTED ATTRIBUTE */} - {selectedAttribute && ( - - - setSelectedAttribute(null)}>} - className='flex flex-col items-center justify-center h-full w-full' + {/* Selected Attribute Display */} + {selectedAttribute && ( + - - - Selected Attribute + + + Selected Column - - [{selectedAttribute.group} ({selectedAttribute.entity.DisplayName})]: {selectedAttribute.attribute.DisplayName} + + {selectedAttribute.attribute.DisplayName} + + + {selectedAttribute.attribute.SchemaName} + + + {selectedAttribute.entity.DisplayName} - - {chartData.length > 0 ? ( - - ) : ( - - No usage data available - - )} - - - - - - - Processes - - - {selectedAttribute?.attribute.AttributeUsages.length === 0 ? ( - - No process usage data available for this attribute + + + + + )} + + {/* Process Distribution and Table */} + {selectedAttribute && ( + <> + + + {/* Charts Grid */} + {(chartData.length > 0 || operationTypeData.length > 0) && ( + + {/* Process Type Distribution Pie Chart */} + {chartData.length > 0 && ( + + + Process Type Distribution + + + + + + )} + + {/* Operation Type Distribution Bar Chart */} + {operationTypeData.length > 0 && ( + + + Operation Type Distribution + + `${e.id}: ${e.formattedValue} in ${e.indexValue}`} + /> + + + + )} + + )} + + {/* Process Table */} + + Processes ({selectedAttribute.attribute.AttributeUsages.length}) + {selectedAttribute.attribute.AttributeUsages.length === 0 ? ( + + No process usage data available for this column ) : ( - - - - +
+ + + Process - + Name - + Type - + Usage - {selectedAttribute?.attribute.AttributeUsages.map((usage, idx) => ( + {selectedAttribute.attribute.AttributeUsages.map((usage, idx) => ( - + {getProcessChip(usage.ComponentType)} - - {usage.Name} - - - {OperationType[usage.OperationType]} - - - {usage.Usage} - + {usage.Name} + {OperationType[usage.OperationType]} + {usage.Usage} ))} @@ -577,69 +641,33 @@ export const ProcessesView = ({ }: IProcessesViewProps) => { )} - - - )} - - - - {/* Warnings */} - - } - aria-controls="warnings-content" - id="warnings-header" - sx={{ - backgroundColor: 'background.paper', - color: 'text.secondary', - '&:hover': { - backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)' - } - }} - > - - - - Erroneous Attributes ({warnings.filter(warning => warning.Type === WarningType.Attribute).length}) - - - - - {warnings.filter(warning => warning.Type === WarningType.Attribute).length > 0 ? ( - - {warnings.filter(warning => warning.Type === WarningType.Attribute).map((warning, index) => ( - - {warning.Message} - - ))} + + )} + + {!selectedAttribute && ( + + + + Search and select a column to view its process dependencies + - ) : ( - - No attribute errors found. - )} - - + + + + {/* About This Page */} + + About Process Explorer + + The Process Explorer helps you understand how columns are used across your Dataverse environment. + By standard Microsoft can show some dependencies, but this tool aggregates that data with additional analysis of webresources, cloud flows, and more. + Make sure to understand the limitations, as the analysis may not capture 100% of all dependencies. + + + How to use: Search for a column by name, then view its process dependencies in the table. + You can share specific columns using the URL - the page will automatically load your selected column. + + diff --git a/Website/components/shared/Sidebar.tsx b/Website/components/shared/Sidebar.tsx index c65e1f7..56bfc6c 100644 --- a/Website/components/shared/Sidebar.tsx +++ b/Website/components/shared/Sidebar.tsx @@ -41,14 +41,12 @@ const Sidebar = ({ }: SidebarProps) => { href: '/insights', icon: InsightsIcon, active: pathname === '/insights', - new: true, }, { label: 'Metadata', href: '/metadata', icon: MetadataIcon, active: pathname === '/metadata', - new: true, }, { label: 'Diagram', @@ -62,7 +60,6 @@ const Sidebar = ({ }: SidebarProps) => { href: '/processes', icon: ProcessesIcon, active: pathname === '/processes', - new: true, } ]; diff --git a/Website/components/shared/elements/TabPanel.tsx b/Website/components/shared/elements/TabPanel.tsx index d9c4db3..42cc53e 100644 --- a/Website/components/shared/elements/TabPanel.tsx +++ b/Website/components/shared/elements/TabPanel.tsx @@ -20,7 +20,7 @@ const CustomTabPanel = (props: TabPanelProps) => { className={className} {...other} > - {value === index && {children}} + {children} ); }