Skip to content

Commit c9e864d

Browse files
fix(web): Fix carousel perf issue + improvements to withAuth middleware (#507)
1 parent 660623a commit c9e864d

File tree

27 files changed

+1112
-164
lines changed

27 files changed

+1112
-164
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111
- Fixed Bitbucket Cloud pagination not working beyond first page. [#295](https://github.com/sourcebot-dev/sourcebot/issues/295)
1212
- Fixed search bar line wrapping. [#501](https://github.com/sourcebot-dev/sourcebot/pull/501)
13+
- Fixed carousel perf issues. [#507](https://github.com/sourcebot-dev/sourcebot/pull/507)
1314

1415
## [4.6.7] - 2025-09-08
1516

packages/mcp/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,10 @@ server.tool(
161161
};
162162
}
163163

164-
const content: TextContent[] = response.repos.map(repo => {
164+
const content: TextContent[] = response.map(repo => {
165165
return {
166166
type: "text",
167-
text: `id: ${repo.name}\nurl: ${repo.webUrl}`,
167+
text: `id: ${repo.repoName}\nurl: ${repo.webUrl}`,
168168
}
169169
});
170170

packages/mcp/src/schemas.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,34 @@ export const searchResponseSchema = z.object({
9292
isBranchFilteringEnabled: z.boolean(),
9393
});
9494

95-
export const repositorySchema = z.object({
96-
name: z.string(),
97-
branches: z.array(z.string()),
95+
enum RepoIndexingStatus {
96+
NEW = 'NEW',
97+
IN_INDEX_QUEUE = 'IN_INDEX_QUEUE',
98+
INDEXING = 'INDEXING',
99+
INDEXED = 'INDEXED',
100+
FAILED = 'FAILED',
101+
IN_GC_QUEUE = 'IN_GC_QUEUE',
102+
GARBAGE_COLLECTING = 'GARBAGE_COLLECTING',
103+
GARBAGE_COLLECTION_FAILED = 'GARBAGE_COLLECTION_FAILED'
104+
}
105+
106+
export const repositoryQuerySchema = z.object({
107+
codeHostType: z.string(),
108+
repoId: z.number(),
109+
repoName: z.string(),
110+
repoDisplayName: z.string().optional(),
111+
repoCloneUrl: z.string(),
98112
webUrl: z.string().optional(),
99-
rawConfig: z.record(z.string(), z.string()).optional(),
113+
linkedConnections: z.array(z.object({
114+
id: z.number(),
115+
name: z.string(),
116+
})),
117+
imageUrl: z.string().optional(),
118+
indexedAt: z.coerce.date().optional(),
119+
repoIndexingStatus: z.nativeEnum(RepoIndexingStatus),
100120
});
101121

102-
export const listRepositoriesResponseSchema = z.object({
103-
repos: z.array(repositorySchema),
104-
});
122+
export const listRepositoriesResponseSchema = repositoryQuerySchema.array();
105123

106124
export const fileSourceRequestSchema = z.object({
107125
fileName: z.string(),

packages/mcp/src/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export type SearchResultChunk = SearchResultFile["chunks"][number];
2222
export type SearchSymbol = z.infer<typeof symbolSchema>;
2323

2424
export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>;
25-
export type Repository = ListRepositoriesResponse["repos"][number];
2625

2726
export type FileSourceRequest = z.infer<typeof fileSourceRequestSchema>;
2827
export type FileSourceResponse = z.infer<typeof fileSourceResponseSchema>;

packages/web/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"build": "cross-env SKIP_ENV_VALIDATION=1 next build",
88
"start": "next start",
99
"lint": "cross-env SKIP_ENV_VALIDATION=1 eslint .",
10-
"test": "vitest",
10+
"test": "cross-env SKIP_ENV_VALIDATION=1 vitest",
1111
"dev:emails": "email dev --dir ./src/emails",
1212
"stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe"
1313
},
@@ -212,7 +212,8 @@
212212
"tsx": "^4.19.2",
213213
"typescript": "^5",
214214
"vite-tsconfig-paths": "^5.1.3",
215-
"vitest": "^2.1.5"
215+
"vitest": "^2.1.5",
216+
"vitest-mock-extended": "^3.1.0"
216217
},
217218
"resolutions": {
218219
"@types/react": "19.1.10",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_NAME } from '@/lib/constants';
2+
import { ApiKey, Org, PrismaClient, User } from '@prisma/client';
3+
import { beforeEach } from 'vitest';
4+
import { mockDeep, mockReset } from 'vitest-mock-extended';
5+
6+
beforeEach(() => {
7+
mockReset(prisma);
8+
});
9+
10+
export const prisma = mockDeep<PrismaClient>();
11+
12+
export const MOCK_ORG: Org = {
13+
id: SINGLE_TENANT_ORG_ID,
14+
name: SINGLE_TENANT_ORG_NAME,
15+
domain: SINGLE_TENANT_ORG_DOMAIN,
16+
createdAt: new Date(),
17+
updatedAt: new Date(),
18+
isOnboarded: true,
19+
imageUrl: null,
20+
metadata: null,
21+
memberApprovalRequired: false,
22+
stripeCustomerId: null,
23+
stripeSubscriptionStatus: null,
24+
stripeLastUpdatedAt: null,
25+
inviteLinkEnabled: false,
26+
inviteLinkId: null
27+
}
28+
29+
export const MOCK_API_KEY: ApiKey = {
30+
name: 'Test API Key',
31+
hash: 'apikey',
32+
createdAt: new Date(),
33+
lastUsedAt: new Date(),
34+
orgId: 1,
35+
createdById: '1',
36+
}
37+
38+
export const MOCK_USER: User = {
39+
id: '1',
40+
name: 'Test User',
41+
42+
createdAt: new Date(),
43+
updatedAt: new Date(),
44+
hashedPassword: null,
45+
emailVerified: null,
46+
image: null
47+
}
48+

packages/web/src/actions.ts

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { getAuditService } from "@/ee/features/audit/factory";
4040
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
4141
import { getOrgMetadata } from "@/lib/utils";
4242
import { getOrgFromDomain } from "./data/org";
43+
import { withOptionalAuthV2 } from "./withAuthV2";
4344

4445
const ajv = new Ajv({
4546
validateFormats: false,
@@ -637,49 +638,47 @@ export const getConnectionInfo = async (connectionId: number, domain: string) =>
637638
}
638639
})));
639640

640-
export const getRepos = async (domain: string, filter: { status?: RepoIndexingStatus[], connectionId?: number } = {}) => sew(() =>
641-
withAuth((userId) =>
642-
withOrgMembership(userId, domain, async ({ org }) => {
643-
const repos = await prisma.repo.findMany({
644-
where: {
645-
orgId: org.id,
646-
...(filter.status ? {
647-
repoIndexingStatus: { in: filter.status }
648-
} : {}),
649-
...(filter.connectionId ? {
650-
connections: {
651-
some: {
652-
connectionId: filter.connectionId
653-
}
654-
}
655-
} : {}),
656-
},
657-
include: {
641+
export const getRepos = async (filter: { status?: RepoIndexingStatus[], connectionId?: number } = {}) => sew(() =>
642+
withOptionalAuthV2(async ({ org }) => {
643+
const repos = await prisma.repo.findMany({
644+
where: {
645+
orgId: org.id,
646+
...(filter.status ? {
647+
repoIndexingStatus: { in: filter.status }
648+
} : {}),
649+
...(filter.connectionId ? {
658650
connections: {
659-
include: {
660-
connection: true,
651+
some: {
652+
connectionId: filter.connectionId
661653
}
662654
}
655+
} : {}),
656+
},
657+
include: {
658+
connections: {
659+
include: {
660+
connection: true,
661+
}
663662
}
664-
});
663+
}
664+
});
665665

666-
return repos.map((repo) => repositoryQuerySchema.parse({
667-
codeHostType: repo.external_codeHostType,
668-
repoId: repo.id,
669-
repoName: repo.name,
670-
repoDisplayName: repo.displayName ?? undefined,
671-
repoCloneUrl: repo.cloneUrl,
672-
webUrl: repo.webUrl ?? undefined,
673-
linkedConnections: repo.connections.map(({ connection }) => ({
674-
id: connection.id,
675-
name: connection.name,
676-
})),
677-
imageUrl: repo.imageUrl ?? undefined,
678-
indexedAt: repo.indexedAt ?? undefined,
679-
repoIndexingStatus: repo.repoIndexingStatus,
680-
}));
681-
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true
682-
));
666+
return repos.map((repo) => repositoryQuerySchema.parse({
667+
codeHostType: repo.external_codeHostType,
668+
repoId: repo.id,
669+
repoName: repo.name,
670+
repoDisplayName: repo.displayName ?? undefined,
671+
repoCloneUrl: repo.cloneUrl,
672+
webUrl: repo.webUrl ?? undefined,
673+
linkedConnections: repo.connections.map(({ connection }) => ({
674+
id: connection.id,
675+
name: connection.name,
676+
})),
677+
imageUrl: repo.imageUrl ?? undefined,
678+
indexedAt: repo.indexedAt ?? undefined,
679+
repoIndexingStatus: repo.repoIndexingStatus,
680+
}))
681+
}));
683682

684683
export const getRepoInfoByName = async (repoName: string, domain: string) => sew(() =>
685684
withAuth((userId) =>

packages/web/src/app/[domain]/chat/[id]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ interface PageProps {
2222
export default async function Page(props: PageProps) {
2323
const params = await props.params;
2424
const languageModels = await getConfiguredLanguageModelsInfo();
25-
const repos = await getRepos(params.domain);
25+
const repos = await getRepos();
2626
const searchContexts = await getSearchContexts(params.domain);
2727
const chatInfo = await getChatInfo({ chatId: params.id }, params.domain);
2828
const session = await auth();

packages/web/src/app/[domain]/chat/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface PageProps {
1818
export default async function Page(props: PageProps) {
1919
const params = await props.params;
2020
const languageModels = await getConfiguredLanguageModelsInfo();
21-
const repos = await getRepos(params.domain);
21+
const repos = await getRepos();
2222
const searchContexts = await getSearchContexts(params.domain);
2323
const session = await auth();
2424
const chatHistory = session ? await getUserChatHistory(params.domain) : [];

packages/web/src/app/[domain]/components/errorNavIndicator.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ import { env } from "@/env.mjs";
1010
import { useQuery } from "@tanstack/react-query";
1111
import { ConnectionSyncStatus, RepoIndexingStatus } from "@sourcebot/db";
1212
import { getConnections } from "@/actions";
13-
import { getRepos } from "@/actions";
1413
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
14+
import { getRepos } from "@/app/api/(client)/client";
1515

1616
export const ErrorNavIndicator = () => {
1717
const domain = useDomain();
1818
const captureEvent = useCaptureEvent();
1919

2020
const { data: repos, isPending: isPendingRepos, isError: isErrorRepos } = useQuery({
2121
queryKey: ['repos', domain],
22-
queryFn: () => unwrapServiceError(getRepos(domain)),
22+
queryFn: () => unwrapServiceError(getRepos()),
2323
select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.FAILED),
2424
refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS,
2525
});

0 commit comments

Comments
 (0)