Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Infrastructure/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ resource webApp 'Microsoft.Web/sites@2021-02-01' = {
serverFarmId: appServicePlan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'NODE|20-lts'
linuxFxVersion: 'NODE|24-lts'

appSettings: [
{
Expand Down
12 changes: 12 additions & 0 deletions Website/.claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(npm run lint:*)",
"Bash(npm install:*)",
"Bash(npm run build:*)",
"Bash(npx tsc:*)"
],
"deny": [],
"ask": []
}
}
9 changes: 6 additions & 3 deletions Website/app/metadata/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { DatamodelView } from "@/components/datamodelview/DatamodelView";
import Layout from "@/components/shared/Layout";
import { Suspense } from "react";
import { DatamodelViewProvider } from "@/contexts/DatamodelViewContext";
import { EntityFiltersProvider } from "@/contexts/EntityFiltersContext";

export default function Data() {
return (
<Suspense>
<DatamodelViewProvider>
<Layout>
<DatamodelView />
</Layout>
<EntityFiltersProvider>
<Layout>
<DatamodelView />
</Layout>
</EntityFiltersProvider>
</DatamodelViewProvider>
</Suspense>
)
Expand Down
265 changes: 144 additions & 121 deletions Website/components/datamodelview/Attributes.tsx

Large diffs are not rendered by default.

254 changes: 198 additions & 56 deletions Website/components/datamodelview/DatamodelView.tsx

Large diffs are not rendered by default.

10 changes: 2 additions & 8 deletions Website/components/datamodelview/Keys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ type SortDirection = 'asc' | 'desc' | null

interface IKeysProps {
entity: EntityType;
onVisibleCountChange?: (count: number) => void;
search?: string;
}

function Keys({ entity, onVisibleCountChange, search = "" }: IKeysProps & { search?: string }) {
function Keys({ entity, search = "" }: IKeysProps) {
const [sortColumn, setSortColumn] = useState<SortColumn>("name")
const [sortDirection, setSortDirection] = useState<SortDirection>("asc")
const [searchQuery, setSearchQuery] = useState("")
Expand Down Expand Up @@ -107,12 +107,6 @@ function Keys({ entity, onVisibleCountChange, search = "" }: IKeysProps & { sear
const sortedKeys = getSortedKeys();
const highlightTerm = searchQuery || search; // Use internal search or parent search for highlighting

React.useEffect(() => {
if (onVisibleCountChange) {
onVisibleCountChange(sortedKeys.length);
}
}, [onVisibleCountChange, sortedKeys.length]);

const SortIcon = ({ column }: { column: SortColumn }) => {
if (sortColumn !== column) return <ArrowDownwardRounded className="ml-2 h-4 w-4" />
if (sortDirection === 'asc') return <ArrowUpwardRounded className="ml-2 h-4 w-4" />
Expand Down
86 changes: 50 additions & 36 deletions Website/components/datamodelview/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const List = ({ setCurrentIndex }: IListProps) => {
const parentRef = useRef<HTMLDivElement | null>(null);
// used to relocate section after search/filter
const [sectionVirtualItem, setSectionVirtualItem] = useState<string | null>(null);

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

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

const lowerSearch = search.trim().toLowerCase();
const items: Array<
| { type: 'group'; group: GroupType }
Expand Down Expand Up @@ -78,7 +79,7 @@ export const List = ({ setCurrentIndex }: IListProps) => {
if (!sync) {
dispatch({ type: 'SET_LOADING_SECTION', payload: null });
}

const virtualItems = instance.getVirtualItems();
if (virtualItems.length === 0) return;

Expand All @@ -101,19 +102,19 @@ export const List = ({ setCurrentIndex }: IListProps) => {
const item = flatItems[vi.index];
if (!item || item.type !== 'entity') continue;
actualIndex++;

const itemTop = vi.start;
const itemBottom = vi.end;

// Calculate intersection
const intersectionTop = Math.max(itemTop, viewportTop);
const intersectionBottom = Math.min(itemBottom, viewportBottom);

// Skip if no intersection
if (intersectionTop >= intersectionBottom) continue;

const visibleArea = intersectionBottom - intersectionTop;

// Update most visible entity without array operations
if (!mostVisibleEntity || visibleArea > mostVisibleEntity.visibleArea) {
mostVisibleEntity = {
Expand All @@ -133,7 +134,10 @@ export const List = ({ setCurrentIndex }: IListProps) => {
updateURL({ query: { group: mostVisibleEntity.group.Name, section: mostVisibleEntity.entity.SchemaName } });
dispatch({ type: "SET_CURRENT_GROUP", payload: mostVisibleEntity.group.Name });
dispatch({ type: "SET_CURRENT_SECTION", payload: mostVisibleEntity.entity.SchemaName });
setCurrentIndex(mostVisibleEntity.index);
// Only update the index when not searching - during search, index should only change via next/previous buttons
if (!search) {
setCurrentIndex(mostVisibleEntity.index);
}
}
}, 100);

Expand All @@ -144,16 +148,22 @@ export const List = ({ setCurrentIndex }: IListProps) => {
estimateSize: (index) => {
const item = flatItems[index];
if (!item) return 200;
return item.type === 'group' ? 100 : 500;
return item.type === 'group' ? 100 : 500;
},
onChange: debouncedOnChange,
});


// Set shouldAdjustScrollPositionOnItemSizeChange to prevent scroll position adjustments
// when item sizes change due to filtering/searching
useEffect(() => {
rowVirtualizer.shouldAdjustScrollPositionOnItemSizeChange = () => false;
}, [rowVirtualizer]);

const scrollToSection = useCallback((sectionId: string) => {
const sectionIndex = flatItems.findIndex(item =>
const sectionIndex = flatItems.findIndex(item =>
item.type === 'entity' && item.entity.SchemaName === sectionId
);

if (sectionIndex === -1) {
console.warn(`Section ${sectionId} not found in virtualized list`);
return;
Expand All @@ -163,11 +173,30 @@ export const List = ({ setCurrentIndex }: IListProps) => {

}, [flatItems]);

const scrollToAttribute = useCallback((sectionId: string, attrSchema: string) => {
const attrId = `attr-${sectionId}-${attrSchema}`;
const attributeLocation = document.getElementById(attrId);

if (attributeLocation) {
// Attribute is already rendered, scroll directly to it
attributeLocation.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
// Attribute not found, need to scroll to section first
scrollToSection(sectionId);
setTimeout(() => {
const attributeLocationAfterScroll = document.getElementById(attrId);
if (attributeLocationAfterScroll) {
attributeLocationAfterScroll.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
}
}, [scrollToSection]);

const scrollToGroup = useCallback((groupName: string) => {
const groupIndex = flatItems.findIndex(item =>
const groupIndex = flatItems.findIndex(item =>
item.type === 'group' && item.group.Name === groupName
);

if (groupIndex === -1) {
console.warn(`Group ${groupName} not found in virtualized list`);
return;
Expand All @@ -184,19 +213,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_GROUP', payload: scrollToGroup });
dispatch({ type: 'SET_RESTORE_SECTION', payload: restoreSection });
}, [dispatch, scrollToSection, scrollToGroup]);

// Callback to handle section content changes (for tab switches, expansions, etc.)
const handleSectionResize = useCallback((index: number) => {
if (index !== -1) {
const containerElement = document.querySelector(`[data-index="${index}"]`) as HTMLElement;
if (containerElement) {
rowVirtualizer.measureElement(containerElement);
}
}
}, [rowVirtualizer]);
}, [dispatch, scrollToSection, scrollToAttribute, scrollToGroup]);

const smartScrollToIndex = useCallback((index: number) => {
rowVirtualizer.scrollToIndex(index, { align: 'start' });
Expand All @@ -213,7 +233,7 @@ export const List = ({ setCurrentIndex }: IListProps) => {
});
};
requestAnimationFrame(tryFix);
}, [rowVirtualizer]);
}, [rowVirtualizer]);

return (
<>
Expand All @@ -231,7 +251,7 @@ export const List = ({ setCurrentIndex }: IListProps) => {
</div>
</div>
)}

{/* Virtualized list */}
<div
className={`mx-6 my-6 transition-opacity duration-300 ${loadingSection ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}
Expand All @@ -258,18 +278,15 @@ export const List = ({ setCurrentIndex }: IListProps) => {
}}
ref={(el) => {
if (el) {
// trigger remeasurement when content changes and load
requestAnimationFrame(() => {
handleSectionResize(virtualItem.index);
});
rowVirtualizer.measureElement(el);
}
}}
>
{item.type === 'group' ? (
<div className="flex items-center py-6 my-4">
<div className="flex-1 h-0.5 bg-gray-200" />
<Tooltip title="Copy link to this group">
<div
<div
className="px-4 text-md font-semibold text-gray-700 uppercase tracking-wide whitespace-nowrap cursor-pointer hover:text-blue-600 transition-colors"
onClick={() => handleCopyGroupLink(item.group.Name)}
>
Expand All @@ -283,9 +300,6 @@ export const List = ({ setCurrentIndex }: IListProps) => {
<Section
entity={item.entity}
group={item.group}
onTabChange={() => {

}}
search={search}
/>
</div>
Expand Down
20 changes: 10 additions & 10 deletions Website/components/datamodelview/Relationships.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { EntityType } from "@/lib/Types"
import { CascadeConfiguration } from "./entity/CascadeConfiguration"
import { useState } from "react"
import { useState, useEffect } from "react"
import { useDatamodelView, useDatamodelViewDispatch } from "@/contexts/DatamodelViewContext"
import React from "react"
import { highlightMatch } from "../datamodelview/List";
Expand All @@ -14,15 +14,16 @@ type SortColumn = 'name' | 'tableSchema' | 'lookupField' | 'type' | 'behavior' |

interface IRelationshipsProps {
entity: EntityType;
search?: string;
onVisibleCountChange?: (count: number) => void;
}

export const Relationships = ({ entity, onVisibleCountChange, search = "" }: IRelationshipsProps & { search?: string }) => {
export const Relationships = ({ entity, search = "", onVisibleCountChange }: IRelationshipsProps) => {
const [sortColumn, setSortColumn] = useState<SortColumn>("name")
const [sortDirection, setSortDirection] = useState<SortDirection>("asc")
const [typeFilter, setTypeFilter] = useState<string>("all")
const [searchQuery, setSearchQuery] = useState("")

const theme = useTheme();

const dispatch = useDatamodelViewDispatch();
Expand Down Expand Up @@ -130,11 +131,10 @@ export const Relationships = ({ entity, onVisibleCountChange, search = "" }: IRe
const sortedRelationships = getSortedRelationships();
const highlightTerm = searchQuery || search; // Use internal search or parent search for highlighting

React.useEffect(() => {
if (onVisibleCountChange) {
onVisibleCountChange(sortedRelationships.length);
}
}, [onVisibleCountChange, sortedRelationships.length]);
// Notify parent of visible count changes
useEffect(() => {
onVisibleCountChange?.(sortedRelationships.length);
}, [sortedRelationships.length, onVisibleCountChange]);

return <>
<Box
Expand Down Expand Up @@ -184,8 +184,8 @@ export const Relationships = ({ entity, onVisibleCountChange, search = "" }: IRe
<InputLabel id="relationship-type-filter-label" className="text-xs md:text-sm">
Filter by type
</InputLabel>
<Select
value={typeFilter}
<Select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
label="Filter by type"
labelId="relationship-type-filter-label"
Expand Down
Loading
Loading