Skip to content
17 changes: 11 additions & 6 deletions Website/components/datamodelview/Attributes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { highlightMatch } from "../datamodelview/List";
import { Box, Button, FormControl, InputAdornment, InputLabel, MenuItem, Select, Table, TableBody, TableCell, TableHead, TableRow, TextField, Tooltip, Typography, useTheme } from "@mui/material"
import { ClearRounded, SearchRounded, Visibility, VisibilityOff, ArrowUpwardRounded, ArrowDownwardRounded } from "@mui/icons-material"
import { useEntityFiltersDispatch } from "@/contexts/EntityFiltersContext"
import { useDatamodelData } from "@/contexts/DatamodelDataContext"

type SortDirection = 'asc' | 'desc' | null
type SortColumn = 'displayName' | 'schemaName' | 'type' | 'description' | null
Expand All @@ -37,6 +38,7 @@ export const Attributes = ({ entity, search = "", onVisibleCountChange }: IAttri

const theme = useTheme();
const entityFiltersDispatch = useEntityFiltersDispatch();
const { searchScope } = useDatamodelData();

// Report filter state changes to context
useEffect(() => {
Expand Down Expand Up @@ -144,7 +146,10 @@ export const Attributes = ({ entity, search = "", onVisibleCountChange }: IAttri
}

const sortedAttributes = getSortedAttributes();
const highlightTerm = searchQuery || search; // Use internal search or parent search for highlighting
// Only highlight if search scope includes columns
// Use internal search query first, or parent search if column scopes are enabled
const highlightTerm = searchQuery ||
(search && (searchScope.columnNames || searchScope.columnDescriptions || searchScope.columnDataTypes) ? search : "");

// Notify parent of visible count changes
useEffect(() => {
Expand Down Expand Up @@ -432,21 +437,21 @@ function getAttributeComponent(entity: EntityType, attribute: AttributeType, hig
case 'ChoiceAttribute':
return <ChoiceAttribute key={key} attribute={attribute} highlightMatch={highlightMatch} highlightTerm={highlightTerm} />;
case 'DateTimeAttribute':
return <DateTimeAttribute key={key} attribute={attribute} />;
return <DateTimeAttribute key={key} attribute={attribute} highlightMatch={highlightMatch} highlightTerm={highlightTerm} />;
case 'GenericAttribute':
return <GenericAttribute key={key} attribute={attribute} />;
return <GenericAttribute key={key} attribute={attribute} highlightMatch={highlightMatch} highlightTerm={highlightTerm} />;
case 'IntegerAttribute':
return <IntegerAttribute key={key} attribute={attribute} />;
case 'LookupAttribute':
return <LookupAttribute key={key} attribute={attribute} />;
case 'DecimalAttribute':
return <DecimalAttribute key={key} attribute={attribute} />;
case 'StatusAttribute':
return <StatusAttribute key={key} attribute={attribute} />;
return <StatusAttribute key={key} attribute={attribute} highlightMatch={highlightMatch} highlightTerm={highlightTerm} />;
case 'StringAttribute':
return <StringAttribute key={key} attribute={attribute} />;
return <StringAttribute key={key} attribute={attribute} highlightMatch={highlightMatch} highlightTerm={highlightTerm} />;
case 'BooleanAttribute':
return <BooleanAttribute key={key} attribute={attribute} />;
return <BooleanAttribute key={key} attribute={attribute} highlightMatch={highlightMatch} highlightTerm={highlightTerm} />;
case 'FileAttribute':
return <FileAttribute key={key} attribute={attribute} />;
default:
Expand Down
334 changes: 244 additions & 90 deletions Website/components/datamodelview/DatamodelView.tsx

Large diffs are not rendered by default.

54 changes: 50 additions & 4 deletions Website/components/datamodelview/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { AttributeType, EntityType, GroupType } from "@/lib/Types";
import { updateURL } from "@/lib/url-utils";
import { copyToClipboard, generateGroupLink } from "@/lib/clipboard-utils";
import { useSnackbar } from "@/contexts/SnackbarContext";
import { useEntityFilters } from "@/contexts/EntityFiltersContext";
import { Box, CircularProgress, debounce, Tooltip } from '@mui/material';

interface IListProps {
setCurrentIndex: (index: number) => void;
entityActiveTabs: Map<string, number>;
}

// Helper to highlight search matches
Expand All @@ -22,15 +24,26 @@ export function highlightMatch(text: string, search: string) {
return <>{text.slice(0, idx)}<mark className="bg-yellow-200 text-black px-0.5 rounded">{text.slice(idx, idx + search.length)}</mark>{text.slice(idx + search.length)}</>;
}

export const List = ({ setCurrentIndex }: IListProps) => {
export const List = ({ setCurrentIndex, entityActiveTabs }: IListProps) => {
const dispatch = useDatamodelViewDispatch();
const { currentSection, loadingSection } = useDatamodelView();
const { groups, filtered, search } = useDatamodelData();
const { selectedSecurityRoles } = useEntityFilters();
const { showSnackbar } = useSnackbar();
const parentRef = useRef<HTMLDivElement | null>(null);
// used to relocate section after search/filter
const [sectionVirtualItem, setSectionVirtualItem] = useState<string | null>(null);

// Helper function to check if entity has access from selected security roles
const hasSecurityRoleAccess = useCallback((entity: EntityType): boolean => {
if (selectedSecurityRoles.length === 0) return false;

return entity.SecurityRoles.some(role =>
selectedSecurityRoles.includes(role.Name) &&
(role.Read !== null && role.Read >= 0) // Has any read access
);
}, [selectedSecurityRoles]);

const handleCopyGroupLink = useCallback(async (groupName: string) => {
const link = generateGroupLink(groupName);
const success = await copyToClipboard(link);
Expand All @@ -43,7 +56,7 @@ export const List = ({ setCurrentIndex }: IListProps) => {

// Only recalculate items when filtered or search changes
const flatItems = useMemo(() => {
if (filtered && filtered.length > 0) return filtered.filter(item => item.type !== 'attribute');
if (filtered && filtered.length > 0) return filtered.filter(item => item.type !== 'attribute' && item.type !== 'relationship');

const lowerSearch = search.trim().toLowerCase();
const items: Array<
Expand All @@ -54,6 +67,13 @@ export const List = ({ setCurrentIndex }: IListProps) => {
// Filter entities in this group
const filteredEntities = group.Entities.filter((entity: EntityType) => {
const typedEntity = entity;

// If security roles are selected, only show entities with access
if (selectedSecurityRoles.length > 0) {
const hasAccess = hasSecurityRoleAccess(typedEntity);
if (!hasAccess) return false;
}

if (!lowerSearch) return true;
// Match entity schema or display name
const entityMatch = typedEntity.SchemaName.toLowerCase().includes(lowerSearch) ||
Expand All @@ -73,7 +93,7 @@ export const List = ({ setCurrentIndex }: IListProps) => {
}
}
return items;
}, [filtered, search, groups]);
}, [filtered, search, groups, selectedSecurityRoles, hasSecurityRoleAccess]);

const debouncedOnChange = debounce((instance, sync) => {
if (!sync) {
Expand Down Expand Up @@ -192,6 +212,29 @@ export const List = ({ setCurrentIndex }: IListProps) => {
}
}, [scrollToSection]);

const scrollToRelationship = useCallback((sectionId: string, relSchema: string) => {
const relId = `rel-${sectionId}-${relSchema}`;

// Helper function to attempt scrolling to relationship with retries
const attemptScroll = (attemptsLeft: number) => {
const relationshipLocation = document.getElementById(relId);

if (relationshipLocation) {
// Relationship found, scroll to it
relationshipLocation.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else if (attemptsLeft > 0) {
// Relationship not rendered yet, retry after delay
setTimeout(() => attemptScroll(attemptsLeft - 1), 100);
} else {
// Give up after all retries, just scroll to section
scrollToSection(sectionId);
}
};

// Start attempting to scroll with 5 retries (total 500ms wait time)
attemptScroll(5);
}, [scrollToSection]);

const scrollToGroup = useCallback((groupName: string) => {
const groupIndex = flatItems.findIndex(item =>
item.type === 'group' && item.group.Name === groupName
Expand All @@ -214,9 +257,10 @@ export const List = ({ setCurrentIndex }: IListProps) => {
useEffect(() => {
dispatch({ type: 'SET_SCROLL_TO_SECTION', payload: scrollToSection });
dispatch({ type: 'SET_SCROLL_TO_ATTRIBUTE', payload: scrollToAttribute });
dispatch({ type: 'SET_SCROLL_TO_RELATIONSHIP', payload: scrollToRelationship });
dispatch({ type: 'SET_SCROLL_TO_GROUP', payload: scrollToGroup });
dispatch({ type: 'SET_RESTORE_SECTION', payload: restoreSection });
}, [dispatch, scrollToSection, scrollToAttribute, scrollToGroup]);
}, [dispatch, scrollToSection, scrollToAttribute, scrollToRelationship, scrollToGroup, restoreSection]);

const smartScrollToIndex = useCallback((index: number) => {
rowVirtualizer.scrollToIndex(index, { align: 'start' });
Expand Down Expand Up @@ -301,6 +345,8 @@ export const List = ({ setCurrentIndex }: IListProps) => {
entity={item.entity}
group={item.group}
search={search}
activeTab={entityActiveTabs.get(item.entity.SchemaName)}
highlightSecurityRole={selectedSecurityRoles.length > 0 && hasSecurityRoleAccess(item.entity)}
/>
</div>
)}
Expand Down
18 changes: 11 additions & 7 deletions Website/components/datamodelview/Relationships.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRe

const dispatch = useDatamodelViewDispatch();
const { scrollToSection } = useDatamodelView();
const { groups } = useDatamodelData();
const { groups, searchScope } = useDatamodelData();

// Helper function to check if an entity is in the solution
const isEntityInSolution = (entitySchemaName: string): boolean => {
Expand Down Expand Up @@ -147,7 +147,10 @@ export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRe
]

const sortedRelationships = getSortedRelationships();
const highlightTerm = searchQuery || search; // Use internal search or parent search for highlighting
// Only highlight if search scope includes relationships
// Use internal search query first, or parent search if relationships scope is enabled
const highlightTerm = searchQuery ||
(search && searchScope.relationships ? search : "");

// Notify parent of visible count changes
useEffect(() => {
Expand Down Expand Up @@ -393,6 +396,7 @@ export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRe
{sortedRelationships.map((relationship, index) =>
<TableRow
key={relationship.RelationshipSchema}
id={`rel-${entity.SchemaName}-${relationship.RelationshipSchema}`}
className="transition-colors duration-150 border-b"
sx={{
'&:hover': { backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)' },
Expand All @@ -405,7 +409,7 @@ export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRe
}}
>
<TableCell className="break-words py-1 md:py-1.5 text-xs md:text-sm">
{highlightMatch(relationship.Name, highlightTerm)}
{relationship.Name}
</TableCell>
<TableCell className="py-1 md:py-1.5">
{isEntityInSolution(relationship.TableSchema) ? (
Expand All @@ -430,13 +434,13 @@ export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRe
}
}}
>
{highlightMatch(relationship.TableSchema, highlightTerm)}
{relationship.TableSchema}
</Button>
) : (
<Chip
key={relationship.TableSchema}
icon={<ContentPasteOffRounded className="w-2 h-2 md:w-3 md:h-3" />}
label={highlightMatch(relationship.TableSchema, highlightTerm)}
label={relationship.TableSchema}
size="small"
disabled
sx={{
Expand All @@ -452,7 +456,7 @@ export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRe
)}
</TableCell>
<TableCell className="break-words py-1 md:py-1.5 text-xs md:text-sm">
{relationship.LookupDisplayName}
{highlightMatch(relationship.LookupDisplayName, highlightTerm)}
</TableCell>
<TableCell className="py-1 md:py-1.5 text-xs md:text-sm">
{relationship.RelationshipType}
Expand All @@ -461,7 +465,7 @@ export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRe
<CascadeConfiguration config={relationship.CascadeConfiguration} />
</TableCell>
<TableCell className="break-words py-1 md:py-1.5 text-xs md:text-sm">
{relationship.RelationshipSchema}
{highlightMatch(relationship.RelationshipSchema, highlightTerm)}
{relationship.IntersectEntitySchemaName &&
(<Typography variant="body2" className="text-xs md:text-sm text-secondary"><b>Intersecting table:</b> {relationship.IntersectEntitySchemaName}</Typography>)}
</TableCell>
Expand Down
28 changes: 23 additions & 5 deletions Website/components/datamodelview/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SecurityRoles } from "./entity/SecurityRoles"
import Keys from "./Keys"
import { Attributes } from "./Attributes"
import { Relationships } from "./Relationships"
import { highlightMatch } from "./List"
import React from "react"
import { Box, Paper, Tab, Tabs } from "@mui/material"
import CustomTabPanel from "../shared/elements/TabPanel"
Expand All @@ -15,11 +16,20 @@ interface ISectionProps {
entity: EntityType;
group: GroupType;
search?: string;
activeTab?: number; // External tab control for search navigation
highlightSecurityRole?: boolean; // Highlight when entity matches selected security roles
}

export const Section = React.memo(
({ entity, group, search }: ISectionProps) => {
({ entity, group, search, activeTab, highlightSecurityRole }: ISectionProps) => {
const [tab, setTab] = React.useState(0);

// Update local tab state when external activeTab prop changes
React.useEffect(() => {
if (activeTab !== undefined) {
setTab(activeTab);
}
}, [activeTab]);
const [visibleAttributeCount, setVisibleAttributeCount] = React.useState(entity.Attributes.length);
const [visibleRelationshipCount, setVisibleRelationshipCount] = React.useState(entity.Relationships.length);

Expand All @@ -37,12 +47,18 @@ export const Section = React.memo(

return (
<Box data-entity-schema={entity.SchemaName} data-group={group.Name} className="mb-10">
<Paper className="rounded-lg" sx={{ backgroundColor: 'background.paper' }} variant="outlined">
<Paper
className="rounded-lg"
sx={{
backgroundColor: 'background.paper',
}}
variant="outlined"
>
<Box className="flex flex-col xl:flex-row min-w-0 p-6">
<EntityHeader entity={entity} />
{entity.SecurityRoles.length > 0 && (
<div className="md:w-full xl:w-2/3 md:border-t xl:border-t-0 mt-6 xl:mt-0 xl:pt-0">
<SecurityRoles roles={entity.SecurityRoles} />
<SecurityRoles roles={entity.SecurityRoles} highlightMatch={highlightMatch} highlightTerm={search || ''} highlightSecurityRole={highlightSecurityRole} />
</div>
)}
</Box>
Expand Down Expand Up @@ -114,10 +130,12 @@ export const Section = React.memo(
},
// Custom comparison function to prevent unnecessary re-renders
(prevProps, nextProps) => {
// Only re-render if entity, search or group changes
// Only re-render if entity, search, group, activeTab, or highlightSecurityRole changes
return prevProps.entity.SchemaName === nextProps.entity.SchemaName &&
prevProps.search === nextProps.search &&
prevProps.group.Name === nextProps.group.Name;
prevProps.group.Name === nextProps.group.Name &&
prevProps.activeTab === nextProps.activeTab &&
prevProps.highlightSecurityRole === nextProps.highlightSecurityRole;
}
);

Expand Down
Loading
Loading