Skip to content

Commit

Permalink
fix: flickering on modules and graph pages (#4251)
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman authored Jan 31, 2025
1 parent 438fee6 commit 90a2fc4
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 25 deletions.
8 changes: 4 additions & 4 deletions frontend/console/src/features/graph/GraphPage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { useState } from 'react'
import { Loader } from '../../shared/components/Loader'
import { ResizablePanels } from '../../shared/components/ResizablePanels'
import { useModules } from '../modules/hooks/use-modules'
import { useStreamModules } from '../modules/hooks/use-stream-modules'
import { Timeline } from '../timeline/Timeline'
import { GraphPane } from './GraphPane'
import { headerForNode } from './RightPanelHeader'
import { type FTLNode, panelsForNode } from './graph-utils'

export const GraphPage = () => {
const modules = useModules()
const { data, isLoading } = useStreamModules()
const [selectedNode, setSelectedNode] = useState<FTLNode | null>(null)
const [selectedModuleName, setSelectedModuleName] = useState<string | null>(null)

if (!modules.isSuccess) {
if (isLoading) {
return (
<div className='flex justify-center items-center h-full'>
<Loader />
Expand All @@ -28,7 +28,7 @@ export const GraphPage = () => {
return (
<div className='flex h-full'>
<ResizablePanels
mainContent={<GraphPane onTapped={handleNodeTapped} />}
mainContent={<GraphPane modules={data} onTapped={handleNodeTapped} />}
rightPanelHeader={headerForNode(selectedNode, selectedModuleName)}
rightPanelPanels={panelsForNode(selectedNode, selectedModuleName)}
bottomPanelContent={<Timeline timeSettings={{ isTailing: true, isPaused: false }} filters={[]} />}
Expand Down
31 changes: 22 additions & 9 deletions frontend/console/src/features/graph/GraphPane.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Background, BackgroundVariant, Controls, type Edge, ReactFlow as Flow, type Node, ReactFlowProvider } from '@xyflow/react'
import dagre from 'dagre'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import type React from 'react'
import { useUserPreferences } from '../../shared/providers/user-preferences-provider'
import { useStreamModules } from '../modules/hooks/use-stream-modules'
import { hashString } from '../../shared/utils/string.utils'
import type { StreamModulesResult } from '../modules/hooks/use-stream-modules'
import { DeclNode } from './DeclNode'
import { GroupNode } from './GroupNode'
import { type FTLNode, getGraphData } from './graph-utils'
Expand All @@ -16,6 +17,7 @@ const NODE_TYPES = {
}

interface GraphPaneProps {
modules?: StreamModulesResult
onTapped?: (item: FTLNode | null, moduleName: string | null) => void
}

Expand Down Expand Up @@ -243,15 +245,28 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'LR') =>
return { nodes, edges }
}

export const GraphPane: React.FC<GraphPaneProps> = ({ onTapped }) => {
const modules = useStreamModules()
export const GraphPane: React.FC<GraphPaneProps> = ({ modules, onTapped }) => {
const { isDarkMode } = useUserPreferences()
const [nodePositions] = useState<Record<string, { x: number; y: number }>>({})
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)
const [moduleKey, setModuleKey] = useState<string>('empty')

useEffect(() => {
const updateKey = async () => {
if (!modules?.modules) {
setModuleKey('empty')
return
}
const fullKey = modules.modules.map((m) => `${m.name}:${m.schema}`).join('-')
const hash = await hashString(fullKey)
setModuleKey(hash)
}
updateKey()
}, [modules])

const { nodes, edges } = useMemo(() => {
return getGraphData(modules.data, isDarkMode, nodePositions, selectedNodeId)
}, [modules.data, isDarkMode, nodePositions, selectedNodeId])
return getGraphData(modules, isDarkMode, nodePositions, selectedNodeId)
}, [modules, isDarkMode, nodePositions, selectedNodeId])

const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => {
if (!nodes.length) return { nodes: [], edges: [] }
Expand All @@ -268,16 +283,13 @@ export const GraphPane: React.FC<GraphPaneProps> = ({ onTapped }) => {

const onEdgeClick = useCallback(
(_event: React.MouseEvent, edge: Edge) => {
// Find the source and target nodes of the clicked edge
const sourceNode = layoutedNodes.find((n) => n.id === edge.source)
const targetNode = layoutedNodes.find((n) => n.id === edge.target)

// If either node is already selected, clear selection
if (sourceNode?.id === selectedNodeId || targetNode?.id === selectedNodeId) {
setSelectedNodeId(null)
onTapped?.(null, null)
} else {
// Otherwise select the source node
setSelectedNodeId(sourceNode?.id || null)
onTapped?.((sourceNode?.data?.item as FTLNode) || null, sourceNode?.id || null)
}
Expand All @@ -294,6 +306,7 @@ export const GraphPane: React.FC<GraphPaneProps> = ({ onTapped }) => {
<ReactFlowProvider>
<div className={isDarkMode ? 'dark' : 'light'} style={{ width: '100%', height: '100%', position: 'relative' }}>
<Flow
key={moduleKey}
nodes={layoutedNodes}
edges={layoutedEdges}
nodeTypes={NODE_TYPES}
Expand Down
10 changes: 7 additions & 3 deletions frontend/console/src/features/modules/ModulesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ import { AttributeBadge } from '../../shared/components/AttributeBadge'
import { List } from '../../shared/components/List'
import { classNames } from '../../shared/utils'
import { deploymentTextColor } from '../deployments/deployment.utils'
import { useModules } from './hooks/use-modules'
import { useStreamModules } from './hooks/use-stream-modules'
import { sortModules } from './module.utils'

export const ModulesPanel = () => {
const modules = useModules()
const { data } = useStreamModules()
const modules = sortModules(data?.modules ?? [])

const moduleHref = (module: Module) => `/modules/${module.name}`
const moduleKey = (module: Module) => module.name

return (
<div className='p-2'>
<List
items={modules.data?.modules ?? []}
items={modules}
href={moduleHref}
keyExtractor={moduleKey}
renderItem={(module) => (
<div className='flex w-full' data-module-row={module.name}>
<div className='flex gap-x-4 items-center w-1/2'>
Expand Down
7 changes: 5 additions & 2 deletions frontend/console/src/features/modules/ModulesTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
listExpandedModulesFromLocalStorage,
setExpandedDeclTypesInLocalStorage,
setHideUnexportedFromLocalStorage,
sortModules,
toggleModuleExpansionInLocalStorage,
} from './module.utils'
import { declTypeMultiselectOpts } from './schema/schema.utils'
Expand Down Expand Up @@ -257,7 +258,9 @@ export const ModulesTree = ({ modules }: { modules: ModuleTreeItem[] }) => {
setExpandedDeclTypesInLocalStorage(newExpanded)
}

modules.sort((m1, m2) => Number(m1.isBuiltin) - Number(m2.isBuiltin))
const sortedModules = useMemo(() => {
return sortModules(modules)
}, [modules])

return (
<div className='flex flex-col h-full border-r border-gray-300 dark:border-gray-700'>
Expand All @@ -278,7 +281,7 @@ export const ModulesTree = ({ modules }: { modules: ModuleTreeItem[] }) => {
</div>
<nav className='overflow-y-auto flex-1'>
<ul id='module-tree-content' className='p-2'>
{modules.map((m) => (
{sortedModules.map((m) => (
<ModuleSection
key={m.name}
module={m}
Expand Down
10 changes: 10 additions & 0 deletions frontend/console/src/features/modules/module.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,13 @@ export const getExpandedDeclTypesFromLocalStorage = (): string[] => {
export const setExpandedDeclTypesInLocalStorage = (types: string[]) => {
localStorage.setItem(EXPANDED_DECL_TYPES_KEY, JSON.stringify(types))
}

export const sortModules = <T extends { name: string }>(modules: T[]) => {
return [...modules].sort((m1, m2) => {
// First sort by builtin status (builtin goes to bottom)
const builtinDiff = Number(m1.name === 'builtin') - Number(m2.name === 'builtin')
if (builtinDiff !== 0) return builtinDiff
// Then sort alphabetically by name
return m1.name.localeCompare(m2.name)
})
}
16 changes: 9 additions & 7 deletions frontend/console/src/shared/components/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,26 @@ type ListProps<T> = {
renderItem: (item: T) => React.ReactNode
href?: (item: T) => string
className?: string
keyExtractor?: (item: T) => string | number
}

export const List = <T,>({ items, renderItem, href, className }: ListProps<T>) => {
export const List = <T,>({ items, renderItem, href, className, keyExtractor }: ListProps<T>) => {
const baseClasses = 'relative flex justify-between items-center gap-x-4 p-4'
return (
<ul className={classNames('divide-y divide-gray-100 dark:divide-gray-700 overflow-hidden', className)}>
{items.map((item, index) =>
href ? (
<Link key={index} className={`${baseClasses} cursor-pointer hover:bg-gray-100/50 dark:hover:bg-gray-700/50`} to={href(item)}>
{items.map((item, index) => {
const key = keyExtractor ? keyExtractor(item) : index
return href ? (
<Link key={key} className={`${baseClasses} cursor-pointer hover:bg-gray-100/50 dark:hover:bg-gray-700/50`} to={href(item)}>
{renderItem(item)}
<ArrowRight01Icon className='size-5 text-gray-400' />
</Link>
) : (
<li key={index} className={baseClasses}>
<li key={key} className={baseClasses}>
{renderItem(item)}
</li>
),
)}
)
})}
</ul>
)
}
10 changes: 10 additions & 0 deletions frontend/console/src/shared/utils/string.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const hashString = async (str: string) => {
const encoder = new TextEncoder()
const data = encoder.encode(str)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
.slice(0, 8)
}

0 comments on commit 90a2fc4

Please sign in to comment.