Skip to content

Commit 15fb12f

Browse files
committed
fix(search): align Google source management and guides
1 parent bc3f8a5 commit 15fb12f

11 files changed

Lines changed: 213 additions & 29 deletions

File tree

‎apps/docs/content/docs/search/gmail.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ Configurations are additive: a narrower one does not restrict an existing broade
5252
</Step>
5353
</Steps>
5454

55-
<Image className="mx-auto h-auto w-full max-w-md" src="/static/search/gmail-setup.jpg" alt="Gmail Search source configuration" width={520} height={329} />
56-
5755
## Set up a central service account
5856

5957
Open **Settings → Sources**, enable **Gmail**, and select **Manage → Advanced → Add sync configuration**. If personal connections are disabled for your organization, select **Add source** from the provider page instead.
@@ -114,7 +112,9 @@ Teammates follow these same steps after joining the organization. Once an admin
114112

115113
## Source options
116114

117-
An admin opens **Settings → Sources**, selects **Manage** beside **Gmail**, then opens the configuration from **Advanced** or the source list. Select its **Settings** tab to change these options. Filters apply separately to each mailbox in the source. **Documents** shows indexed threads and **Sync history** shows recent runs.
115+
An admin opens **Settings → Sources** and selects **Manage** beside **Gmail** to open its configuration list. Each row shows **Member accounts** or **Service account** beside its sync status. Open a configuration's **Settings** tab to edit its filters, then select **Save**. Filters apply separately to each mailbox in the source. **Documents** shows indexed threads and **Sync history** shows recent runs.
116+
117+
**Sync using** identifies the configuration's fixed connection method. To replace a central credential, choose another **Indexing account** and select **Change indexing account**.
118118

119119
| Option | Behavior |
120120
| --- | --- |

‎apps/docs/content/docs/search/google-calendar.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ The default date range covers the previous and next 30 days. Save any changes to
5050
</Step>
5151
</Steps>
5252

53-
<Image className="mx-auto h-auto w-full max-w-md" src="/static/search/google-calendar-setup.jpg" alt="Google Calendar Search source configuration" width={520} height={397} />
54-
5553
<Callout type="info">
5654
`primary` means the connected or impersonated person's main calendar. A calendar selected from the list is a specific calendar ID, even when it is your main calendar. That same ID applies to every selected user, and only users with access to it can search its events.
5755
</Callout>
@@ -116,7 +114,9 @@ Teammates repeat only these connection steps after joining the organization. The
116114

117115
## Source options
118116

119-
An admin opens **Settings → Sources**, selects **Manage** beside **Google Calendar**, then opens the configuration from **Advanced** or the source list. Select its **Settings** tab to change these options. **Documents** shows indexed events and **Sync history** shows recent runs.
117+
An admin opens **Settings → Sources** and selects **Manage** beside **Google Calendar** to open its configuration list. Each row shows **Member accounts** or **Service account** beside its sync status. Open a configuration's **Settings** tab to edit its filters, then select **Save**. **Documents** shows indexed events and **Sync history** shows recent runs.
118+
119+
**Sync using** identifies the configuration's fixed connection method. To replace a central credential, choose another **Indexing account** and select **Change indexing account**.
120120

121121
| Option | Behavior |
122122
| --- | --- |

