Skip to content

Commit 5f7d6c9

Browse files
committed
fix(integrations): fall back from emptied categories and fail closed on detail gate
1 parent ed05d9c commit 5f7d6c9

3 files changed

Lines changed: 77 additions & 27 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx‎

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useEffect, useMemo, useRef, useState } from 'react'
3+
import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react'
44
import { Chip, ChipDropdown, ChipLink, cn } from '@sim/emcn'
55
import { ArrowLeft, Plus } from '@sim/emcn/icons'
66
import { useRouter } from 'next/navigation'
@@ -67,44 +67,82 @@ interface IntegrationBlockDetailProps {
6767
/**
6868
* Integration detail page, shown only when the viewer's integration policy
6969
* permits the integration. A direct link to a restricted one renders an
70-
* access-required state instead of the connect actions.
70+
* access-required state instead of the connect actions, and the check fails
71+
* closed until the deployment allowlist has loaded.
7172
*/
7273
export function IntegrationBlockDetail({ integration, workspaceId }: IntegrationBlockDetailProps) {
73-
const { isLoading, isBlockPermitted, isBlockRequestable } = usePermissionConfig()
74+
const {
75+
isBlockPermitted,
76+
isBlockRequestable,
77+
isIntegrationAvailabilityReady,
78+
integrationAvailabilityError,
79+
refetchIntegrationAvailability,
80+
} = usePermissionConfig()
7481
const accessRequests = useWorkspaceAccessRequestFeatures()
7582

76-
if (isLoading) return <IntegrationBlockDetailFallback workspaceId={workspaceId} />
83+
if (integrationAvailabilityError) {
84+
return (
85+
<IntegrationAccessNotice
86+
workspaceId={workspaceId}
87+
title='Unable to check access'
88+
description={integrationAvailabilityError.message}
89+
action={<Chip onClick={() => void refetchIntegrationAvailability()}>Try again</Chip>}
90+
/>
91+
)
92+
}
93+
if (!isIntegrationAvailabilityReady) {
94+
return <IntegrationBlockDetailFallback workspaceId={workspaceId} />
95+
}
7796
if (isBlockPermitted(integration.type)) {
7897
return <IntegrationBlockDetailContent integration={integration} workspaceId={workspaceId} />
7998
}
8099

81100
const requestable = accessRequests.data?.enabled === true && isBlockRequestable(integration.type)
82101

102+
return (
103+
<IntegrationAccessNotice
104+
workspaceId={workspaceId}
105+
title='Access required'
106+
description={
107+
requestable
108+
? 'Ask an administrator for access.'
109+
: 'Your organization restricts this integration.'
110+
}
111+
action={
112+
requestable ? (
113+
<RequestAccessAction
114+
scope={{ kind: 'workspace', workspaceId }}
115+
target={{ kind: 'integration', id: integration.type }}
116+
label={integration.name}
117+
variant='primary'
118+
/>
119+
) : undefined
120+
}
121+
/>
122+
)
123+
}
124+
125+
interface IntegrationAccessNoticeProps {
126+
workspaceId: string
127+
title: string
128+
description: string
129+
action?: ReactNode
130+
}
131+
132+
function IntegrationAccessNotice({
133+
workspaceId,
134+
title,
135+
description,
136+
action,
137+
}: IntegrationAccessNoticeProps) {
83138
return (
84139
<div className='flex h-full flex-col bg-[var(--bg)]'>
85140
<div className={PAGE_HEADER_BAR}>
86141
<ChipLink href={`/workspace/${workspaceId}/integrations`} leftIcon={ArrowLeft}>
87142
Integrations
88143
</ChipLink>
89144
</div>
90-
<EmptyState
91-
title='Access required'
92-
description={
93-
requestable
94-
? 'Ask an administrator for access.'
95-
: 'Your organization restricts this integration.'
96-
}
97-
action={
98-
requestable ? (
99-
<RequestAccessAction
100-
scope={{ kind: 'workspace', workspaceId }}
101-
target={{ kind: 'integration', id: integration.type }}
102-
label={integration.name}
103-
variant='primary'
104-
/>
105-
) : undefined
106-
}
107-
/>
145+
<EmptyState title={title} description={description} action={action} />
108146
</div>
109147
)
110148
}

‎apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,10 @@ function IntegrationsContent() {
152152
const workspaceId = (params?.workspaceId as string) || ''
153153
const { integrationAvailability, isBlockPermitted } = usePermissionConfig()
154154

155-
const [{ category: selectedCategory, search: urlSearchTerm }, setIntegrationFilters] =
156-
useQueryStates(integrationsParsers, integrationsUrlKeys)
155+
const [{ category: urlCategory, search: urlSearchTerm }, setIntegrationFilters] = useQueryStates(
156+
integrationsParsers,
157+
integrationsUrlKeys
158+
)
157159

158160
/**
159161
* The input is controlled directly by the instant nuqs value; only the URL
@@ -235,6 +237,16 @@ function IntegrationsContent() {
235237
...permittedCategorySections.map((section) => section.label),
236238
]
237239

240+
/**
241+
* A category the viewer's integration policy leaves empty is not offered, so a
242+
* link or a policy change that points at one falls back to All. Connected stays
243+
* selected while credentials load so the page does not flash All first.
244+
*/
245+
const selectedCategory =
246+
categoryOptions.includes(urlCategory) || (urlCategory === CONNECTED_LABEL && credentialsLoading)
247+
? urlCategory
248+
: ALL_CATEGORY
249+
238250
const isAllCategorySelected = selectedCategory === ALL_CATEGORY
239251
const isFeaturedSelected = selectedCategory === FEATURED_LABEL
240252
const isConnectedSelected = selectedCategory === CONNECTED_LABEL

‎apps/sim/hooks/use-permission-config.test.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,9 @@ describe('usePermissionConfig deployment readiness', () => {
111111
integrationAvailability: [{ type: 'airtable', state: 'unavailable', oauthAvailable: false }],
112112
})
113113
render()
114-
expect(current!.isBlockAllowed('airtable')).toBe(false)
115-
expect(current!.isBlockPermitted('airtable')).toBe(true)
116-
expect(current!.isBlockPermitted('slack')).toBe(false)
114+
expect(current.isBlockAllowed('airtable')).toBe(false)
115+
expect(current.isBlockPermitted('airtable')).toBe(true)
116+
expect(current.isBlockPermitted('slack')).toBe(false)
117117
})
118118

119119
it('does not offer a request before deployment availability resolves', () => {

0 commit comments

Comments
 (0)