feat(search): unify source setup and organization usage by waleedlatif1 · Pull Request #7735 · simstudioai/sim · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions apps/docs/content/docs/search/confluence.mdx
26 changes: 18 additions & 8 deletions apps/docs/content/docs/search/jira.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,11 @@ Open your organization's **Settings → Sources**, turn on **Jira**, then select

### Choose the projects

Enter **Jira Domain**. Under **Account for browsing**, select an account or choose **Connect Jira account**, then select **Projects**. To enter keys such as `ENG, SUPPORT` manually, use the switch beside Projects; this works without a browsing account.
Under **Account for browsing**, select a saved account (including your personal Search account) or choose **Connect Jira account**. Enter **Jira Domain**, then choose **Projects**. **Select all** selects every project the account can currently browse; newly created projects are not added automatically. Clear the picker search before selecting all.

**Account for browsing** only loads the project picker, using OAuth or **Add service account**. It does not enroll anyone for Search. A service account needs Jira access and the [Jira read scopes](/integrations/atlassian-service-account#jira-and-jira-service-management); Confluence-only tokens do not work here.
To enter keys such as `ENG, SUPPORT` manually, use the switch beside **Projects**; this works without a browsing account. Switching between the picker and manual entry keeps your selection.

<Image className="mx-auto h-auto w-full max-w-md"
src="/static/search/jira-setup.png"
alt="Jira source setup with an account for browsing, site domain, and projects"
width={500}
height={420}
/>
**Account for browsing** only loads the project picker, using OAuth or **Add service account**. It does not enroll anyone for Search. A service account needs Jira access and the [Jira read scopes](/integrations/atlassian-service-account#jira-and-jira-service-management); Confluence-only tokens do not work here.

</Step>
<Step>
Expand All @@ -71,6 +66,21 @@ Each teammate follows this step. Existing authorized accounts may already be con

For workspace Search, start from **Search → Add source**; its source-creation button is **Create & Invite**. Search manages the sync schedule and hides item limits.

## Connect before a source exists

After an admin approves Jira, a teammate can start from **Integrations → Connect**. The same setup opens when you choose **Connect a different site or content scope**:

1. Under **Your account**, select a saved account or choose **Connect account**. Authorize using the Atlassian email matching your verified Sim email.
2. Enter the hostname under **Atlassian site**, then choose **Projects**. Use **Select all** for the complete current list, or **Enter keys manually** for comma-separated keys. You can select up to 1,000 projects in this form.
3. Select **Connect & Sync**. Sim saves the selected scope and starts indexing with your account.

<Image className="mx-auto h-auto w-full max-w-md"
src="/static/search/jira-personal-setup.png"
alt="Jira personal source setup with a saved account, Atlassian site, and selected projects"
width={500}
height={462}
/>

## Manage the source

Admins open **Settings → Sources → Jira → Manage**, then a source's **Documents**, **Settings**, or **Sync history**. Metadata tags include issue type, status, priority, labels, assignee, and last updated.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed apps/docs/public/static/search/confluence-setup.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed apps/docs/public/static/search/jira-setup.png
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
listPersonalSourceSetupAccountsContract,
personalSourceSetupContract,
} from '@/lib/api/contracts/knowledge/personal-source-setup'
import {
defineInternalJsonRoute,
extendInternalErrorPolicy,
internalErrorResponse,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { internalKnowledgeErrorPolicies } from '@/lib/knowledge/api/route-policies'
import { knowledgeOperations } from '@/lib/knowledge/application/operations'
import {
listPersonalSourceSetupAccounts,
personalSourceSetup,
} from '@/lib/knowledge/application/personal-source-setup'
import {
SelectorConnectionUnavailableError,
SelectorContextUnavailableError,
SelectorOptionsUnavailableError,
} from '@/lib/selectors/server/errors'
import { IntegrationNotAllowedError } from '@/ee/access-control/utils/permission-check'

const errorPolicy = extendInternalErrorPolicy(
internalKnowledgeErrorPolicies.connectAccount,
(error) => {
if (error instanceof SelectorConnectionUnavailableError)
return internalErrorResponse(error.status, {
error: 'Reconnect your account to choose projects or spaces',
})
if (error instanceof SelectorContextUnavailableError)
return internalErrorResponse(400, {
error: 'Enter your Atlassian site to choose projects or spaces',
})
if (error instanceof SelectorOptionsUnavailableError)
return internalErrorResponse(error.status, {
error:
'Could not load projects or spaces. Check the site and account access, then try again.',
})
if (error instanceof IntegrationNotAllowedError)
return internalErrorResponse(403, { error: error.message })
return null
}
)

export const GET = defineInternalJsonRoute({
contract: listPersonalSourceSetupAccountsContract,
auth: internalSessionAuth,
operation: knowledgeOperations.listPersonalSourceSetupAccounts,
rateLimit: internalRateLimits.user({ bucketName: 'knowledge.search.personal-setup.accounts' }),
errorPolicy,
mapInput: ({ query }) => query,
useCase: listPersonalSourceSetupAccounts,
present: (data) => ({ success: true as const, data }),
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
})

export const POST = defineInternalJsonRoute({
contract: personalSourceSetupContract,
auth: internalSessionAuth,
operation: knowledgeOperations.personalSourceSetup,
rateLimit: internalRateLimits.user({ bucketName: 'knowledge.search.personal-setup' }),
errorPolicy,
parseOptions: { maxBodyBytes: 384 * 1024 },
mapInput: ({ body }) => body,
useCase: personalSourceSetup,
present: (data) => ({ success: true as const, data }),
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
})
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export function ConnectAccountOptions({
</div>
{enrollment.setupConnector && (
<SourceSetupModal
organizationId={organization.id}
connector={enrollment.setupConnector}
isPending={enrollment.isPending}
onClose={enrollment.closeSetup}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,11 @@ describe('organization integrations role and source paths', () => {
}
)

it('keeps source setup fields after a failure without duplicating the toast inside the modal', async () => {
it('keeps manual source setup fields after a failure without duplicating the toast inside the modal', async () => {
const message = 'Connection unavailable'
mocks.enrollmentError = message
mocks.setupConnector = SEARCH_CONNECTORS.find((connector) => connector.type === 'jira') ?? null
mocks.setupConnector =
SEARCH_CONNECTORS.find((connector) => connector.type === 'github') ?? null
await render()
expect(document.querySelector('[role="dialog"]')).not.toBeNull()
expect(document.body.textContent).not.toContain(message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ function SearchIntegrationConnectionControl({
})
const name = connector?.meta.name ?? target.provider
const action = target.credentialId ? 'Reconnect' : 'Connect'
const label = connection.connected
const connected = connection.connected
const label = connected
? `Connected ${name}`
: connection.isLoading
? `Checking ${name} connections…`
Expand All @@ -76,18 +77,14 @@ function SearchIntegrationConnectionControl({
!connector ||
connection.isLoading ||
connection.isStarting ||
connection.connected ||
connected ||
(!connection.available && !connection.pending)
}
onClick={handleConnect}
leading={
connector && <BrandIcon icon={connector.serviceIcon} className='size-[16px] shrink-0' />
}
trailing={
connection.connected ? (
<Check className='size-[16px] text-[var(--text-icon)]' />
) : undefined
}
trailing={connected ? <Check className='size-[16px] text-[var(--text-icon)]' /> : undefined}
/>
{connection.pending && <Chip onClick={connection.cancel}>Cancel</Chip>}
{connection.error && (
Expand All @@ -100,6 +97,8 @@ function SearchIntegrationConnectionControl({
)}
{setupOpen && connector && (
<SourceSetupModal
organizationId={organizationId}
onConnected={connection.completeSetup}
connector={connector}
onClose={() => setSetupOpen(false)}
isPending={connection.isStarting}
Expand Down
Loading
Loading