Skip to content

Commit 0db78fd

Browse files
committed
fix(search): make service account setup directly accessible
1 parent 92e66ee commit 0db78fd

3 files changed

Lines changed: 125 additions & 49 deletions

File tree

‎apps/sim/app/o/[organizationId]/settings/components/integrations/live-search-policy-modal.tsx‎

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ChipSwitch,
1515
toast,
1616
} from '@sim/emcn'
17+
import { Plus } from '@sim/emcn/icons'
1718
import { getErrorMessage } from '@sim/utils/errors'
1819
import { useRouter } from 'next/navigation'
1920
import type { SearchIntegrationApproval } from '@/lib/api/contracts/knowledge/search-integrations'
@@ -25,6 +26,7 @@ import {
2526
type LiveSearchPolicy,
2627
normalizeLiveSearchPolicy,
2728
} from '@/lib/sim-search/live/policy-schema'
29+
import { searchSetupParam, serializeSearchSetup } from '@/lib/sim-search/search-params'
2830
import { SearchSourcePagination } from '@/app/o/[organizationId]/settings/components/integrations/search-source-pagination'
2931
import { useSearchSources } from '@/hooks/queries/kb/connectors'
3032
import { useUpdateSearchIntegration } from '@/hooks/queries/search-integrations'
@@ -59,7 +61,7 @@ export function LiveSearchPolicyModal({
5961
const close = () => {
6062
if (!update.isPending) onClose()
6163
}
62-
const save = () => {
64+
const save = (openSetup = needsServiceSetup) => {
6365
if (update.isPending) return
6466
try {
6567
const policy = normalizeLiveSearchPolicy(provider, {
@@ -78,8 +80,13 @@ export function LiveSearchPolicyModal({
7880
{
7981
onSuccess: () => {
8082
onClose()
81-
if (needsServiceSetup)
82-
router.push(organizationRoutes(organizationId).searchProvider(provider))
83+
if (openSetup)
84+
router.push(
85+
serializeSearchSetup(organizationRoutes(organizationId).searchProvider(provider), {
86+
addConnector: searchSetupParam.parser.parse(provider),
87+
'source-access': null,
88+
})
89+
)
8390
else toast.success('Search settings saved')
8491
},
8592
}
@@ -161,6 +168,8 @@ export function LiveSearchPolicyModal({
161168
sourceId={sourceId}
162169
onChange={setSourceId}
163170
canManage={integration.approved}
171+
onAdd={() => save(true)}
172+
disabled={update.isPending}
164173
/>
165174
))}
166175
<ChipModalError>{error || update.error?.message}</ChipModalError>
@@ -173,12 +182,12 @@ export function LiveSearchPolicyModal({
173182
? 'Saving…'
174183
: needsServiceSetup
175184
? provider === 'github'
176-
? 'Save and add repositories'
185+
? 'Add repository'
177186
: provider === 'gitlab'
178-
? 'Save and add projects'
179-
: 'Save and add connection'
187+
? 'Add project'
188+
: 'Add connection'
180189
: 'Save settings',
181-
onClick: save,
190+
onClick: () => save(),
182191
disabled: update.isPending,
183192
}}
184193
/>
@@ -192,6 +201,8 @@ interface ServiceAccountSourceProps {
192201
sourceId: string
193202
onChange: (sourceId: string) => void
194203
canManage: boolean
204+
onAdd: () => void
205+
disabled: boolean
195206
}
196207

197208
function ServiceAccountSource({
@@ -200,6 +211,8 @@ function ServiceAccountSource({
200211
sourceId,
201212
onChange,
202213
canManage,
214+
onAdd,
215+
disabled,
203216
}: ServiceAccountSourceProps) {
204217
const sources = useSearchSources(
205218
{ kind: 'organization', organizationId },
@@ -212,28 +225,36 @@ function ServiceAccountSource({
212225
label: `${source.sourceDescription || connectorDisplayName(provider)}${source.enabled ? '' : ' · Paused'}`,
213226
disabled: !source.enabled || source.availability !== 'available',
214227
}))
228+
if (
229+
!sourceId &&
230+
!sources.isPending &&
231+
!sources.isError &&
232+
!options.length &&
233+
!sources.hasNextPage
234+
)
235+
return null
215236
return (
216-
<ChipModalField
217-
type='custom'
218-
title='Service account connection'
219-
hint={!sourceId && canManage ? 'Select a connection before search can run.' : undefined}
220-
>
237+
<ChipModalField type='custom' title='Service account connection'>
221238
<div className='flex flex-col items-start gap-2'>
222-
<ChipSelect
223-
aria-label='Service account connection'
224-
options={options}
225-
value={sourceId}
226-
onChange={onChange}
227-
placeholder={sources.isPending ? 'Loading connections…' : 'Select a connection'}
228-
displayLabel={
229-
sourceId && !options.some((option) => option.value === sourceId)
230-
? 'Configured connection'
231-
: undefined
232-
}
233-
disabled={sources.isPending || sources.isError}
234-
searchable
235-
fullWidth
236-
/>
239+
{(sourceId || options.length > 0 || sources.isPending) && (
240+
<ChipSelect
241+
aria-label='Service account connection'
242+
options={options}
243+
value={sourceId}
244+
onChange={onChange}
245+
placeholder={sources.isPending ? 'Loading connections…' : 'Select a connection'}
246+
displayLabel={
247+
sourceId && !options.some((option) => option.value === sourceId)
248+
? 'Configured connection'
249+
: undefined
250+
}
251+
disabled={disabled || sources.isPending || sources.isError || !options.length}
252+
searchable
253+
fullWidth
254+
dropdownWidth='trigger'
255+
align='start'
256+
/>
257+
)}
237258
{sources.isError && !sources.isFetchNextPageError && (
238259
<>
239260
<ChipModalError>{sources.error?.message}</ChipModalError>
@@ -243,17 +264,18 @@ function ServiceAccountSource({
243264
</>
244265
)}
245266
<SearchSourcePagination {...sources} />
246-
{canManage ? (
247-
<ChipLink
248-
href={
249-
sourceId
250-
? organizationRoutes(organizationId).searchSource(sourceId)
251-
: organizationRoutes(organizationId).searchProvider(provider)
252-
}
253-
>
254-
{sourceId ? 'Edit connection and resources' : 'Set up service account'}
255-
</ChipLink>
256-
) : null}
267+
{sourceId && (
268+
<div className='flex flex-wrap gap-2'>
269+
{canManage && (
270+
<ChipLink href={organizationRoutes(organizationId).searchSource(sourceId)}>
271+
Edit connection
272+
</ChipLink>
273+
)}
274+
<Chip leftIcon={Plus} disabled={disabled} onClick={onAdd}>
275+
Add connection
276+
</Chip>
277+
</div>
278+
)}
257279
</div>
258280
</ChipModalField>
259281
)

‎apps/sim/app/o/[organizationId]/settings/components/integrations/live-search-settings.test.tsx‎

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,8 @@ describe('live search administration', () => {
215215
await act(async () =>
216216
(document.querySelector('button[aria-label="Set up GitHub"]') as HTMLButtonElement).click()
217217
)
218-
expect(button('Save and add repositories')).toBeDefined()
219-
await act(async () => button('Save and add repositories')!.click())
218+
expect(button('Add repository')).toBeDefined()
219+
await act(async () => button('Add repository')!.click())
220220
expect(mocks.save).toHaveBeenCalledWith(
221221
expect.objectContaining({
222222
connectorType: 'github',
@@ -226,7 +226,9 @@ describe('live search administration', () => {
226226
expect.any(Object)
227227
)
228228
await act(async () => mocks.save.mock.calls.at(-1)?.[1].onSuccess())
229-
expect(mocks.push).toHaveBeenCalledWith('/o/org/settings/integrations/providers/github')
229+
expect(mocks.push).toHaveBeenCalledWith(
230+
'/o/org/settings/integrations/providers/github?addConnector=github'
231+
)
230232
})
231233
it('uses the standard settings empty state before the first source', async () => {
232234
mocks.policies.mockReturnValue({ data: [], refetch: mocks.refetch })
@@ -313,7 +315,7 @@ describe('live search administration', () => {
313315
expect.any(Object)
314316
)
315317
})
316-
it('keeps service-account search fail-closed while setup is unfinished', async () => {
318+
it('opens connection setup directly without an empty selector and keeps search fail-closed', async () => {
317319
await act(async () =>
318320
root.render(
319321
<LiveSearchPolicyModal
@@ -325,11 +327,8 @@ describe('live search administration', () => {
325327
)
326328
expect(document.querySelector('textarea')).toBeNull()
327329
await act(async () => button('Service account')!.click())
328-
expect(document.body.textContent).toContain('Select a connection before search can run.')
329-
expect(
330-
document.querySelector('a[href="/o/org/settings/integrations/providers/gmail"]')
331-
).not.toBeNull()
332-
await act(async () => button('Save and add connection')!.click())
330+
expect(document.querySelector('[aria-label="Service account connection"]')).toBeNull()
331+
await act(async () => button('Add connection')!.click())
333332
expect(mocks.save).toHaveBeenCalledWith(
334333
expect.objectContaining({
335334
approved: true,
@@ -338,6 +337,56 @@ describe('live search administration', () => {
338337
expect.any(Object)
339338
)
340339
expect(mocks.save.mock.calls.at(-1)?.[0].policy.sourceId).toBeUndefined()
340+
expect(mocks.push).not.toHaveBeenCalled()
341+
await act(async () => mocks.save.mock.calls.at(-1)?.[1].onSuccess())
342+
expect(mocks.push).toHaveBeenCalledWith(
343+
'/o/org/settings/integrations/providers/gmail?addConnector=gmail'
344+
)
345+
})
346+
it('can add another connection without clearing the currently configured source', async () => {
347+
mocks.sources.mockReturnValue({
348+
data: [
349+
{
350+
connectorId: 'current-source',
351+
sourceDescription: 'Existing service account',
352+
accessMode: 'admin',
353+
enabled: true,
354+
availability: 'available',
355+
},
356+
],
357+
hasNextPage: false,
358+
})
359+
await act(async () =>
360+
root.render(
361+
<LiveSearchPolicyModal
362+
organizationId='org'
363+
integration={{
364+
connectorType: 'gmail',
365+
approved: true,
366+
policy: {
367+
...defaultLiveSearchPolicy(),
368+
accessMode: 'service_account',
369+
sourceId: 'current-source',
370+
},
371+
}}
372+
onClose={vi.fn()}
373+
/>
374+
)
375+
)
376+
expect(document.body.textContent).toContain('Existing service account')
377+
expect(button('Save settings')).toBeDefined()
378+
await act(async () => button('Add connection')!.click())
379+
expect(mocks.save).toHaveBeenCalledWith(
380+
expect.objectContaining({
381+
policy: expect.objectContaining({ sourceId: 'current-source' }),
382+
}),
383+
expect.any(Object)
384+
)
385+
expect(mocks.push).not.toHaveBeenCalled()
386+
await act(async () => mocks.save.mock.calls.at(-1)?.[1].onSuccess())
387+
expect(mocks.push).toHaveBeenCalledWith(
388+
'/o/org/settings/integrations/providers/gmail?addConnector=gmail'
389+
)
341390
})
342391
it('preserves a configured source while its paginated inventory is loading', async () => {
343392
mocks.sources.mockReturnValue({ isPending: true, hasNextPage: false })
@@ -403,7 +452,7 @@ describe('live search administration', () => {
403452
await act(async () => button('GitHub App')!.click())
404453
expect(document.body.textContent).toContain('Manage GitHub repositories')
405454
expect(document.body.textContent).not.toContain('Service account connection')
406-
await act(async () => button('Save and add repositories')!.click())
455+
await act(async () => button('Add repository')!.click())
407456
expect(mocks.save).toHaveBeenCalledWith(
408457
expect.objectContaining({
409458
connectorType: 'github',

‎apps/sim/lib/sim-search/search-params.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseAsString, parseAsStringLiteral } from 'nuqs/server'
1+
import { createSerializer, parseAsString, parseAsStringLiteral } from 'nuqs/server'
22

33
const SEARCH_SETUP_SOURCES = [
44
'coda',
@@ -24,6 +24,11 @@ export const searchSetupAccessParam = {
2424
parser: parseAsStringLiteral(['members']),
2525
} as const
2626

27+
export const serializeSearchSetup = createSerializer({
28+
[searchSetupParam.key]: searchSetupParam.parser,
29+
[searchSetupAccessParam.key]: searchSetupAccessParam.parser,
30+
})
31+
2732
/** Null closes the source management panel. */
2833
export const managedSourceParam = {
2934
key: 'manage-source',

0 commit comments

Comments
 (0)