Skip to content

Commit c83a4ee

Browse files
committed
improvement(navigation): eliminate hidden workspace requests
1 parent 6a5e250 commit c83a4ee

11 files changed

Lines changed: 405 additions & 41 deletions

File tree

.agents/skills/react-query-best-practices/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Read these before analyzing:
3535
- 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
3636
- `keepPreviousData` / `placeholderData` only on variable-key queries (where params change), never on static keys
3737
- Use `enabled` to prevent queries from running without required params
38+
- 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
3839

3940
### Mutations
4041
- Use `onSettled` (not `onSuccess`) for cache reconciliation — it fires on both success and error

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export default function Logs() {
240240

241241
const viewMode = useFilterStore((s) => s.viewMode)
242242
const setViewMode = useFilterStore((s) => s.setViewMode)
243+
const isDashboardView = viewMode === 'dashboard'
243244

244245
const [{ selectedLogId, isSidebarOpen }, dispatch] = useReducer(logSelectionReducer, {
245246
selectedLogId: null,
@@ -352,6 +353,7 @@ export default function Logs() {
352353
)
353354

354355
const logsQuery = useLogsList(workspaceId, logFilters, {
356+
enabled: !isDashboardView,
355357
refetchInterval: isLive ? LIVE_REFRESH_INTERVAL_MS : false,
356358
})
357359

@@ -370,6 +372,7 @@ export default function Logs() {
370372
)
371373

372374
const dashboardStatsQuery = useDashboardStats(workspaceId, dashboardFilters, {
375+
enabled: isDashboardView,
373376
refetchInterval: isLive ? LIVE_REFRESH_INTERVAL_MS : false,
374377
})
375378

@@ -777,8 +780,6 @@ export default function Logs() {
777780
setPreviewLogId(null)
778781
}
779782

780-
const isDashboardView = viewMode === 'dashboard'
781-
782783
const rows: ResourceRow[] = useMemo(
783784
() =>
784785
logs.map((log) => {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const mocks = vi.hoisted(() => ({
9+
pathname: '/workspace/workspace-1/tables',
10+
workspaceId: 'workspace-1' as string | undefined,
11+
searchOpen: false,
12+
useProviderModels: vi.fn(() => ({
13+
data: undefined,
14+
isLoading: false,
15+
isFetching: false,
16+
error: null,
17+
})),
18+
setProviderModels: vi.fn(),
19+
setProviderLoading: vi.fn(),
20+
setOpenRouterModelInfo: vi.fn(),
21+
}))
22+
23+
vi.mock('@sim/logger', () => ({
24+
createLogger: () => ({ error: vi.fn(), warn: vi.fn() }),
25+
}))
26+
27+
vi.mock('next/navigation', () => ({
28+
useParams: () => ({ workspaceId: mocks.workspaceId }),
29+
usePathname: () => mocks.pathname,
30+
}))
31+
32+
vi.mock('@/hooks/queries/providers', () => ({
33+
useProviderModels: mocks.useProviderModels,
34+
}))
35+
36+
vi.mock('@/providers/utils', () => ({
37+
updateBasetenProviderModels: vi.fn(),
38+
updateFireworksProviderModels: vi.fn(),
39+
updateLiteLLMProviderModels: vi.fn(),
40+
updateOllamaCloudProviderModels: vi.fn(),
41+
updateOllamaProviderModels: vi.fn(),
42+
updateOpenRouterProviderModels: vi.fn(),
43+
updateTogetherProviderModels: vi.fn(),
44+
updateVLLMProviderModels: vi.fn(),
45+
}))
46+
47+
vi.mock('@/stores/modals/search/store', () => ({
48+
useSearchModalStore: (selector: (state: { isOpen: boolean }) => unknown) =>
49+
selector({ isOpen: mocks.searchOpen }),
50+
}))
51+
52+
vi.mock('@/stores/providers', () => ({
53+
useProvidersStore: (
54+
selector: (state: {
55+
setProviderModels: typeof mocks.setProviderModels
56+
setProviderLoading: typeof mocks.setProviderLoading
57+
setOpenRouterModelInfo: typeof mocks.setOpenRouterModelInfo
58+
}) => unknown
59+
) =>
60+
selector({
61+
setProviderModels: mocks.setProviderModels,
62+
setProviderLoading: mocks.setProviderLoading,
63+
setOpenRouterModelInfo: mocks.setOpenRouterModelInfo,
64+
}),
65+
}))
66+
67+
import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader'
68+
69+
let root: Root
70+
71+
function renderLoader() {
72+
act(() => {
73+
root.render(<ProviderModelsLoader />)
74+
})
75+
}
76+
77+
function expectEveryProviderEnabled(enabled: boolean) {
78+
expect(mocks.useProviderModels).toHaveBeenCalledTimes(9)
79+
for (const call of mocks.useProviderModels.mock.calls) {
80+
expect(call[2]).toEqual({ enabled })
81+
}
82+
}
83+
84+
describe('ProviderModelsLoader request gating', () => {
85+
beforeEach(() => {
86+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
87+
root = createRoot(document.createElement('div'))
88+
mocks.pathname = '/workspace/workspace-1/tables'
89+
mocks.workspaceId = 'workspace-1'
90+
mocks.searchOpen = false
91+
})
92+
93+
afterEach(() => {
94+
act(() => root.unmount())
95+
vi.clearAllMocks()
96+
})
97+
98+
it.each(['tables', 'knowledge', 'files', 'logs', 'settings'])(
99+
'defers every provider catalog on the %s route',
100+
(route) => {
101+
mocks.pathname = `/workspace/workspace-1/${route}`
102+
renderLoader()
103+
104+
expectEveryProviderEnabled(false)
105+
}
106+
)
107+
108+
it.each(['home', 'w/workflow-1', 'chat/chat-1'])(
109+
'loads every provider catalog on the %s route',
110+
(route) => {
111+
mocks.pathname = `/workspace/workspace-1/${route}`
112+
renderLoader()
113+
114+
expectEveryProviderEnabled(true)
115+
}
116+
)
117+
118+
it('loads every provider catalog when global search opens on a resource route', () => {
119+
mocks.searchOpen = true
120+
renderLoader()
121+
122+
expectEveryProviderEnabled(true)
123+
})
124+
125+
it('does not create an empty-workspace route prefix', () => {
126+
mocks.workspaceId = undefined
127+
mocks.searchOpen = true
128+
renderLoader()
129+
130+
expectEveryProviderEnabled(false)
131+
})
132+
})

apps/sim/app/workspace/[workspaceId]/providers/provider-models-loader.tsx

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useEffect } from 'react'
44
import { createLogger } from '@sim/logger'
5-
import { useParams } from 'next/navigation'
5+
import { useParams, usePathname } from 'next/navigation'
66
import { useProviderModels } from '@/hooks/queries/providers'
77
import {
88
updateBasetenProviderModels,
@@ -14,15 +14,37 @@ import {
1414
updateTogetherProviderModels,
1515
updateVLLMProviderModels,
1616
} from '@/providers/utils'
17+
import { useSearchModalStore } from '@/stores/modals/search/store'
1718
import { type ProviderName, useProvidersStore } from '@/stores/providers'
1819

1920
const logger = createLogger('ProviderModelsLoader')
2021

21-
function useSyncProvider(provider: ProviderName, workspaceId?: string) {
22+
function shouldLoadProviderModels(
23+
pathname: string | null,
24+
workspaceId: string | undefined,
25+
isSearchModalOpen: boolean
26+
): boolean {
27+
if (!workspaceId) return false
28+
if (isSearchModalOpen) return true
29+
30+
const workspaceBase = `/workspace/${workspaceId}`
31+
return (
32+
pathname === workspaceBase ||
33+
pathname === `${workspaceBase}/home` ||
34+
pathname === `${workspaceBase}/w` ||
35+
pathname?.startsWith(`${workspaceBase}/w/`) === true ||
36+
pathname === `${workspaceBase}/chat` ||
37+
pathname?.startsWith(`${workspaceBase}/chat/`) === true
38+
)
39+
}
40+
41+
function useSyncProvider(provider: ProviderName, enabled: boolean, workspaceId?: string) {
2242
const setProviderModels = useProvidersStore((state) => state.setProviderModels)
2343
const setProviderLoading = useProvidersStore((state) => state.setProviderLoading)
2444
const setOpenRouterModelInfo = useProvidersStore((state) => state.setOpenRouterModelInfo)
25-
const { data, isLoading, isFetching, error } = useProviderModels(provider, workspaceId)
45+
const { data, isLoading, isFetching, error } = useProviderModels(provider, workspaceId, {
46+
enabled,
47+
})
2648

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

6991
export function ProviderModelsLoader() {
7092
const params = useParams()
93+
const pathname = usePathname()
7194
const workspaceId = params?.workspaceId as string | undefined
95+
const isSearchModalOpen = useSearchModalStore((state) => state.isOpen)
96+
const shouldLoad = shouldLoadProviderModels(pathname, workspaceId, isSearchModalOpen)
7297

73-
useSyncProvider('base')
74-
useSyncProvider('ollama')
75-
useSyncProvider('ollama-cloud', workspaceId)
76-
useSyncProvider('vllm')
77-
useSyncProvider('litellm')
78-
useSyncProvider('openrouter')
79-
useSyncProvider('fireworks', workspaceId)
80-
useSyncProvider('together', workspaceId)
81-
useSyncProvider('baseten', workspaceId)
98+
useSyncProvider('base', shouldLoad)
99+
useSyncProvider('ollama', shouldLoad)
100+
useSyncProvider('ollama-cloud', shouldLoad, workspaceId)
101+
useSyncProvider('vllm', shouldLoad)
102+
useSyncProvider('litellm', shouldLoad)
103+
useSyncProvider('openrouter', shouldLoad)
104+
useSyncProvider('fireworks', shouldLoad, workspaceId)
105+
useSyncProvider('together', shouldLoad, workspaceId)
106+
useSyncProvider('baseten', shouldLoad, workspaceId)
82107
return null
83108
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,14 @@ export function DeployModal({
135135
const userPermissions = useUserPermissionsContext()
136136
const canManageWorkspaceKeys = userPermissions.canAdmin
137137
const { config: permissionConfig, isPublicApiDisabled } = usePermissionConfig()
138-
const { data: apiKeysData, isLoading: isLoadingKeys } = useApiKeys(workflowWorkspaceId || '')
138+
const { data: apiKeysData, isLoading: isLoadingKeys } = useApiKeys(
139+
workflowWorkspaceId || '',
140+
'combined',
141+
{ enabled: open }
142+
)
139143
const { data: workspaceSettingsData, isLoading: isLoadingSettings } = useWorkspaceSettings(
140-
workflowWorkspaceId || ''
144+
workflowWorkspaceId || '',
145+
{ enabled: open }
141146
)
142147
const apiKeyWorkspaceKeys = apiKeysData?.workspaceKeys || []
143148
const apiKeyPersonalKeys = apiKeysData?.personalKeys || []
@@ -165,7 +170,9 @@ export function DeployModal({
165170
refetch: refetchChatInfo,
166171
} = useChatDeploymentInfo(workflowId, { enabled: open })
167172

168-
const { data: mcpServers = [] } = useWorkflowMcpServers(workflowWorkspaceId || '')
173+
const { data: mcpServers = [] } = useWorkflowMcpServers(workflowWorkspaceId || '', {
174+
enabled: open,
175+
})
169176
const hasMcpServers = mcpServers.length > 0
170177

171178
const deployMutation = useDeployWorkflow()

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import { useState } from 'react'
44
import { Chip, Tooltip, toast } from '@sim/emcn'
5+
import { useQueryClient } from '@tanstack/react-query'
6+
import { useParams } from 'next/navigation'
57
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
68
import { DeployModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal'
79
import {
@@ -10,7 +12,10 @@ import {
1012
useDeployReadiness,
1113
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks'
1214
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
15+
import { apiKeysQueryOptions } from '@/hooks/queries/api-keys'
1316
import { useDeployedWorkflowState, useDeploymentInfo } from '@/hooks/queries/deployments'
17+
import { workflowMcpServersQueryOptions } from '@/hooks/queries/workflow-mcp-servers'
18+
import { workspaceSettingsQueryOptions } from '@/hooks/queries/workspace'
1419
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
1520
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1621

@@ -21,6 +26,9 @@ interface DeployProps {
2126
}
2227

2328
export function Deploy({ activeWorkflowId, userPermissions, disabled = false }: DeployProps) {
29+
const queryClient = useQueryClient()
30+
const params = useParams()
31+
const workspaceId = params.workspaceId as string | undefined
2432
const [isModalOpen, setIsModalOpen] = useState(false)
2533
const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
2634
const isRegistryLoading = hydrationPhase === 'idle' || hydrationPhase === 'state-loading'
@@ -129,6 +137,15 @@ export function Deploy({ activeWorkflowId, userPermissions, disabled = false }:
129137
return 'Deploy'
130138
}
131139

140+
const prefetchDeployModal = () => {
141+
if (!workspaceId || isRegistryLoading || isDisabled) return
142+
void Promise.all([
143+
queryClient.prefetchQuery(apiKeysQueryOptions(workspaceId, 'combined')),
144+
queryClient.prefetchQuery(workspaceSettingsQueryOptions(workspaceId)),
145+
queryClient.prefetchQuery(workflowMcpServersQueryOptions(workspaceId)),
146+
])
147+
}
148+
132149
return (
133150
<>
134151
<Tooltip.Root>
@@ -137,6 +154,8 @@ export function Deploy({ activeWorkflowId, userPermissions, disabled = false }:
137154
<Chip
138155
variant='border'
139156
onClick={onDeployClick}
157+
onMouseEnter={prefetchDeployModal}
158+
onFocus={prefetchDeployModal}
140159
disabled={isRegistryLoading || isDisabled}
141160
>
142161
{getButtonLabel()}

apps/sim/hooks/queries/api-keys.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
1+
import {
2+
keepPreviousData,
3+
queryOptions,
4+
useMutation,
5+
useQuery,
6+
useQueryClient,
7+
} from '@tanstack/react-query'
28
import { requestJson } from '@/lib/api/client/request'
39
import type { ContractBodyInput } from '@/lib/api/contracts'
410
import {
@@ -38,6 +44,10 @@ type CombinedApiKeysData = {
3844

3945
export type ApiKeyScope = 'combined' | 'personal' | 'workspace'
4046

47+
interface UseApiKeysOptions {
48+
enabled?: boolean
49+
}
50+
4151
/**
4252
* Fetch API keys for one settings plane, or both for compatibility callers.
4353
*/
@@ -77,24 +87,34 @@ export async function fetchApiKeys(
7787
}
7888
}
7989

80-
/**
81-
* Hook to fetch API keys for the requested settings plane.
82-
*/
83-
export function useApiKeys(workspaceId: string, scope: ApiKeyScope = 'combined') {
84-
return useQuery({
90+
export function apiKeysQueryOptions(workspaceId: string, scope: ApiKeyScope = 'combined') {
91+
return queryOptions({
8592
queryKey:
8693
scope === 'personal'
8794
? apiKeysKeys.personal()
8895
: scope === 'workspace'
8996
? apiKeysKeys.workspace(workspaceId)
9097
: apiKeysKeys.combined(workspaceId),
9198
queryFn: ({ signal }) => fetchApiKeys(workspaceId, scope, signal),
92-
enabled: scope === 'personal' || !!workspaceId,
9399
staleTime: API_KEYS_COMBINED_STALE_TIME,
94100
placeholderData: scope === 'personal' ? undefined : keepPreviousData,
95101
})
96102
}
97103

104+
/**
105+
* Hook to fetch API keys for the requested settings plane.
106+
*/
107+
export function useApiKeys(
108+
workspaceId: string,
109+
scope: ApiKeyScope = 'combined',
110+
options?: UseApiKeysOptions
111+
) {
112+
return useQuery({
113+
...apiKeysQueryOptions(workspaceId, scope),
114+
enabled: (scope === 'personal' || !!workspaceId) && (options?.enabled ?? true),
115+
})
116+
}
117+
98118
/**
99119
* Create API key mutation params
100120
*/

0 commit comments

Comments
 (0)