add label filter
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
"graphology": "^0.26.0",
|
"graphology": "^0.26.0",
|
||||||
"graphology-generators": "^0.11.2",
|
"graphology-generators": "^0.11.2",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
|
"minisearch": "^7.1.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
|
@@ -33,6 +33,7 @@
|
|||||||
"graphology": "^0.26.0",
|
"graphology": "^0.26.0",
|
||||||
"graphology-generators": "^0.11.2",
|
"graphology-generators": "^0.11.2",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
|
"minisearch": "^7.1.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
|
@@ -1,18 +1,28 @@
|
|||||||
import ThemeProvider from '@/components/ThemeProvider'
|
import ThemeProvider from '@/components/ThemeProvider'
|
||||||
import BackendMessageAlert from '@/components/BackendMessageAlert'
|
import MessageAlert from '@/components/MessageAlert'
|
||||||
import { GraphViewer } from '@/GraphViewer'
|
import { GraphViewer } from '@/GraphViewer'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { healthCheckInterval } from '@/lib/constants'
|
||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const health = useBackendState.use.health()
|
const message = useBackendState.use.message()
|
||||||
|
|
||||||
|
// health check
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
await useBackendState.getState().check()
|
||||||
|
}, healthCheckInterval * 1000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<div className={cn('h-screen w-screen', !health && 'pointer-events-none')}>
|
<div className={cn('h-screen w-screen', message !== null && 'pointer-events-none')}>
|
||||||
<GraphViewer />
|
<GraphViewer />
|
||||||
</div>
|
</div>
|
||||||
{!health && <BackendMessageAlert />}
|
{message !== null && <MessageAlert />}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ import ZoomControl from '@/components/ZoomControl'
|
|||||||
import FullScreenControl from '@/components/FullScreenControl'
|
import FullScreenControl from '@/components/FullScreenControl'
|
||||||
import Settings from '@/components/Settings'
|
import Settings from '@/components/Settings'
|
||||||
import GraphSearch from '@/components/GraphSearch'
|
import GraphSearch from '@/components/GraphSearch'
|
||||||
|
import GraphLabels from '@/components/GraphLabels'
|
||||||
import PropertiesView from '@/components/PropertiesView'
|
import PropertiesView from '@/components/PropertiesView'
|
||||||
|
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
@@ -144,9 +145,9 @@ export const GraphViewer = () => {
|
|||||||
|
|
||||||
<FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
|
<FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
|
||||||
|
|
||||||
<div className="absolute top-2 left-2">
|
<div className="absolute top-2 left-2 flex items-start gap-2">
|
||||||
|
<GraphLabels />
|
||||||
<GraphSearch
|
<GraphSearch
|
||||||
type="nodes"
|
|
||||||
value={searchInitSelectedNode}
|
value={searchInitSelectedNode}
|
||||||
onFocus={onSearchFocus}
|
onFocus={onSearchFocus}
|
||||||
onChange={onSearchSelect}
|
onChange={onSearchSelect}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { backendBaseUrl } from '@/lib/constants'
|
import { backendBaseUrl } from '@/lib/constants'
|
||||||
|
import { errorMessage } from '@/lib/utils'
|
||||||
|
|
||||||
export type LightragNodeType = {
|
export type LightragNodeType = {
|
||||||
id: string
|
id: string
|
||||||
@@ -81,7 +82,7 @@ export const checkHealth = async (): Promise<
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: e instanceof Error ? e.message : `${e}`
|
message: errorMessage(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
|
|
||||||
import { useBackendState } from '@/stores/state'
|
|
||||||
import { AlertCircle } from 'lucide-react'
|
|
||||||
|
|
||||||
const BackendMessageAlert = () => {
|
|
||||||
const health = useBackendState.use.health()
|
|
||||||
const message = useBackendState.use.message()
|
|
||||||
const messageTitle = useBackendState.use.messageTitle()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
variant={health ? 'default' : 'destructive'}
|
|
||||||
className="absolute top-1/2 left-1/2 w-auto -translate-x-1/2 -translate-y-1/2 transform"
|
|
||||||
>
|
|
||||||
{!health && <AlertCircle className="h-4 w-4" />}
|
|
||||||
<AlertTitle>{messageTitle}</AlertTitle>
|
|
||||||
<AlertDescription>{message}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BackendMessageAlert
|
|
@@ -0,0 +1,87 @@
|
|||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import { AsyncSelect } from '@/components/ui/AsyncSelect'
|
||||||
|
import { getGraphLabels } from '@/api/lightrag'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import MiniSearch from 'minisearch'
|
||||||
|
|
||||||
|
const GraphLabels = () => {
|
||||||
|
const label = useSettingsStore.use.queryLabel()
|
||||||
|
const [labels, setLabels] = useState<{
|
||||||
|
labels: string[]
|
||||||
|
searchEngine: MiniSearch | null
|
||||||
|
}>({
|
||||||
|
labels: [],
|
||||||
|
searchEngine: null
|
||||||
|
})
|
||||||
|
const [fetched, setFetched] = useState(false)
|
||||||
|
|
||||||
|
const fetchData = useCallback(
|
||||||
|
async (query?: string): Promise<string[]> => {
|
||||||
|
let _labels = labels.labels
|
||||||
|
let _searchEngine = labels.searchEngine
|
||||||
|
|
||||||
|
if (!fetched || !_searchEngine) {
|
||||||
|
_labels = ['*'].concat(await getGraphLabels())
|
||||||
|
|
||||||
|
// Ensure query label exists
|
||||||
|
if (!_labels.includes(useSettingsStore.getState().queryLabel)) {
|
||||||
|
useSettingsStore.getState().setQueryLabel(_labels[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create search engine
|
||||||
|
_searchEngine = new MiniSearch({
|
||||||
|
idField: 'id',
|
||||||
|
fields: ['value'],
|
||||||
|
searchOptions: {
|
||||||
|
prefix: true,
|
||||||
|
fuzzy: 0.2,
|
||||||
|
boost: {
|
||||||
|
label: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add documents
|
||||||
|
const documents = _labels.map((str, index) => ({ id: index, value: str }))
|
||||||
|
_searchEngine.addAll(documents)
|
||||||
|
|
||||||
|
setLabels({
|
||||||
|
labels: _labels,
|
||||||
|
searchEngine: _searchEngine
|
||||||
|
})
|
||||||
|
setFetched(true)
|
||||||
|
}
|
||||||
|
if (!query) {
|
||||||
|
return _labels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search labels
|
||||||
|
return _searchEngine.search(query).map((result) => _labels[result.id])
|
||||||
|
},
|
||||||
|
[labels, fetched, setLabels, setFetched]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setQueryLabel = useCallback((label: string) => {
|
||||||
|
useSettingsStore.getState().setQueryLabel(label)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncSelect<string>
|
||||||
|
className="ml-2"
|
||||||
|
triggerClassName="max-h-8"
|
||||||
|
searchInputClassName="max-h-8"
|
||||||
|
triggerTooltip="Select query label"
|
||||||
|
fetcher={fetchData}
|
||||||
|
renderOption={(item) => <div>{item}</div>}
|
||||||
|
getOptionValue={(item) => item}
|
||||||
|
getDisplayValue={(item) => <div>{item}</div>}
|
||||||
|
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
||||||
|
label="Label"
|
||||||
|
placeholder="Search labels..."
|
||||||
|
value={label !== null ? label : ''}
|
||||||
|
onChange={setQueryLabel}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GraphLabels
|
@@ -1,14 +1,14 @@
|
|||||||
import { FC, useCallback } from 'react'
|
import { FC, useCallback, useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
EdgeById,
|
EdgeById,
|
||||||
NodeById,
|
NodeById,
|
||||||
useGraphSearch,
|
|
||||||
GraphSearchInputProps,
|
GraphSearchInputProps,
|
||||||
GraphSearchContextProvider,
|
|
||||||
GraphSearchContextProviderProps
|
GraphSearchContextProviderProps
|
||||||
} from '@react-sigma/graph-search'
|
} from '@react-sigma/graph-search'
|
||||||
import { AsyncSelect } from '@/components/ui/AsyncSelect'
|
import { AsyncSearch } from '@/components/ui/AsyncSearch'
|
||||||
import { searchResultLimit } from '@/lib/constants'
|
import { searchResultLimit } from '@/lib/constants'
|
||||||
|
import { useGraphStore } from '@/stores/graph'
|
||||||
|
import MiniSearch from 'minisearch'
|
||||||
|
|
||||||
interface OptionItem {
|
interface OptionItem {
|
||||||
id: string
|
id: string
|
||||||
@@ -27,6 +27,10 @@ function OptionComponent(item: OptionItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messageId = '__message_item'
|
const messageId = '__message_item'
|
||||||
|
const lastGraph: any = {
|
||||||
|
graph: null,
|
||||||
|
searchEngine: null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component thats display the search input.
|
* Component thats display the search input.
|
||||||
@@ -34,15 +38,44 @@ const messageId = '__message_item'
|
|||||||
export const GraphSearchInput = ({
|
export const GraphSearchInput = ({
|
||||||
onChange,
|
onChange,
|
||||||
onFocus,
|
onFocus,
|
||||||
type,
|
|
||||||
value
|
value
|
||||||
}: {
|
}: {
|
||||||
onChange: GraphSearchInputProps['onChange']
|
onChange: GraphSearchInputProps['onChange']
|
||||||
onFocus?: GraphSearchInputProps['onFocus']
|
onFocus?: GraphSearchInputProps['onFocus']
|
||||||
type?: GraphSearchInputProps['type']
|
|
||||||
value?: GraphSearchInputProps['value']
|
value?: GraphSearchInputProps['value']
|
||||||
}) => {
|
}) => {
|
||||||
const { search } = useGraphSearch()
|
const graph = useGraphStore.use.sigmaGraph()
|
||||||
|
|
||||||
|
const search = useMemo(() => {
|
||||||
|
if (lastGraph.graph == graph) {
|
||||||
|
return lastGraph.searchEngine
|
||||||
|
}
|
||||||
|
if (!graph || graph.nodes().length == 0) return
|
||||||
|
|
||||||
|
lastGraph.graph = graph
|
||||||
|
|
||||||
|
const searchEngine = new MiniSearch({
|
||||||
|
idField: 'id',
|
||||||
|
fields: ['label'],
|
||||||
|
searchOptions: {
|
||||||
|
prefix: true,
|
||||||
|
fuzzy: 0.2,
|
||||||
|
boost: {
|
||||||
|
label: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add documents
|
||||||
|
const documents = graph.nodes().map((id: string) => ({
|
||||||
|
id: id,
|
||||||
|
label: graph.getNodeAttribute(id, 'label')
|
||||||
|
}))
|
||||||
|
searchEngine.addAll(documents)
|
||||||
|
|
||||||
|
lastGraph.searchEngine = searchEngine
|
||||||
|
return searchEngine
|
||||||
|
}, [graph])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loading the options while the user is typing.
|
* Loading the options while the user is typing.
|
||||||
@@ -50,8 +83,11 @@ export const GraphSearchInput = ({
|
|||||||
const loadOptions = useCallback(
|
const loadOptions = useCallback(
|
||||||
async (query?: string): Promise<OptionItem[]> => {
|
async (query?: string): Promise<OptionItem[]> => {
|
||||||
if (onFocus) onFocus(null)
|
if (onFocus) onFocus(null)
|
||||||
if (!query) return []
|
if (!query || !search) return []
|
||||||
const result = (await search(query, type)) as OptionItem[]
|
const result: OptionItem[] = search.search(query).map((result) => ({
|
||||||
|
id: result.id,
|
||||||
|
type: 'nodes'
|
||||||
|
}))
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return result.length <= searchResultLimit
|
return result.length <= searchResultLimit
|
||||||
@@ -65,25 +101,24 @@ export const GraphSearchInput = ({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[type, search, onFocus]
|
[search, onFocus]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncSelect
|
<AsyncSearch
|
||||||
className="bg-background/60 w-52 rounded-xl border-1 opacity-60 backdrop-blur-lg transition-opacity hover:opacity-100"
|
className="bg-background/60 w-24 rounded-xl border-1 opacity-60 backdrop-blur-lg transition-all hover:w-fit hover:opacity-100"
|
||||||
fetcher={loadOptions}
|
fetcher={loadOptions}
|
||||||
renderOption={OptionComponent}
|
renderOption={OptionComponent}
|
||||||
getOptionValue={(item) => item.id}
|
getOptionValue={(item) => item.id}
|
||||||
value={value && value.type !== 'message' ? value.id : null}
|
value={value && value.type !== 'message' ? value.id : null}
|
||||||
onChange={(id) => {
|
onChange={(id) => {
|
||||||
if (id !== messageId && type) onChange(id ? { id, type } : null)
|
if (id !== messageId) onChange(id ? { id, type: 'nodes' } : null)
|
||||||
}}
|
}}
|
||||||
onFocus={(id) => {
|
onFocus={(id) => {
|
||||||
if (id !== messageId && onFocus && type) onFocus(id ? { id, type } : null)
|
if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
|
||||||
}}
|
}}
|
||||||
label={'item'}
|
label={'item'}
|
||||||
preload={false}
|
placeholder="Search nodes..."
|
||||||
placeholder="Type search here..."
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -91,13 +126,8 @@ export const GraphSearchInput = ({
|
|||||||
/**
|
/**
|
||||||
* Component that display the search.
|
* Component that display the search.
|
||||||
*/
|
*/
|
||||||
const GraphSearch: FC<GraphSearchInputProps & GraphSearchContextProviderProps> = ({
|
const GraphSearch: FC<GraphSearchInputProps & GraphSearchContextProviderProps> = ({ ...props }) => {
|
||||||
minisearchOptions,
|
return <GraphSearchInput {...props} />
|
||||||
...props
|
}
|
||||||
}) => (
|
|
||||||
<GraphSearchContextProvider minisearchOptions={minisearchOptions}>
|
|
||||||
<GraphSearchInput {...props} />
|
|
||||||
</GraphSearchContextProvider>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default GraphSearch
|
export default GraphSearch
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import { useBackendState } from '@/stores/state'
|
||||||
|
import { controlButtonVariant } from '@/lib/constants'
|
||||||
|
|
||||||
|
import { AlertCircle } from 'lucide-react'
|
||||||
|
|
||||||
|
const MessageAlert = () => {
|
||||||
|
const health = useBackendState.use.health()
|
||||||
|
const message = useBackendState.use.message()
|
||||||
|
const messageTitle = useBackendState.use.messageTitle()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
variant={health ? 'default' : 'destructive'}
|
||||||
|
className="bg-background/90 absolute top-1/2 left-1/2 w-auto -translate-x-1/2 -translate-y-1/2 transform backdrop-blur-lg"
|
||||||
|
>
|
||||||
|
{!health && <AlertCircle className="h-4 w-4" />}
|
||||||
|
<AlertTitle>{messageTitle}</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
<div className="h-2" />
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-auto" />
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={controlButtonVariant}
|
||||||
|
className="text-primary max-h-8 border !p-2 text-xs"
|
||||||
|
onClick={() => useBackendState.getState().clear()}
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MessageAlert
|
@@ -0,0 +1,243 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { Loader2 } from 'lucide-react'
|
||||||
|
import { useDebounce } from '@/hooks/useDebounce'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList
|
||||||
|
} from '@/components/ui/Command'
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
disabled?: boolean
|
||||||
|
description?: string
|
||||||
|
icon?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsyncSearchProps<T> {
|
||||||
|
/** Async function to fetch options */
|
||||||
|
fetcher: (query?: string) => Promise<T[]>
|
||||||
|
/** Preload all data ahead of time */
|
||||||
|
preload?: boolean
|
||||||
|
/** Function to filter options */
|
||||||
|
filterFn?: (option: T, query: string) => boolean
|
||||||
|
/** Function to render each option */
|
||||||
|
renderOption: (option: T) => React.ReactNode
|
||||||
|
/** Function to get the value from an option */
|
||||||
|
getOptionValue: (option: T) => string
|
||||||
|
/** Custom not found message */
|
||||||
|
notFound?: React.ReactNode
|
||||||
|
/** Custom loading skeleton */
|
||||||
|
loadingSkeleton?: React.ReactNode
|
||||||
|
/** Currently selected value */
|
||||||
|
value: string | null
|
||||||
|
/** Callback when selection changes */
|
||||||
|
onChange: (value: string) => void
|
||||||
|
/** Callback when focus changes */
|
||||||
|
onFocus: (value: string) => void
|
||||||
|
/** Label for the select field */
|
||||||
|
label: string
|
||||||
|
/** Placeholder text when no selection */
|
||||||
|
placeholder?: string
|
||||||
|
/** Disable the entire select */
|
||||||
|
disabled?: boolean
|
||||||
|
/** Custom width for the popover */
|
||||||
|
width?: string | number
|
||||||
|
/** Custom class names */
|
||||||
|
className?: string
|
||||||
|
/** Custom trigger button class names */
|
||||||
|
triggerClassName?: string
|
||||||
|
/** Custom no results message */
|
||||||
|
noResultsMessage?: string
|
||||||
|
/** Allow clearing the selection */
|
||||||
|
clearable?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AsyncSearch<T>({
|
||||||
|
fetcher,
|
||||||
|
preload,
|
||||||
|
filterFn,
|
||||||
|
renderOption,
|
||||||
|
getOptionValue,
|
||||||
|
notFound,
|
||||||
|
loadingSkeleton,
|
||||||
|
label,
|
||||||
|
placeholder = 'Select...',
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onFocus,
|
||||||
|
disabled = false,
|
||||||
|
className,
|
||||||
|
noResultsMessage
|
||||||
|
}: AsyncSearchProps<T>) {
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [options, setOptions] = useState<T[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [selectedValue, setSelectedValue] = useState(value)
|
||||||
|
const [focusedValue, setFocusedValue] = useState<string | null>(null)
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const debouncedSearchTerm = useDebounce(searchTerm, preload ? 0 : 150)
|
||||||
|
const [originalOptions, setOriginalOptions] = useState<T[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
setSelectedValue(value)
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
// Effect for initial fetch
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeOptions = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
// If we have a value, use it for the initial search
|
||||||
|
const data = value !== null ? await fetcher(value) : []
|
||||||
|
setOriginalOptions(data)
|
||||||
|
setOptions(data)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch options')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
initializeOptions()
|
||||||
|
}
|
||||||
|
}, [mounted, fetcher, value])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOptions = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
const data = await fetcher(debouncedSearchTerm)
|
||||||
|
setOriginalOptions(data)
|
||||||
|
setOptions(data)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch options')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
fetchOptions()
|
||||||
|
} else if (!preload) {
|
||||||
|
fetchOptions()
|
||||||
|
} else if (preload) {
|
||||||
|
if (debouncedSearchTerm) {
|
||||||
|
setOptions(
|
||||||
|
originalOptions.filter((option) =>
|
||||||
|
filterFn ? filterFn(option, debouncedSearchTerm) : true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setOptions(originalOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [fetcher, debouncedSearchTerm, mounted, preload, filterFn])
|
||||||
|
|
||||||
|
const handleSelect = useCallback(
|
||||||
|
(currentValue: string) => {
|
||||||
|
if (currentValue !== selectedValue) {
|
||||||
|
setSelectedValue(currentValue)
|
||||||
|
onChange(currentValue)
|
||||||
|
}
|
||||||
|
setOpen(false)
|
||||||
|
},
|
||||||
|
[selectedValue, setSelectedValue, setOpen, onChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleFocus = useCallback(
|
||||||
|
(currentValue: string) => {
|
||||||
|
if (currentValue !== focusedValue) {
|
||||||
|
setFocusedValue(currentValue)
|
||||||
|
onFocus(currentValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[focusedValue, setFocusedValue, onFocus]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(disabled && 'cursor-not-allowed opacity-50', className)}
|
||||||
|
onFocus={() => {
|
||||||
|
setOpen(true)
|
||||||
|
}}
|
||||||
|
onBlur={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Command shouldFilter={false} className="bg-transparent">
|
||||||
|
<div>
|
||||||
|
<CommandInput
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={searchTerm}
|
||||||
|
className="max-h-8"
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSearchTerm(value)
|
||||||
|
if (value && !open) setOpen(true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{loading && options.length > 0 && (
|
||||||
|
<div className="absolute top-1/2 right-2 flex -translate-y-1/2 transform items-center">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<CommandList className="max-h-auto" hidden={!open || debouncedSearchTerm.length === 0}>
|
||||||
|
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
||||||
|
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
||||||
|
{!loading &&
|
||||||
|
!error &&
|
||||||
|
options.length === 0 &&
|
||||||
|
(notFound || (
|
||||||
|
<CommandEmpty>{noResultsMessage ?? `No ${label.toLowerCase()} found.`}</CommandEmpty>
|
||||||
|
))}
|
||||||
|
<CommandGroup>
|
||||||
|
{options.map((option, idx) => (
|
||||||
|
<>
|
||||||
|
<CommandItem
|
||||||
|
key={getOptionValue(option) + `${idx}`}
|
||||||
|
value={getOptionValue(option)}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
onMouseEnter={() => handleFocus(getOptionValue(option))}
|
||||||
|
className="truncate"
|
||||||
|
>
|
||||||
|
{renderOption(option)}
|
||||||
|
</CommandItem>
|
||||||
|
{idx !== options.length - 1 && (
|
||||||
|
<div key={idx} className="bg-foreground/10 h-[1px]" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DefaultLoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem disabled>
|
||||||
|
<div className="flex w-full items-center gap-2">
|
||||||
|
<div className="bg-muted h-6 w-6 animate-pulse rounded-full" />
|
||||||
|
<div className="flex flex-1 flex-col gap-1">
|
||||||
|
<div className="bg-muted h-4 w-24 animate-pulse rounded" />
|
||||||
|
<div className="bg-muted h-3 w-16 animate-pulse rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
)
|
||||||
|
}
|
@@ -1,8 +1,9 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Loader2 } from 'lucide-react'
|
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
|
||||||
import { useDebounce } from '@/hooks/useDebounce'
|
import { useDebounce } from '@/hooks/useDebounce'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList
|
CommandList
|
||||||
} from '@/components/ui/Command'
|
} from '@/components/ui/Command'
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||||
|
|
||||||
export interface Option {
|
export interface Option {
|
||||||
value: string
|
value: string
|
||||||
@@ -31,30 +33,34 @@ export interface AsyncSelectProps<T> {
|
|||||||
renderOption: (option: T) => React.ReactNode
|
renderOption: (option: T) => React.ReactNode
|
||||||
/** Function to get the value from an option */
|
/** Function to get the value from an option */
|
||||||
getOptionValue: (option: T) => string
|
getOptionValue: (option: T) => string
|
||||||
|
/** Function to get the display value for the selected option */
|
||||||
|
getDisplayValue: (option: T) => React.ReactNode
|
||||||
/** Custom not found message */
|
/** Custom not found message */
|
||||||
notFound?: React.ReactNode
|
notFound?: React.ReactNode
|
||||||
/** Custom loading skeleton */
|
/** Custom loading skeleton */
|
||||||
loadingSkeleton?: React.ReactNode
|
loadingSkeleton?: React.ReactNode
|
||||||
/** Currently selected value */
|
/** Currently selected value */
|
||||||
value: string | null
|
value: string
|
||||||
/** Callback when selection changes */
|
/** Callback when selection changes */
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
/** Callback when focus changes */
|
|
||||||
onFocus: (value: string) => void
|
|
||||||
/** Label for the select field */
|
/** Label for the select field */
|
||||||
label: string
|
label: string
|
||||||
/** Placeholder text when no selection */
|
/** Placeholder text when no selection */
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
/** Disable the entire select */
|
/** Disable the entire select */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
/** Custom width for the popover */
|
/** Custom width for the popover *
|
||||||
width?: string | number
|
width?: string | number
|
||||||
/** Custom class names */
|
/** Custom class names */
|
||||||
className?: string
|
className?: string
|
||||||
/** Custom trigger button class names */
|
/** Custom trigger button class names */
|
||||||
triggerClassName?: string
|
triggerClassName?: string
|
||||||
|
/** Custom search input class names */
|
||||||
|
searchInputClassName?: string
|
||||||
/** Custom no results message */
|
/** Custom no results message */
|
||||||
noResultsMessage?: string
|
noResultsMessage?: string
|
||||||
|
/** Custom trigger tooltip */
|
||||||
|
triggerTooltip?: string
|
||||||
/** Allow clearing the selection */
|
/** Allow clearing the selection */
|
||||||
clearable?: boolean
|
clearable?: boolean
|
||||||
}
|
}
|
||||||
@@ -65,16 +71,20 @@ export function AsyncSelect<T>({
|
|||||||
filterFn,
|
filterFn,
|
||||||
renderOption,
|
renderOption,
|
||||||
getOptionValue,
|
getOptionValue,
|
||||||
|
getDisplayValue,
|
||||||
notFound,
|
notFound,
|
||||||
loadingSkeleton,
|
loadingSkeleton,
|
||||||
label,
|
label,
|
||||||
placeholder = 'Select...',
|
placeholder = 'Select...',
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onFocus,
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className,
|
className,
|
||||||
noResultsMessage
|
triggerClassName,
|
||||||
|
searchInputClassName,
|
||||||
|
noResultsMessage,
|
||||||
|
triggerTooltip,
|
||||||
|
clearable = true
|
||||||
}: AsyncSelectProps<T>) {
|
}: AsyncSelectProps<T>) {
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
@@ -82,7 +92,7 @@ export function AsyncSelect<T>({
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [selectedValue, setSelectedValue] = useState(value)
|
const [selectedValue, setSelectedValue] = useState(value)
|
||||||
const [focusedValue, setFocusedValue] = useState<string | null>(null)
|
const [selectedOption, setSelectedOption] = useState<T | null>(null)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const debouncedSearchTerm = useDebounce(searchTerm, preload ? 0 : 150)
|
const debouncedSearchTerm = useDebounce(searchTerm, preload ? 0 : 150)
|
||||||
const [originalOptions, setOriginalOptions] = useState<T[]>([])
|
const [originalOptions, setOriginalOptions] = useState<T[]>([])
|
||||||
@@ -92,6 +102,16 @@ export function AsyncSelect<T>({
|
|||||||
setSelectedValue(value)
|
setSelectedValue(value)
|
||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
|
// Initialize selectedOption when options are loaded and value exists
|
||||||
|
useEffect(() => {
|
||||||
|
if (value && options.length > 0) {
|
||||||
|
const option = options.find((opt) => getOptionValue(opt) === value)
|
||||||
|
if (option) {
|
||||||
|
setSelectedOption(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [value, options, getOptionValue])
|
||||||
|
|
||||||
// Effect for initial fetch
|
// Effect for initial fetch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeOptions = async () => {
|
const initializeOptions = async () => {
|
||||||
@@ -99,7 +119,7 @@ export function AsyncSelect<T>({
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
// If we have a value, use it for the initial search
|
// If we have a value, use it for the initial search
|
||||||
const data = value !== null ? await fetcher(value) : []
|
const data = await fetcher(value)
|
||||||
setOriginalOptions(data)
|
setOriginalOptions(data)
|
||||||
setOptions(data)
|
setOptions(data)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -149,75 +169,85 @@ export function AsyncSelect<T>({
|
|||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(currentValue: string) => {
|
(currentValue: string) => {
|
||||||
console.log('handleSelect')
|
const newValue = clearable && currentValue === selectedValue ? '' : currentValue
|
||||||
if (currentValue !== selectedValue) {
|
setSelectedValue(newValue)
|
||||||
setSelectedValue(currentValue)
|
setSelectedOption(options.find((option) => getOptionValue(option) === newValue) || null)
|
||||||
onChange(currentValue)
|
onChange(newValue)
|
||||||
}
|
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
},
|
},
|
||||||
[selectedValue, onChange]
|
[selectedValue, onChange, clearable, options, getOptionValue]
|
||||||
)
|
|
||||||
|
|
||||||
const handleFocus = useCallback(
|
|
||||||
(currentValue: string) => {
|
|
||||||
if (currentValue !== focusedValue) {
|
|
||||||
setFocusedValue(currentValue)
|
|
||||||
onFocus(currentValue)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[focusedValue, onFocus]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
className={cn(disabled && 'cursor-not-allowed opacity-50', className)}
|
<PopoverTrigger asChild>
|
||||||
onFocus={() => setOpen(true)}
|
<Button
|
||||||
onBlur={() => setOpen(false)}
|
variant="outline"
|
||||||
>
|
role="combobox"
|
||||||
<Command shouldFilter={false} className="bg-transparent">
|
aria-expanded={open}
|
||||||
<div className="relative w-full">
|
className={cn(
|
||||||
<CommandInput
|
'justify-between',
|
||||||
placeholder={placeholder}
|
disabled && 'cursor-not-allowed opacity-50',
|
||||||
value={searchTerm}
|
triggerClassName
|
||||||
onValueChange={(value) => {
|
|
||||||
setSearchTerm(value)
|
|
||||||
if (value && !open) setOpen(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{loading && options.length > 0 && (
|
|
||||||
<div className="absolute top-1/2 right-2 flex -translate-y-1/2 transform items-center">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
disabled={disabled}
|
||||||
<CommandList className="max-h-auto" hidden={!open || debouncedSearchTerm.length === 0}>
|
tooltip={triggerTooltip}
|
||||||
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
side="bottom"
|
||||||
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
>
|
||||||
{!loading &&
|
{selectedOption ? getDisplayValue(selectedOption) : placeholder}
|
||||||
!error &&
|
<ChevronsUpDown className="opacity-50" size={10} />
|
||||||
options.length === 0 &&
|
</Button>
|
||||||
(notFound || (
|
</PopoverTrigger>
|
||||||
<CommandEmpty>{noResultsMessage ?? `No ${label.toLowerCase()} found.`}</CommandEmpty>
|
<PopoverContent className={cn('p-0', className)} onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||||
))}
|
<Command shouldFilter={false}>
|
||||||
<CommandGroup>
|
<div className="relative w-full border-b">
|
||||||
{options.map((option, idx) => (
|
<CommandInput
|
||||||
<>
|
placeholder={`Search ${label.toLowerCase()}...`}
|
||||||
|
value={searchTerm}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSearchTerm(value)
|
||||||
|
}}
|
||||||
|
className={searchInputClassName}
|
||||||
|
/>
|
||||||
|
{loading && options.length > 0 && (
|
||||||
|
<div className="absolute top-1/2 right-2 flex -translate-y-1/2 transform items-center">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<CommandList>
|
||||||
|
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
||||||
|
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
||||||
|
{!loading &&
|
||||||
|
!error &&
|
||||||
|
options.length === 0 &&
|
||||||
|
(notFound || (
|
||||||
|
<CommandEmpty>
|
||||||
|
{noResultsMessage ?? `No ${label.toLowerCase()} found.`}
|
||||||
|
</CommandEmpty>
|
||||||
|
))}
|
||||||
|
<CommandGroup>
|
||||||
|
{options.map((option) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={getOptionValue(option)}
|
key={getOptionValue(option)}
|
||||||
value={getOptionValue(option)}
|
value={getOptionValue(option)}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
onMouseEnter={() => handleFocus(getOptionValue(option))}
|
className="truncate"
|
||||||
>
|
>
|
||||||
{renderOption(option)}
|
{renderOption(option)}
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
'ml-auto h-3 w-3',
|
||||||
|
selectedValue === getOptionValue(option) ? 'opacity-100' : 'opacity-0'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
{idx !== options.length - 1 && <div className="bg-foreground/10 h-[1px]" />}
|
))}
|
||||||
</>
|
</CommandGroup>
|
||||||
))}
|
</CommandList>
|
||||||
</CommandGroup>
|
</Command>
|
||||||
</CommandList>
|
</PopoverContent>
|
||||||
</Command>
|
</Popover>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
import Graph, { DirectedGraph } from 'graphology'
|
import Graph, { DirectedGraph } from 'graphology'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import { randomColor } from '@/lib/utils'
|
import { randomColor, errorMessage } from '@/lib/utils'
|
||||||
import * as Constants from '@/lib/constants'
|
import * as Constants from '@/lib/constants'
|
||||||
import { useGraphStore, RawGraph } from '@/stores/graph'
|
import { useGraphStore, RawGraph } from '@/stores/graph'
|
||||||
import { queryGraphs } from '@/api/lightrag'
|
import { queryGraphs } from '@/api/lightrag'
|
||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
|
||||||
|
import seedrandom from 'seedrandom'
|
||||||
|
|
||||||
const validateGraph = (graph: RawGraph) => {
|
const validateGraph = (graph: RawGraph) => {
|
||||||
if (!graph) {
|
if (!graph) {
|
||||||
@@ -53,9 +56,7 @@ const fetchGraph = async (label: string) => {
|
|||||||
try {
|
try {
|
||||||
rawData = await queryGraphs(label)
|
rawData = await queryGraphs(label)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
useBackendState
|
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
|
||||||
.getState()
|
|
||||||
.setErrorMessage(e instanceof Error ? e.message : `${e}`, 'Query Graphs Error!')
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,9 +70,11 @@ const fetchGraph = async (label: string) => {
|
|||||||
const node = rawData.nodes[i]
|
const node = rawData.nodes[i]
|
||||||
nodeIdMap[node.id] = i
|
nodeIdMap[node.id] = i
|
||||||
|
|
||||||
|
// const seed = node.labels.length > 0 ? node.labels[0] : node.id
|
||||||
|
seedrandom(node.id, { global: true })
|
||||||
|
node.color = randomColor()
|
||||||
node.x = Math.random()
|
node.x = Math.random()
|
||||||
node.y = Math.random()
|
node.y = Math.random()
|
||||||
node.color = randomColor()
|
|
||||||
node.degree = 0
|
node.degree = 0
|
||||||
node.size = 10
|
node.size = 10
|
||||||
}
|
}
|
||||||
@@ -150,8 +153,10 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
|||||||
return graph
|
return graph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lastQueryLabel = { label: '' }
|
||||||
|
|
||||||
const useLightrangeGraph = () => {
|
const useLightrangeGraph = () => {
|
||||||
const [fetchLabel, setFetchLabel] = useState<string>('*')
|
const queryLabel = useSettingsStore.use.queryLabel()
|
||||||
const rawGraph = useGraphStore.use.rawGraph()
|
const rawGraph = useGraphStore.use.rawGraph()
|
||||||
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
||||||
|
|
||||||
@@ -170,12 +175,13 @@ const useLightrangeGraph = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fetchLabel) {
|
if (queryLabel) {
|
||||||
const state = useGraphStore.getState()
|
if (lastQueryLabel.label !== queryLabel) {
|
||||||
if (state.queryLabel !== fetchLabel) {
|
lastQueryLabel.label = queryLabel
|
||||||
|
const state = useGraphStore.getState()
|
||||||
state.reset()
|
state.reset()
|
||||||
fetchGraph(fetchLabel).then((data) => {
|
fetchGraph(queryLabel).then((data) => {
|
||||||
state.setQueryLabel(fetchLabel)
|
// console.debug('Query label: ' + queryLabel)
|
||||||
state.setSigmaGraph(createSigmaGraph(data))
|
state.setSigmaGraph(createSigmaGraph(data))
|
||||||
data?.buildDynamicMap()
|
data?.buildDynamicMap()
|
||||||
state.setRawGraph(data)
|
state.setRawGraph(data)
|
||||||
@@ -186,7 +192,7 @@ const useLightrangeGraph = () => {
|
|||||||
state.reset()
|
state.reset()
|
||||||
state.setSigmaGraph(new DirectedGraph())
|
state.setSigmaGraph(new DirectedGraph())
|
||||||
}
|
}
|
||||||
}, [fetchLabel])
|
}, [queryLabel])
|
||||||
|
|
||||||
const lightrageGraph = useCallback(() => {
|
const lightrageGraph = useCallback(() => {
|
||||||
if (sigmaGraph) {
|
if (sigmaGraph) {
|
||||||
@@ -197,7 +203,7 @@ const useLightrangeGraph = () => {
|
|||||||
return graph as Graph<NodeType, EdgeType>
|
return graph as Graph<NodeType, EdgeType>
|
||||||
}, [sigmaGraph])
|
}, [sigmaGraph])
|
||||||
|
|
||||||
return { lightrageGraph, fetchLabel, setFetchLabel, getNode, getEdge }
|
return { lightrageGraph, getNode, getEdge }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useLightrangeGraph
|
export default useLightrangeGraph
|
||||||
|
@@ -19,3 +19,7 @@ export const searchResultLimit = 20
|
|||||||
|
|
||||||
export const minNodeSize = 4
|
export const minNodeSize = 4
|
||||||
export const maxNodeSize = 20
|
export const maxNodeSize = 20
|
||||||
|
|
||||||
|
export const healthCheckInterval = 15 // seconds
|
||||||
|
|
||||||
|
export const defaultQueryLabel = '*'
|
||||||
|
@@ -15,6 +15,10 @@ export function randomColor() {
|
|||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function errorMessage(error: any) {
|
||||||
|
return error instanceof Error ? error.message : `${error}`
|
||||||
|
}
|
||||||
|
|
||||||
type WithSelectors<S> = S extends { getState: () => infer T }
|
type WithSelectors<S> = S extends { getState: () => infer T }
|
||||||
? S & { use: { [K in keyof T]: () => T[K] } }
|
? S & { use: { [K in keyof T]: () => T[K] } }
|
||||||
: never
|
: never
|
||||||
|
@@ -63,7 +63,6 @@ interface GraphState {
|
|||||||
selectedEdge: string | null
|
selectedEdge: string | null
|
||||||
focusedEdge: string | null
|
focusedEdge: string | null
|
||||||
|
|
||||||
queryLabel: string | null
|
|
||||||
rawGraph: RawGraph | null
|
rawGraph: RawGraph | null
|
||||||
sigmaGraph: DirectedGraph | null
|
sigmaGraph: DirectedGraph | null
|
||||||
|
|
||||||
@@ -78,7 +77,6 @@ interface GraphState {
|
|||||||
|
|
||||||
setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
|
setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
|
||||||
|
|
||||||
setQueryLabel: (queryLabel: string | null) => void
|
|
||||||
setRawGraph: (rawGraph: RawGraph | null) => void
|
setRawGraph: (rawGraph: RawGraph | null) => void
|
||||||
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
||||||
}
|
}
|
||||||
@@ -91,7 +89,6 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
|||||||
|
|
||||||
moveToSelectedNode: false,
|
moveToSelectedNode: false,
|
||||||
|
|
||||||
queryLabel: null,
|
|
||||||
rawGraph: null,
|
rawGraph: null,
|
||||||
sigmaGraph: null,
|
sigmaGraph: null,
|
||||||
|
|
||||||
@@ -113,17 +110,11 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
|||||||
focusedNode: null,
|
focusedNode: null,
|
||||||
selectedEdge: null,
|
selectedEdge: null,
|
||||||
focusedEdge: null,
|
focusedEdge: null,
|
||||||
queryLabel: null,
|
|
||||||
rawGraph: null,
|
rawGraph: null,
|
||||||
sigmaGraph: null,
|
sigmaGraph: null,
|
||||||
moveToSelectedNode: false
|
moveToSelectedNode: false
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setQueryLabel: (queryLabel: string | null) =>
|
|
||||||
set({
|
|
||||||
queryLabel
|
|
||||||
}),
|
|
||||||
|
|
||||||
setRawGraph: (rawGraph: RawGraph | null) =>
|
setRawGraph: (rawGraph: RawGraph | null) =>
|
||||||
set({
|
set({
|
||||||
rawGraph
|
rawGraph
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||||
import { createSelectors } from '@/lib/utils'
|
import { createSelectors } from '@/lib/utils'
|
||||||
|
import { defaultQueryLabel } from '@/lib/constants'
|
||||||
|
|
||||||
type Theme = 'dark' | 'light' | 'system'
|
type Theme = 'dark' | 'light' | 'system'
|
||||||
|
|
||||||
@@ -10,7 +11,11 @@ interface SettingsState {
|
|||||||
enableEdgeEvents: boolean
|
enableEdgeEvents: boolean
|
||||||
enableHideUnselectedEdges: boolean
|
enableHideUnselectedEdges: boolean
|
||||||
showEdgeLabel: boolean
|
showEdgeLabel: boolean
|
||||||
|
|
||||||
setTheme: (theme: Theme) => void
|
setTheme: (theme: Theme) => void
|
||||||
|
|
||||||
|
queryLabel: string
|
||||||
|
setQueryLabel: (queryLabel: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useSettingsStoreBase = create<SettingsState>()(
|
const useSettingsStoreBase = create<SettingsState>()(
|
||||||
@@ -22,16 +27,26 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
enableHideUnselectedEdges: true,
|
enableHideUnselectedEdges: true,
|
||||||
showEdgeLabel: false,
|
showEdgeLabel: false,
|
||||||
|
|
||||||
setTheme: (theme: Theme) => set({ theme })
|
queryLabel: defaultQueryLabel,
|
||||||
|
|
||||||
|
setTheme: (theme: Theme) => set({ theme }),
|
||||||
|
|
||||||
|
setQueryLabel: (queryLabel: string) =>
|
||||||
|
set({
|
||||||
|
queryLabel
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
version: 2,
|
version: 3,
|
||||||
migrate: (state: any, version: number) => {
|
migrate: (state: any, version: number) => {
|
||||||
if (version < 2) {
|
if (version < 2) {
|
||||||
state.showEdgeLabel = false
|
state.showEdgeLabel = false
|
||||||
}
|
}
|
||||||
|
if (version < 3) {
|
||||||
|
state.queryLabel = defaultQueryLabel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user