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: 2 additions & 0 deletions .agents/skills/react-query-best-practices/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Read these before analyzing:
- Every query must have an explicit `staleTime` (default 0 is almost never correct), assigned from a named exported constant — never an inline numeric literal. A server-side prefetch hydrating the same query key must import and reuse that constant instead of restating the number
- `keepPreviousData` / `placeholderData` only on variable-key queries (where params change), never on static keys
- Use `enabled` to prevent queries from running without required params
- Warm data for hover/focus intent with `queryClient.prefetchQuery` and shared `queryOptions`; never temporarily enable a mounted hidden observer, which can remain active after focus restoration and refetch data for closed UI
- When gating a query by view or modal state, move every consumer to the active query too: imperative refresh/pagination, loading and error feedback, and data-derived controls must never read a disabled query or placeholder data from a previous key
- Compose caller-controlled `enabled` options with required-param guards (`Boolean(id) && (options?.enabled ?? true)`). Never spread options after an internal guard, because `{ enabled: true }` can silently re-enable an invalid request.
- A disabled query can still report `isPending: true`. Aggregate loading state only for queries that are applicable/enabled, or an optional query can hold the whole surface in a permanent loading state.
- Deferred authorization or policy queries must fail closed. Do not give pending/error data the same fallback as a successfully loaded unrestricted policy; disable guarded actions until the policy query succeeds.
Expand Down
37 changes: 26 additions & 11 deletions apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export default function Logs() {

const viewMode = useFilterStore((s) => s.viewMode)
const setViewMode = useFilterStore((s) => s.setViewMode)
const isDashboardView = viewMode === 'dashboard'

const [{ selectedLogId, isSidebarOpen }, dispatch] = useReducer(logSelectionReducer, {
selectedLogId: null,
Expand Down Expand Up @@ -277,7 +278,7 @@ export default function Logs() {
const isSidebarOpenRef = useRef(false)
const shouldScrollIntoViewRef = useRef(false)
const resourceTableRef = useRef<ResourceTableHandle>(null)
const logsRefetchRef = useRef<() => void>(() => {})
const activeViewRefetchRef = useRef<() => void>(() => {})
const activeLogRefetchRef = useRef<() => void>(() => {})
const activeLogTabRef = useRef<string>('overview')
const logsQueryRef = useRef({ isFetching: false, hasNextPage: false, fetchNextPage: () => {} })
Expand Down Expand Up @@ -316,6 +317,7 @@ export default function Logs() {
)

const selectedDetailQuery = useLogDetail(selectedLogId ?? undefined, workspaceId, {
enabled: isSidebarOpen,
refetchInterval,
})

Expand Down Expand Up @@ -352,6 +354,7 @@ export default function Logs() {
)

const logsQuery = useLogsList(workspaceId, logFilters, {
enabled: !isDashboardView || isSidebarOpen,
refetchInterval: isLive ? LIVE_REFRESH_INTERVAL_MS : false,
})

Expand All @@ -370,6 +373,7 @@ export default function Logs() {
)

const dashboardStatsQuery = useDashboardStats(workspaceId, dashboardFilters, {
enabled: isDashboardView,
refetchInterval: isLive ? LIVE_REFRESH_INTERVAL_MS : false,
})

Expand All @@ -394,7 +398,14 @@ export default function Logs() {
selectedLogIndexRef.current = selectedLogIndex
selectedLogIdRef.current = selectedLogId
isSidebarOpenRef.current = isSidebarOpen
logsRefetchRef.current = logsQuery.refetch
activeViewRefetchRef.current = () => {
if (isDashboardView) {
void dashboardStatsQuery.refetch()
}
if (!isDashboardView || isSidebarOpen) {
void logsQuery.refetch()
}
}
activeLogRefetchRef.current = selectedDetailQuery.refetch
logsQueryRef.current = {
isFetching: logsQuery.isFetching,
Expand Down Expand Up @@ -641,22 +652,25 @@ export default function Logs() {

const handleRefresh = useCallback(() => {
triggerVisualRefresh()
logsRefetchRef.current()
if (selectedLogIdRef.current) {
activeViewRefetchRef.current()
if (selectedLogIdRef.current && isSidebarOpenRef.current) {
activeLogRefetchRef.current()
}
}, [triggerVisualRefresh])

const prevIsFetchingRef = useRef(logsQuery.isFetching)
const activeViewIsFetching = isDashboardView
? dashboardStatsQuery.isFetching || (isSidebarOpen && logsQuery.isFetching)
: logsQuery.isFetching
const prevIsFetchingRef = useRef(activeViewIsFetching)
useEffect(() => {
const wasFetching = prevIsFetchingRef.current
const isFetching = logsQuery.isFetching
const isFetching = activeViewIsFetching
prevIsFetchingRef.current = isFetching

if (isLive && !wasFetching && isFetching) {
triggerVisualRefresh()
}
}, [logsQuery.isFetching, isLive, triggerVisualRefresh])
}, [activeViewIsFetching, isLive, triggerVisualRefresh])

const handleExport = useCallback(async () => {
setIsExporting(true)
Expand Down Expand Up @@ -777,8 +791,6 @@ export default function Logs() {
setPreviewLogId(null)
}

const isDashboardView = viewMode === 'dashboard'
Comment thread
waleedlatif1 marked this conversation as resolved.

Comment thread
waleedlatif1 marked this conversation as resolved.
const rows: ResourceRow[] = useMemo(
() =>
logs.map((log) => {
Expand Down Expand Up @@ -1135,14 +1147,17 @@ export default function Logs() {
)

const refreshIcon = isVisuallyRefreshing ? SpinningRefreshCw : RefreshCw
const hasExportableLogs = isDashboardView
? !dashboardStatsQuery.isPlaceholderData && (dashboardStatsQuery.data?.totalRuns ?? 0) > 0
: !logsQuery.isPlaceholderData && logs.length > 0

const headerActions = useMemo<ResourceAction[]>(
() => [
{
text: 'Export',
icon: Download,
onSelect: handleExport,
disabled: !userPermissions.canEdit || isExporting || logs.length === 0,
disabled: !userPermissions.canEdit || isExporting || !hasExportableLogs,
},
{
text: 'Refresh',
Expand Down Expand Up @@ -1170,7 +1185,7 @@ export default function Logs() {
handleExport,
userPermissions.canEdit,
isExporting,
logs.length,
hasExportableLogs,
]
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* @vitest-environment jsdom
*/
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

const mocks = vi.hoisted(() => ({
pathname: '/workspace/workspace-1/tables',
workspaceId: 'workspace-1' as string | undefined,
searchOpen: false,
useProviderModels: vi.fn(() => ({
data: undefined,
isLoading: false,
isFetching: false,
error: null,
})),
setProviderModels: vi.fn(),
setProviderLoading: vi.fn(),
setOpenRouterModelInfo: vi.fn(),
}))

vi.mock('@sim/logger', () => ({
createLogger: () => ({ error: vi.fn(), warn: vi.fn() }),
}))

vi.mock('next/navigation', () => ({
useParams: () => ({ workspaceId: mocks.workspaceId }),
usePathname: () => mocks.pathname,
}))

vi.mock('@/hooks/queries/providers', () => ({
useProviderModels: mocks.useProviderModels,
}))

vi.mock('@/providers/utils', () => ({
updateBasetenProviderModels: vi.fn(),
updateFireworksProviderModels: vi.fn(),
updateLiteLLMProviderModels: vi.fn(),
updateOllamaCloudProviderModels: vi.fn(),
updateOllamaProviderModels: vi.fn(),
updateOpenRouterProviderModels: vi.fn(),
updateTogetherProviderModels: vi.fn(),
updateVLLMProviderModels: vi.fn(),
}))

vi.mock('@/stores/modals/search/store', () => ({
useSearchModalStore: (selector: (state: { isOpen: boolean }) => unknown) =>
selector({ isOpen: mocks.searchOpen }),
}))

vi.mock('@/stores/providers', () => ({
useProvidersStore: (
selector: (state: {
setProviderModels: typeof mocks.setProviderModels
setProviderLoading: typeof mocks.setProviderLoading
setOpenRouterModelInfo: typeof mocks.setOpenRouterModelInfo
}) => unknown
) =>
selector({
setProviderModels: mocks.setProviderModels,
setProviderLoading: mocks.setProviderLoading,
setOpenRouterModelInfo: mocks.setOpenRouterModelInfo,
}),
}))

import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader'

let root: Root

function renderLoader() {
act(() => {
root.render(<ProviderModelsLoader />)
})
}

function expectEveryProviderEnabled(enabled: boolean) {
expect(mocks.useProviderModels).toHaveBeenCalledTimes(9)
for (const call of mocks.useProviderModels.mock.calls) {
expect(call[2]).toEqual({ enabled })
}
}

describe('ProviderModelsLoader request gating', () => {
beforeEach(() => {
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
root = createRoot(document.createElement('div'))
mocks.pathname = '/workspace/workspace-1/tables'
mocks.workspaceId = 'workspace-1'
mocks.searchOpen = false
})

afterEach(() => {
act(() => root.unmount())
vi.clearAllMocks()
})

it.each(['tables', 'knowledge', 'files', 'logs', 'settings'])(
'defers every provider catalog on the %s route',
(route) => {
mocks.pathname = `/workspace/workspace-1/${route}`
renderLoader()

expectEveryProviderEnabled(false)
}
)

it.each(['home', 'w/workflow-1', 'chat/chat-1'])(
'loads every provider catalog on the %s route',
(route) => {
mocks.pathname = `/workspace/workspace-1/${route}`
renderLoader()

expectEveryProviderEnabled(true)
}
)

it('loads every provider catalog when global search opens on a resource route', () => {
mocks.searchOpen = true
renderLoader()

expectEveryProviderEnabled(true)
})

it('does not create an empty-workspace route prefix', () => {
mocks.workspaceId = undefined
mocks.searchOpen = true
renderLoader()

expectEveryProviderEnabled(false)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useEffect } from 'react'
import { createLogger } from '@sim/logger'
import { useParams } from 'next/navigation'
import { useParams, usePathname } from 'next/navigation'
import { useProviderModels } from '@/hooks/queries/providers'
import {
updateBasetenProviderModels,
Expand All @@ -14,15 +14,37 @@ import {
updateTogetherProviderModels,
updateVLLMProviderModels,
} from '@/providers/utils'
import { useSearchModalStore } from '@/stores/modals/search/store'
import { type ProviderName, useProvidersStore } from '@/stores/providers'

const logger = createLogger('ProviderModelsLoader')

function useSyncProvider(provider: ProviderName, workspaceId?: string) {
function shouldLoadProviderModels(
pathname: string | null,
workspaceId: string | undefined,
isSearchModalOpen: boolean
): boolean {
if (!workspaceId) return false
if (isSearchModalOpen) return true

const workspaceBase = `/workspace/${workspaceId}`
return (
pathname === workspaceBase ||
pathname === `${workspaceBase}/home` ||
pathname === `${workspaceBase}/w` ||
pathname?.startsWith(`${workspaceBase}/w/`) === true ||
pathname === `${workspaceBase}/chat` ||
pathname?.startsWith(`${workspaceBase}/chat/`) === true
)
}

function useSyncProvider(provider: ProviderName, enabled: boolean, workspaceId?: string) {
const setProviderModels = useProvidersStore((state) => state.setProviderModels)
const setProviderLoading = useProvidersStore((state) => state.setProviderLoading)
const setOpenRouterModelInfo = useProvidersStore((state) => state.setOpenRouterModelInfo)
const { data, isLoading, isFetching, error } = useProviderModels(provider, workspaceId)
const { data, isLoading, isFetching, error } = useProviderModels(provider, workspaceId, {
enabled,
})

useEffect(() => {
setProviderLoading(provider, isLoading || isFetching)
Expand Down Expand Up @@ -68,16 +90,19 @@ function useSyncProvider(provider: ProviderName, workspaceId?: string) {

export function ProviderModelsLoader() {
const params = useParams()
const pathname = usePathname()
const workspaceId = params?.workspaceId as string | undefined
const isSearchModalOpen = useSearchModalStore((state) => state.isOpen)
const shouldLoad = shouldLoadProviderModels(pathname, workspaceId, isSearchModalOpen)

useSyncProvider('base')
useSyncProvider('ollama')
useSyncProvider('ollama-cloud', workspaceId)
useSyncProvider('vllm')
useSyncProvider('litellm')
useSyncProvider('openrouter')
useSyncProvider('fireworks', workspaceId)
useSyncProvider('together', workspaceId)
useSyncProvider('baseten', workspaceId)
useSyncProvider('base', shouldLoad)
useSyncProvider('ollama', shouldLoad)
useSyncProvider('ollama-cloud', shouldLoad, workspaceId)
useSyncProvider('vllm', shouldLoad)
useSyncProvider('litellm', shouldLoad)
useSyncProvider('openrouter', shouldLoad)
useSyncProvider('fireworks', shouldLoad, workspaceId)
useSyncProvider('together', shouldLoad, workspaceId)
useSyncProvider('baseten', shouldLoad, workspaceId)
return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,14 @@ export function DeployModal({
const userPermissions = useUserPermissionsContext()
const canManageWorkspaceKeys = userPermissions.canAdmin
const { config: permissionConfig, isPublicApiDisabled } = usePermissionConfig()
const { data: apiKeysData, isLoading: isLoadingKeys } = useApiKeys(workflowWorkspaceId || '')
const { data: apiKeysData, isLoading: isLoadingKeys } = useApiKeys(
workflowWorkspaceId || '',
'combined',
{ enabled: open }
)
const { data: workspaceSettingsData, isLoading: isLoadingSettings } = useWorkspaceSettings(
workflowWorkspaceId || ''
workflowWorkspaceId || '',
{ enabled: open }
)
const apiKeyWorkspaceKeys = apiKeysData?.workspaceKeys || []
const apiKeyPersonalKeys = apiKeysData?.personalKeys || []
Expand Down Expand Up @@ -170,7 +175,9 @@ export function DeployModal({
refetch: refetchChatInfo,
} = useChatDeploymentInfo(workflowId, { enabled: open })

const { data: mcpServers = [] } = useWorkflowMcpServers(workflowWorkspaceId || '')
const { data: mcpServers = [] } = useWorkflowMcpServers(workflowWorkspaceId || '', {
enabled: open,
})
const hasMcpServers = mcpServers.length > 0

const deployMutation = useDeployWorkflow()
Expand Down
Loading
Loading