chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1 @@
export { useLogDetailsResize } from './use-log-details-resize'
@@ -0,0 +1,52 @@
import { useCallback, useEffect, useState } from 'react'
import { useLogDetailsUIStore } from '@/stores/logs/store'
/**
* Hook for handling log details panel resize via mouse drag.
* @returns Resize state and mouse event handler.
*/
export function useLogDetailsResize() {
const setPanelWidth = useLogDetailsUIStore((state) => state.setPanelWidth)
const setIsResizing = useLogDetailsUIStore((state) => state.setIsResizing)
const [isResizing, setLocalIsResizing] = useState(false)
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
setLocalIsResizing(true)
setIsResizing(true)
},
[setIsResizing]
)
useEffect(() => {
if (!isResizing) return
const handleMouseMove = (e: MouseEvent) => {
const newWidth = window.innerWidth - e.clientX
setPanelWidth(newWidth)
}
const handleMouseUp = () => {
setLocalIsResizing(false)
setIsResizing(false)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
document.body.style.cursor = 'ew-resize'
document.body.style.userSelect = 'none'
return () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
}, [isResizing, setPanelWidth, setIsResizing])
return {
isResizing,
handleMouseDown,
}
}
@@ -0,0 +1,206 @@
'use client'
import { useCallback, useMemo } from 'react'
import { debounce, useQueryStates } from 'nuqs'
import {
logFilterParsers,
logFilterUrlKeys,
} from '@/app/workspace/[workspaceId]/logs/search-params'
import type { LogLevel, TimeRange, TriggerType } from '@/stores/logs/filters/types'
const DEFAULT_TIME_RANGE: TimeRange = 'All time'
/** Debounce window for `search` URL writes; keystrokes stay instant in the input. */
const SEARCH_DEBOUNCE_MS = 300 as const
/**
* The logs filter state, sourced entirely from typed URL query params via nuqs.
*
* This replaces the former hand-rolled `useFilterStore` URL sync (`syncWithURL`
* / `initializeFromURL` / `popstate`). The URL is now the single source of
* truth — initialization, serialization, and back/forward navigation are all
* handled by nuqs. The action surface mirrors the previous store so consumers
* migrate with minimal churn.
*/
export interface UseLogFilters {
timeRange: TimeRange
startDate: string | undefined
endDate: string | undefined
level: LogLevel
workflowIds: string[]
folderIds: string[]
triggers: TriggerType[]
searchQuery: string
setTimeRange: (timeRange: TimeRange) => void
setDateRange: (startDate: string | undefined, endDate: string | undefined) => void
clearDateRange: () => void
setLevel: (level: LogLevel) => void
setWorkflowIds: (workflowIds: string[]) => void
toggleWorkflowId: (workflowId: string) => void
setFolderIds: (folderIds: string[]) => void
toggleFolderId: (folderId: string) => void
setSearchQuery: (query: string) => void
setTriggers: (triggers: TriggerType[]) => void
toggleTrigger: (trigger: TriggerType) => void
resetFilters: () => void
}
/**
* Hook exposing the logs filter state and actions backed by URL query params.
* `startDate`/`endDate` are only retained while the time range is "Custom range"
* to match the prior store semantics.
*/
export function useLogFilters(): UseLogFilters {
const [filters, setFilters] = useQueryStates(logFilterParsers, logFilterUrlKeys)
const setTimeRange = useCallback(
(timeRange: TimeRange) => {
if (timeRange === 'Custom range') {
setFilters({ timeRange })
} else {
setFilters({ timeRange, startDate: null, endDate: null })
}
},
[setFilters]
)
const setDateRange = useCallback(
(startDate: string | undefined, endDate: string | undefined) => {
setFilters({
timeRange: 'Custom range',
startDate: startDate ?? null,
endDate: endDate ?? null,
})
},
[setFilters]
)
const clearDateRange = useCallback(() => {
setFilters({ timeRange: DEFAULT_TIME_RANGE, startDate: null, endDate: null })
}, [setFilters])
const setLevel = useCallback((level: LogLevel) => setFilters({ level }), [setFilters])
const setWorkflowIds = useCallback(
(workflowIds: string[]) => setFilters({ workflowIds }),
[setFilters]
)
const toggleWorkflowId = useCallback(
(workflowId: string) => {
setFilters((prev) => {
const current = prev.workflowIds
const next = current.includes(workflowId)
? current.filter((id) => id !== workflowId)
: [...current, workflowId]
return { workflowIds: next }
})
},
[setFilters]
)
const setFolderIds = useCallback((folderIds: string[]) => setFilters({ folderIds }), [setFilters])
const toggleFolderId = useCallback(
(folderId: string) => {
setFilters((prev) => {
const current = prev.folderIds
const next = current.includes(folderId)
? current.filter((id) => id !== folderId)
: [...current, folderId]
return { folderIds: next }
})
},
[setFilters]
)
/**
* Debounces only the search param's URL write; the returned `filters.search`
* value still updates instantly so the controlled input stays responsive.
* Clearing flushes immediately so the param drops out without lingering.
*/
const setSearchQuery = useCallback(
(query: string) => {
const trimmed = query.trim()
const next = trimmed.length > 0 ? trimmed : null
setFilters(
{ search: next },
next === null ? undefined : { limitUrlUpdates: debounce(SEARCH_DEBOUNCE_MS) }
)
},
[setFilters]
)
const setTriggers = useCallback(
(triggers: TriggerType[]) => setFilters({ triggers }),
[setFilters]
)
const toggleTrigger = useCallback(
(trigger: TriggerType) => {
setFilters((prev) => {
const current = prev.triggers
const next = current.includes(trigger)
? current.filter((t) => t !== trigger)
: [...current, trigger]
return { triggers: next }
})
},
[setFilters]
)
const resetFilters = useCallback(() => {
setFilters({
timeRange: DEFAULT_TIME_RANGE,
startDate: null,
endDate: null,
level: 'all',
workflowIds: [],
folderIds: [],
triggers: [],
search: null,
})
}, [setFilters])
return useMemo(
() => ({
timeRange: filters.timeRange,
startDate:
filters.timeRange === 'Custom range' ? (filters.startDate ?? undefined) : undefined,
endDate: filters.timeRange === 'Custom range' ? (filters.endDate ?? undefined) : undefined,
level: filters.level,
workflowIds: filters.workflowIds,
folderIds: filters.folderIds,
triggers: filters.triggers,
searchQuery: filters.search,
setTimeRange,
setDateRange,
clearDateRange,
setLevel,
setWorkflowIds,
toggleWorkflowId,
setFolderIds,
toggleFolderId,
setSearchQuery,
setTriggers,
toggleTrigger,
resetFilters,
}),
[
filters,
setTimeRange,
setDateRange,
clearDateRange,
setLevel,
setWorkflowIds,
toggleWorkflowId,
setFolderIds,
toggleFolderId,
setSearchQuery,
setTriggers,
toggleTrigger,
resetFilters,
]
)
}
@@ -0,0 +1,266 @@
import { useCallback, useRef, useState } from 'react'
import type { ParsedFilter } from '@/lib/logs/query-parser'
import { parseQuery } from '@/lib/logs/query-parser'
import type {
Suggestion,
SuggestionGroup,
SuggestionSection,
} from '@/app/workspace/[workspaceId]/logs/types'
interface UseSearchStateOptions {
onFiltersChange: (filters: ParsedFilter[], textSearch: string) => void
getSuggestions: (input: string) => SuggestionGroup | null
debounceMs?: number
initialQuery?: string
}
export function useSearchState({
onFiltersChange,
getSuggestions,
debounceMs = 100,
initialQuery,
}: UseSearchStateOptions) {
const [initialParsed] = useState(() =>
initialQuery ? parseQuery(initialQuery) : { filters: [] as ParsedFilter[], textSearch: '' }
)
const [appliedFilters, setAppliedFilters] = useState<ParsedFilter[]>(initialParsed.filters)
const [currentInput, setCurrentInput] = useState('')
const [textSearch, setTextSearch] = useState<string>(initialParsed.textSearch)
const [isOpen, setIsOpen] = useState(false)
const [suggestions, setSuggestions] = useState<Suggestion[]>([])
const [sections, setSections] = useState<SuggestionSection[]>([])
const [highlightedIndex, setHighlightedIndex] = useState(-1)
const [highlightedBadgeIndex, setHighlightedBadgeIndex] = useState<number | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const debounceRef = useRef<NodeJS.Timeout | null>(null)
const updateSuggestions = useCallback(
(input: string) => {
const suggestionGroup = getSuggestions(input)
if (suggestionGroup && suggestionGroup.suggestions.length > 0) {
setSuggestions(suggestionGroup.suggestions)
setSections(suggestionGroup.sections || [])
setIsOpen(true)
setHighlightedIndex(0)
} else {
setIsOpen(false)
setSuggestions([])
setSections([])
setHighlightedIndex(-1)
}
},
[getSuggestions]
)
const handleInputChange = useCallback(
(value: string) => {
setCurrentInput(value)
setHighlightedBadgeIndex(null)
if (debounceRef.current) {
clearTimeout(debounceRef.current)
}
debounceRef.current = setTimeout(() => {
updateSuggestions(value)
}, debounceMs)
},
[updateSuggestions, debounceMs]
)
const handleSuggestionSelect = useCallback(
(suggestion: Suggestion) => {
if (suggestion.category === 'show-all') {
setTextSearch(suggestion.value)
setCurrentInput('')
setIsOpen(false)
onFiltersChange(appliedFilters, suggestion.value)
return
}
if (suggestion.category === 'filters' && suggestion.value.endsWith(':')) {
setCurrentInput(suggestion.value)
updateSuggestions(suggestion.value)
return
}
const newFilter: ParsedFilter = {
field: suggestion.value.split(':')[0],
operator: '=',
value: suggestion.value.includes(':')
? suggestion.value.split(':').slice(1).join(':').replace(/"/g, '')
: suggestion.value.replace(/"/g, ''),
originalValue: suggestion.value.includes(':')
? suggestion.value.split(':').slice(1).join(':')
: suggestion.value,
}
const updatedFilters = [...appliedFilters, newFilter]
setAppliedFilters(updatedFilters)
setCurrentInput('')
setTextSearch('')
onFiltersChange(updatedFilters, '')
if (inputRef.current) {
inputRef.current.focus()
}
setTimeout(() => {
updateSuggestions('')
}, 50)
},
[appliedFilters, onFiltersChange, updateSuggestions]
)
const removeBadge = useCallback(
(index: number) => {
const updatedFilters = appliedFilters.filter((_, i) => i !== index)
setAppliedFilters(updatedFilters)
onFiltersChange(updatedFilters, textSearch)
if (inputRef.current) {
inputRef.current.focus()
}
},
[appliedFilters, textSearch, onFiltersChange]
)
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === 'Backspace' && currentInput === '') {
event.preventDefault()
if (highlightedBadgeIndex !== null) {
removeBadge(highlightedBadgeIndex)
setHighlightedBadgeIndex(null)
} else if (appliedFilters.length > 0) {
setHighlightedBadgeIndex(appliedFilters.length - 1)
}
return
}
if (
highlightedBadgeIndex !== null &&
!['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)
) {
setHighlightedBadgeIndex(null)
}
if (event.key === 'Enter') {
event.preventDefault()
if (isOpen && highlightedIndex >= 0 && suggestions[highlightedIndex]) {
handleSuggestionSelect(suggestions[highlightedIndex])
} else if (currentInput.trim()) {
setTextSearch(currentInput.trim())
setCurrentInput('')
setIsOpen(false)
onFiltersChange(appliedFilters, currentInput.trim())
}
return
}
if (!isOpen) return
switch (event.key) {
case 'ArrowDown': {
event.preventDefault()
setHighlightedIndex((prev) => Math.min(prev + 1, suggestions.length - 1))
break
}
case 'ArrowUp': {
event.preventDefault()
setHighlightedIndex((prev) => Math.max(prev - 1, 0))
break
}
case 'Escape': {
event.preventDefault()
setIsOpen(false)
setHighlightedIndex(-1)
break
}
case 'Tab': {
if (highlightedIndex >= 0 && suggestions[highlightedIndex]) {
event.preventDefault()
handleSuggestionSelect(suggestions[highlightedIndex])
}
break
}
}
},
[
currentInput,
appliedFilters,
highlightedBadgeIndex,
isOpen,
highlightedIndex,
suggestions,
handleSuggestionSelect,
removeBadge,
onFiltersChange,
]
)
const handleFocus = useCallback(() => {
updateSuggestions(currentInput)
}, [currentInput, updateSuggestions])
const handleBlur = useCallback(() => {
setTimeout(() => {
setIsOpen(false)
setHighlightedIndex(-1)
}, 150)
}, [])
const clearAll = useCallback(() => {
setAppliedFilters([])
setCurrentInput('')
setTextSearch('')
setIsOpen(false)
onFiltersChange([], '')
if (inputRef.current) {
inputRef.current.focus()
}
}, [onFiltersChange])
const initializeFromQuery = useCallback((query: string, filters: ParsedFilter[]) => {
setAppliedFilters(filters)
setTextSearch(query)
setCurrentInput('')
}, [])
return {
appliedFilters,
currentInput,
textSearch,
isOpen,
suggestions,
sections,
highlightedIndex,
highlightedBadgeIndex,
inputRef,
dropdownRef,
handleInputChange,
handleSuggestionSelect,
handleKeyDown,
handleFocus,
handleBlur,
removeBadge,
clearAll,
initializeFromQuery,
setHighlightedIndex,
setHighlightedBadgeIndex,
}
}