Skip to content

Commit c011716

Browse files
committed
Add organization-scoped live search and admin-managed GitLab
1 parent eb22479 commit c011716

45 files changed

Lines changed: 3367 additions & 357 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/app/api/knowledge/sim-search/integrations/route.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export const PUT = defineInternalJsonRoute({
3333
errorPolicy: internalKnowledgeErrorPolicies.connectors,
3434
mapInput: ({ body }) => body,
3535
useCase: approveSearchIntegration,
36-
present: ({ connectorType, approved }) => ({
36+
present: ({ connectorType, approved, policy }) => ({
3737
success: true as const,
38-
data: { connectorType, approved },
38+
data: { connectorType, approved, ...(policy ? { policy } : {}) },
3939
}),
4040
})

‎apps/sim/app/o/[organizationId]/integrations/integrations.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ export function OrganizationIntegrations({
3232
return (
3333
<OrganizationPage
3434
title='Integrations'
35-
description='Connect your accounts for Sim Search'
35+
description={
36+
features.liveEnterpriseSearch
37+
? 'Search documents, messages, and code from your connected apps'
38+
: 'Connect your accounts for Sim Search'
39+
}
3640
searchMode='expanded'
3741
searchPlaceholder='Search integrations'
3842
action={
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/** @vitest-environment jsdom */
2+
import { act } from 'react'
3+
import { createRoot, type Root } from 'react-dom/client'
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5+
6+
const mocks = vi.hoisted(() => ({
7+
inventory: vi.fn(),
8+
policies: vi.fn(),
9+
connect: vi.fn(),
10+
reconnect: vi.fn(),
11+
refetch: vi.fn(),
12+
}))
13+
vi.mock('@/hooks/queries/organization-accounts', () => ({
14+
useOrganizationAccounts: mocks.inventory,
15+
useConnectOrganizationAccount: () => ({ mutate: mocks.connect }),
16+
useReconnectPersonalOrganizationAccount: () => ({ mutate: mocks.reconnect }),
17+
}))
18+
vi.mock('@/hooks/queries/search-integrations', () => ({ useSearchIntegrations: mocks.policies }))
19+
vi.mock('@/app/o/[organizationId]/integrations/disconnect-account-menu', () => ({
20+
DisconnectAccountMenu: ({ accounts }: { accounts: { displayName: string }[] }) => (
21+
<span>{accounts.map((account) => account.displayName).join(', ')}</span>
22+
),
23+
}))
24+
vi.mock('@/app/workspace/[workspaceId]/integrations/components/integrations-showcase', () => ({
25+
IntegrationTile: () => null,
26+
}))
27+
28+
import { LiveMemberIntegrations } from '@/app/o/[organizationId]/integrations/live-member-integrations'
29+
30+
let root: Root
31+
let container: HTMLDivElement
32+
const group = {
33+
status: 'active',
34+
options: [
35+
{
36+
id: 'github-option',
37+
provider: 'github-repositories',
38+
status: 'active',
39+
configurationStatus: 'ready',
40+
},
41+
],
42+
mcpServers: [],
43+
}
44+
const inventory = (overrides = {}) => ({
45+
credentialGroup: group,
46+
viewerAccounts: [],
47+
viewerMcpAccounts: [],
48+
canManage: false,
49+
...overrides,
50+
})
51+
beforeEach(() => {
52+
vi.clearAllMocks()
53+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
54+
mocks.inventory.mockReturnValue({ data: inventory(), refetch: mocks.refetch })
55+
mocks.policies.mockReturnValue({
56+
data: [{ connectorType: 'github', approved: true }],
57+
refetch: mocks.refetch,
58+
})
59+
container = document.createElement('div')
60+
document.body.appendChild(container)
61+
root = createRoot(container)
62+
})
63+
afterEach(() => {
64+
act(() => root.unmount())
65+
container.remove()
66+
vi.unstubAllGlobals()
67+
})
68+
const render = async (search = '') => {
69+
await act(async () =>
70+
root.render(<LiveMemberIntegrations organizationId='org' search={search} />)
71+
)
72+
}
73+
const button = (label: string) =>
74+
[...container.querySelectorAll('button')].find((button) => button.textContent === label)
75+
describe('live member connection states', () => {
76+
it('keeps GitLab organization-managed with no personal token or connect action', async () => {
77+
mocks.policies.mockReturnValue({ data: [{ connectorType: 'gitlab', approved: true }] })
78+
await render()
79+
expect(container.textContent).toContain('Organization managed')
80+
expect(container.textContent).not.toContain('personal access token')
81+
expect(button('Connect')).toBeUndefined()
82+
})
83+
it('connects the active provider option in this organization', async () => {
84+
await render()
85+
await act(async () => button('Connect')!.click())
86+
expect(mocks.connect).toHaveBeenCalledWith(
87+
{ organizationId: 'org', optionId: 'github-option' },
88+
expect.any(Object)
89+
)
90+
})
91+
it.each([
92+
['disabled', { credentialGroup: { ...group, status: 'disabled' } }],
93+
['unconfigured', { credentialGroup: null }],
94+
])('does not offer a connection when %s', async (_name, overrides) => {
95+
mocks.inventory.mockReturnValue({ data: inventory(overrides) })
96+
await render()
97+
expect(button('Connect')).toBeUndefined()
98+
})
99+
it('shows the current account and reconnect action without enabling a disabled integration', async () => {
100+
mocks.inventory.mockReturnValue({
101+
data: inventory({
102+
viewerAccounts: [
103+
{
104+
credentialId: 'own',
105+
providerId: 'github-repositories',
106+
displayName: 'reader',
107+
status: 'needs_reauth',
108+
},
109+
],
110+
}),
111+
})
112+
await render()
113+
await act(async () => button('Reconnect')!.click())
114+
expect(mocks.reconnect).toHaveBeenCalledWith('own', expect.any(Object))
115+
mocks.policies.mockReturnValue({ data: [{ connectorType: 'github', approved: false }] })
116+
await render()
117+
expect(button('Reconnect')?.disabled).toBe(true)
118+
expect(button('Add account')).toBeUndefined()
119+
})
120+
it('never presents another MCP provider as a Coda connection', async () => {
121+
mocks.policies.mockReturnValue({ data: [{ connectorType: 'coda', approved: true }] })
122+
mocks.inventory.mockReturnValue({
123+
data: inventory({
124+
credentialGroup: {
125+
...group,
126+
mcpServers: [{ id: 'coda-server', managedConnectorId: 'coda', enabled: true }],
127+
},
128+
viewerMcpAccounts: [
129+
{
130+
credentialId: 'other',
131+
mcpServerId: 'other-server',
132+
displayName: 'Other private account',
133+
status: 'active',
134+
},
135+
],
136+
}),
137+
})
138+
await render()
139+
expect(container.textContent).not.toContain('Other private account')
140+
await act(async () => button('Connect')!.click())
141+
expect(mocks.connect).toHaveBeenCalledWith(
142+
{ organizationId: 'org', mcpServerId: 'coda-server' },
143+
expect.any(Object)
144+
)
145+
})
146+
it('distinguishes filtered empty, loading, and failed states', async () => {
147+
await render('nothing')
148+
expect(container.textContent).toContain('No integrations match')
149+
mocks.inventory.mockReturnValue({})
150+
await render()
151+
expect(container.textContent).toContain('Loading your connections')
152+
mocks.inventory.mockReturnValue({
153+
error: new Error('Network unavailable'),
154+
refetch: mocks.refetch,
155+
})
156+
await render()
157+
expect(container.textContent).toContain('Network unavailable')
158+
})
159+
})

0 commit comments

Comments
 (0)