‎apps/docs/content/docs/search/google-drive.mdx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Paste that Client ID into **Client ID**, then enter these exact scopes as a comm
8888
https://www.googleapis.com/auth/drive.readonly,https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/admin.directory.group.readonly,https://www.googleapis.com/auth/admin.directory.domain.readonly
8989
```
9090

91-
Select **Authorize**, then **View details** to confirm all four scopes were saved. If your organization requires multi-party approval, another super administrator must approve the request. Delegation changes can take up to 24 hours to propagate. See Google's [Admin Console delegation guide](https://knowledge.workspace.google.com/admin/apps/control-api-access-with-domain-wide-delegation).
91+
Select **Authorize**, then **View details** to confirm all four scopes were saved. If you reuse a Gmail or Calendar service account, retain those services' required scopes and add any missing Drive scopes. If your organization requires multi-party approval, another super administrator must approve the request. Delegation changes can take up to 24 hours to propagate. See Google's [Admin Console delegation guide](https://knowledge.workspace.google.com/admin/apps/control-api-access-with-domain-wide-delegation).
9292

9393
These are Search's central crawl scopes. The general [Google service account guide](/integrations/google-service-account) includes broader scopes for workflow actions; do not copy those into this Search setup.
9494

@@ -123,6 +123,10 @@ Teammates join the Sim organization with their matching verified email; they do
123123

124124
## Source options
125125

126+
An admin opens **Settings → Sources** and selects **Manage** beside **Google Drive** to open its configuration list. Each row shows **Member accounts** or **Service account** beside its sync status. Open a configuration's **Settings** tab to edit its filters, then select **Save**. **Documents** shows indexed files and **Sync history** shows recent runs.
127+
128+
**Sync using** identifies the configuration's fixed connection method. To replace a central credential, choose another **Indexing account** and select **Change indexing account**.
129+
126130
| Option | Behavior |
127131
| --- | --- |
128132
| Folders / Folder IDs | Optional. Includes files in each selected folder and its accessible subfolders. A folder selection does not grant access. |
@@ -135,7 +139,7 @@ Teammates join the Sim organization with their matching verified email; they do
135139

136140
Sim exports Docs and Slides as text and Sheets as XLSX spreadsheets. Supported uploaded files use the knowledge-base document pipeline, including PDF and Office formats. Unsupported files and oversized exports cannot be indexed; Google limits Workspace exports to 10 MB. See [Drive export formats](https://developers.google.com/workspace/drive/api/guides/ref-export-formats) and [download limits](https://developers.google.com/workspace/drive/api/guides/manage-downloads).
137141

138-
Search schedules syncs hourly. Central crawls revisit the selected users' files and permissions, including unchanged files, so permission changes and a new employee's older files are included. Unfinished crawls resume before deletion reconciliation. Content, deletions, and permissions refresh in the background; results are not a live read from Drive. Open **Settings → Sources**, select **Manage** beside **Google Drive**, then select its configuration from **Advanced** (or the source list when personal connections are disabled) to inspect **Documents**, edit **Settings**, or review **Sync history**. **Accounts** on the provider page shows personal account connections where configured; it does not list the central service-account credential.
142+
Search schedules syncs hourly. Central crawls revisit the selected users' files and permissions, including unchanged files, so permission changes and a new employee's older files are included. Unfinished crawls resume before deletion reconciliation. Content, deletions, and permissions refresh in the background; results are not a live read from Drive. **Accounts** on the provider page shows personal account connections where configured; it does not list the central service-account credential.
139143

140144
## Troubleshooting
141145

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ async function click(label: string) {
129129
}
130130

131131
describe('organization integration management entry', () => {
132+
it('uses Sources terminology in search and its empty state', async () => {
133+
await render('?search=not-a-real-source')
134+
expect(container.querySelector('input[placeholder="Search sources..."]')).toHaveValue(
135+
'not-a-real-source'
136+
)
137+
expect(container.textContent).toContain('No matching sources')
138+
expect(container.textContent).not.toContain('No matching integrations')
139+
})
132140
it('offers Drive account management before anyone has connected', async () => {
133141
mocks.overview.mockReturnValue({
134142
data: {

‎apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-setup.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function OrganizationIntegrationsSetup() {
6161

6262
return (
6363
<SettingsPanel
64-
search={{ value: search, onChange: setSearch, placeholder: 'Search integrations...' }}
64+
search={{ value: search, onChange: setSearch, placeholder: 'Search sources...' }}
6565
>
6666
{availability.integrationAvailabilityError && (
6767
<SettingsQueryErrorState
@@ -84,7 +84,7 @@ export function OrganizationIntegrationsSetup() {
8484
) : overview.isPending ? (
8585
<SettingsEmptyState variant='inline'>Loading sources…</SettingsEmptyState>
8686
) : visible.length === 0 ? (
87-
<SettingsEmptyState variant='inline'>No matching integrations</SettingsEmptyState>
87+
<SettingsEmptyState variant='inline'>No matching sources</SettingsEmptyState>
8888
) : (
8989
visible.map(([type, meta]) => {
9090
const provider = providers.get(type)

‎apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.test.tsx‎

Lines changed: 137 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,17 @@ vi.mock('@/lib/credential-groups/providers', () => ({
5353
}))
5454
vi.mock('@/connectors/registry', () => ({
5555
CONNECTOR_META_REGISTRY: {
56-
google_drive: { name: 'Google Drive', auth: { mode: 'oauth', provider: 'google-drive' } },
57-
gmail: { name: 'Gmail', auth: { mode: 'oauth', provider: 'google-email' } },
56+
google_drive: {
57+
name: 'Google Drive',
58+
auth: { mode: 'oauth', provider: 'google-drive', adminCredentialType: 'service_account' },
59+
},
60+
gmail: {
61+
name: 'Gmail',
62+
auth: { mode: 'oauth', provider: 'google-email', adminCredentialType: 'service_account' },
63+
},
5864
google_calendar: {
5965
name: 'Google Calendar',
60-
auth: { mode: 'oauth', provider: 'google-calendar' },
66+
auth: { mode: 'oauth', provider: 'google-calendar', adminCredentialType: 'service_account' },
6167
},
6268
slack: { name: 'Slack', auth: { mode: 'oauth', provider: 'slack' } },
6369
gitlab: { name: 'GitLab', auth: { mode: 'apiKey' } },
@@ -111,11 +117,17 @@ vi.mock('@/app/o/[organizationId]/settings/components/integrations/slack-account
111117
import { SettingsHeaderProvider, SettingsHeaderShell } from '@/components/settings/settings-header'
112118
import { OrganizationProviderDetail } from '@/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail'
113119

114-
const provider = { connectorType: 'google_drive', approved: true, status: 'active' }
120+
const provider = {
121+
connectorType: 'google_drive',
122+
approved: true,
123+
status: 'active',
124+
sourceCount: 0,
125+
}
115126
const source = {
116127
connectorId: 'source-one',
117128
connectorType: 'google_drive',
118129
sourceDescription: 'Engineering handbook',
130+
accessMode: 'admin',
119131
enabled: true,
120132
hasSyncError: false,
121133
isSyncing: false,
@@ -227,7 +239,7 @@ describe('organization provider management', () => {
227239
})
228240
mocks.accounts.mockReturnValue({ data: { credentialGroup: null }, isPending: false })
229241
mocks.sources.mockReturnValue({ data: [], isPending: false })
230-
await render(connectorType)
242+
await render(connectorType, '?view=accounts')
231243
expect(container.textContent).toContain('Waiting for connections')
232244
expect(container.textContent).toContain(
233245
'Members connect their accounts from Integrations. Indexing starts automatically.'
@@ -259,17 +271,19 @@ describe('organization provider management', () => {
259271
'does not ask for personal connections when %s already has a central source',
260272
async (connectorType) => {
261273
mocks.overview.mockReturnValue({
262-
data: { providers: [{ ...provider, connectorType, sourceCount: 1 }] },
274+
data: {
275+
providers: [{ ...provider, connectorType, sourceCount: 1 }],
276+
},
263277
})
264278
mocks.accounts.mockReturnValue({ data: { credentialGroup: null }, isPending: false })
265279
mocks.access = { admin: true, members: true }
266280
await render(connectorType)
267281

268-
expect(container.textContent).toContain('No connected member accounts.')
282+
expect(container.textContent).toContain('Engineering handbook')
283+
expect(container.textContent).not.toContain('No connected member accounts.')
269284
expect(container.textContent).not.toContain(
270285
'Members connect their accounts from Integrations.'
271286
)
272-
await click('Advanced')
273287
await click('Add sync configuration')
274288
await vi.waitFor(() => {
275289
const params = mocks.updateUrl.mock.calls.at(-1)![0].searchParams
@@ -279,6 +293,120 @@ describe('organization provider management', () => {
279293
}
280294
)
281295

296+
describe.each(['gmail', 'google_calendar', 'google_drive'])(
297+
'%s default management view',
298+
(connectorType) => {
299+
function withSources(accessModes: string[]) {
300+
mocks.overview.mockReturnValue({
301+
data: { providers: [{ ...provider, connectorType, sourceCount: accessModes.length }] },
302+
isPending: false,
303+
})
304+
mocks.sources.mockReturnValue({
305+
data: accessModes.map((accessMode, index) => ({
306+
...source,
307+
connectorType,
308+
accessMode,
309+
connectorId: `source-${index}`,
310+
})),
311+
isPending: false,
312+
})
313+
}
314+
315+
it.each([
316+
{ name: 'member-only', modes: ['members'] },
317+
{ name: 'central-only', modes: ['admin'] },
318+
{ name: 'mixed', modes: ['admin', 'members'] },
319+
{ name: 'not configured', modes: [] },
320+
])('opens configurations for the $name setup', async ({ modes }) => {
321+
withSources(modes)
322+
await render(connectorType)
323+
expect(container.querySelector('[role="radio"][aria-checked="true"]')).toHaveTextContent(
324+
'Advanced'
325+
)
326+
expect(mocks.sources).toHaveBeenLastCalledWith(
327+
expect.any(Object),
328+
expect.objectContaining({ enabled: true })
329+
)
330+
expect(mocks.accounts).toHaveBeenLastCalledWith(undefined)
331+
expect(container.textContent).toContain('Add sync configuration')
332+
if (modes.length === 0)
333+
expect(container.textContent).toContain('No sync configurations yet.')
334+
})
335+
336+
it.each(['accounts', 'sources'])('honors explicit %s links', async (view) => {
337+
withSources(['admin'])
338+
await render(connectorType, `?view=${view}`)
339+
expect(mocks.sources).toHaveBeenLastCalledWith(
340+
expect.any(Object),
341+
expect.objectContaining({ enabled: view === 'sources' })
342+
)
343+
expect(container.querySelector('[role="radio"][aria-checked="true"]')).toHaveTextContent(
344+
view === 'accounts' ? 'Accounts' : 'Advanced'
345+
)
346+
})
347+
348+
it('loads the configuration list in parallel with its overview and retains the default', async () => {
349+
mocks.overview.mockReturnValue({ isPending: true })
350+
await render(connectorType)
351+
expect(container.textContent).toContain('Loading integration…')
352+
expect(container.textContent).not.toContain('No connected member accounts.')
353+
expect(mocks.accounts).toHaveBeenLastCalledWith(undefined)
354+
expect(mocks.sources).toHaveBeenLastCalledWith(
355+
expect.any(Object),
356+
expect.objectContaining({ enabled: true })
357+
)
358+
withSources(['members'])
359+
await render(connectorType)
360+
expect(container.querySelector('[role="radio"][aria-checked="true"]')).toHaveTextContent(
361+
'Advanced'
362+
)
363+
expect(container.textContent).toContain('Engineering handbook')
364+
expect(mocks.sources).toHaveBeenLastCalledWith(
365+
expect.any(Object),
366+
expect.objectContaining({ enabled: true })
367+
)
368+
expect(mocks.accounts).toHaveBeenLastCalledWith(undefined)
369+
})
370+
371+
it('preserves an explicit Accounts choice when the overview changes', async () => {
372+
withSources(['members'])
373+
await render(connectorType)
374+
await click('Accounts')
375+
await vi.waitFor(() =>
376+
expect(mocks.updateUrl.mock.calls.at(-1)?.[0].searchParams.get('view')).toBe('accounts')
377+
)
378+
withSources(['admin'])
379+
await render(connectorType)
380+
expect(container.querySelector('[role="radio"][aria-checked="true"]')).toHaveTextContent(
381+
'Accounts'
382+
)
383+
expect(mocks.sources).toHaveBeenLastCalledWith(
384+
expect.any(Object),
385+
expect.objectContaining({ enabled: false })
386+
)
387+
})
388+
389+
it.each([
390+
{ accessMode: 'admin', method: 'Service account' },
391+
{ accessMode: 'members', method: 'Member accounts' },
392+
])(
393+
'identifies $method configurations without changing their title or destination',
394+
async ({ accessMode, method }) => {
395+
withSources([accessMode])
396+
mocks.sources.mockReturnValue({
397+
data: [{ ...source, connectorType, accessMode }],
398+
isPending: false,
399+
})
400+
await render(connectorType)
401+
expect(container.textContent).toContain(`${method} · Last synced`)
402+
expect(
403+
container.querySelector('a[aria-label="Open Engineering handbook"]')
404+
).toHaveAttribute('href', '/o/org-one/settings/integrations/sources/source-one')
405+
}
406+
)
407+
}
408+
)
409+
282410
it.each(['active', 'disabled'])(
283411
'removes only Slack account setup after confirmation, including a %s option',
284412
async (status) => {
@@ -411,7 +539,7 @@ describe('organization provider management', () => {
411539
expect(mocks.people).not.toHaveBeenCalled()
412540
expect(
413541
container.querySelector(
414-
`input[placeholder="${params === '?view=sources' ? 'Search sync configurations...' : 'Search people...'}"]`
542+
`input[placeholder="${params === '?view=accounts' ? 'Search people...' : 'Search sync configurations...'}"]`
415543
)
416544
).toBeEnabled()
417545
}

‎apps/sim/app/o/[organizationId]/settings/integrations/providers/[connectorType]/provider-detail.tsx‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
6060
const automaticSetup = Boolean(meta && canConnectWithDefaults(meta) && searchAccess.memberScoped)
6161
const [view, setView] = useQueryState(
6262
organizationProviderTabParam.key,
63-
organizationProviderTabParam.parser.withDefault(automaticSetup ? 'accounts' : 'sources')
63+
organizationProviderTabParam.parser
6464
)
6565
const [search, setSearch] = useSettingsSearch()
6666
const [peopleSearch, setPeopleSearch] = useOrganizationAccountPeopleSearch()
@@ -260,7 +260,13 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
260260
<SettingsResourceRow
261261
key={source.connectorId}
262262
title={source.sourceDescription || meta.name}
263-
description={
263+
description={[
264+
source.accessMode === 'members'
265+
? 'Member accounts'
266+
: meta.auth.mode === 'oauth' &&
267+
meta.auth.adminCredentialType === 'service_account'
268+
? 'Service account'
269+
: 'Admin account',
264270
!approved
265271
? 'Deactivated'
266272
: !source.enabled
@@ -275,8 +281,8 @@ export function OrganizationProviderDetail({ connectorType }: OrganizationProvid
275281
? 'Indexing'
276282
: source.lastSyncAt
277283
? `Last synced ${format(new Date(source.lastSyncAt), 'MMM d, h:mm a')}`
278-
: 'Waiting for the first sync'
279-
}
284+
: 'Waiting for the first sync',
285+
].join(' · ')}
280286
href={organizationRoutes(organization.id).searchSource(source.connectorId)}
281287
clickLabel={`Open ${source.sourceDescription || meta.name}`}
282288
navigable

‎apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/connector-settings-fields.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ describe('connector settings service-account choices', () => {
366366
},
367367
]
368368
await render(confluenceConnectorMeta)
369-
expect(container.textContent).toContain('Service account')
369+
expect(container.textContent).toContain('Indexing account')
370370
await openAccountChoices()
371371
expect(
372372
Array.from(document.querySelectorAll<HTMLElement>('[role="option"]')).map((node) =>

0 commit comments

Comments
 (0)