From e14ee3d60079281861ffadedfb075b8aedfc1dfb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 04:01:33 +0000 Subject: [PATCH 1/3] feat(web): add Syncing filter to Repositories table Add a new Syncing filter option to the Repositories table status dropdown. This allows users to filter repositories that are currently being synced. Changes: - Add getSyncingJobIds method to BullMQClient to fetch pending/active jobs - Add syncing option to StatusFilter type and status dropdown UI - Implement syncing filter logic to match repos with active jobs or no indexedAt - Add empty state message for syncing filter - Add tests for the new functionality Co-authored-by: Michael Sukkarieh --- packages/shared/src/bullmqClient.test.ts | 18 ++++++++ packages/shared/src/bullmqClient.ts | 13 ++++++ .../repos/components/reposTable.test.tsx | 24 ++++++++++- .../app/(app)/repos/components/reposTable.tsx | 17 ++++---- packages/web/src/app/(app)/repos/page.tsx | 42 ++++++++++++++----- 5 files changed, 96 insertions(+), 18 deletions(-) diff --git a/packages/shared/src/bullmqClient.test.ts b/packages/shared/src/bullmqClient.test.ts index f373cf5b5..5b8ed0888 100644 --- a/packages/shared/src/bullmqClient.test.ts +++ b/packages/shared/src/bullmqClient.test.ts @@ -178,6 +178,24 @@ describe("BullMQClient", () => { ); }); + test("lists syncing job ids", async () => { + mocks.listJobs.mockResolvedValue([ + { id: "syncing-1" }, + { id: "syncing-2" }, + ]); + const client = new BullMQClient({} as Redis); + + await expect( + client.getSyncingJobIds(CONNECTION_QUEUE), + ).resolves.toEqual(["syncing-1", "syncing-2"]); + expect(mocks.listJobs).toHaveBeenCalledWith( + ["waiting", "waiting-children", "delayed", "prioritized", "paused", "active"], + 0, + -1, + true, + ); + }); + test("includes workload data in scheduled jobs", async () => { const client = new BullMQClient({} as Redis); const data = { connectionId: 42 }; diff --git a/packages/shared/src/bullmqClient.ts b/packages/shared/src/bullmqClient.ts index 19178f752..c0815459f 100644 --- a/packages/shared/src/bullmqClient.ts +++ b/packages/shared/src/bullmqClient.ts @@ -137,6 +137,19 @@ export class BullMQClient { return jobs.flatMap((job) => job.id ? [job.id] : []); } + async getSyncingJobIds( + spec: QueueSpec, + ): Promise { + const jobs = await this.getQueue(spec).getJobs( + ["waiting", "waiting-children", "delayed", "prioritized", "paused", "active"], + 0, + -1, + true, + ); + + return jobs.flatMap((job) => job.id ? [job.id] : []); + } + async getJobLogs( spec: QueueSpec, jobId: string, diff --git a/packages/web/src/app/(app)/repos/components/reposTable.test.tsx b/packages/web/src/app/(app)/repos/components/reposTable.test.tsx index 050ec36e7..8793dfd9d 100644 --- a/packages/web/src/app/(app)/repos/components/reposTable.test.tsx +++ b/packages/web/src/app/(app)/repos/components/reposTable.test.tsx @@ -138,6 +138,20 @@ describe("ReposTable", () => { ).toContain("Failed"); }); + test("reflects the syncing status filter from the URL", () => { + navigation.searchParams = "status=syncing"; + + renderTable([repos[0]]); + + expect( + screen + .getByRole("combobox", { + name: "Filter repositories by status", + }) + .textContent, + ).toContain("Syncing"); + }); + test("centers the empty state across the table and hides pagination", () => { navigation.searchParams = "status=warning"; @@ -150,6 +164,14 @@ describe("ReposTable", () => { expect(screen.queryByRole("button", { name: "Next" })).toBeNull(); }); + test("shows empty state message for syncing filter", () => { + navigation.searchParams = "status=syncing"; + + renderTable([]); + + expect(screen.getByText("No repositories are currently syncing.")).toBeTruthy(); + }); + test("clears search and status filters from the empty state", () => { navigation.searchParams = "search=missing&status=failed&page=2&sortBy=indexedAt"; @@ -167,7 +189,7 @@ describe("ReposTable", () => { ); }); - test.each(["search=first", "status=warning"])( + test.each(["search=first", "status=syncing", "status=warning"])( "shows clear filters in the toolbar for %s", (searchParams) => { navigation.searchParams = searchParams; diff --git a/packages/web/src/app/(app)/repos/components/reposTable.tsx b/packages/web/src/app/(app)/repos/components/reposTable.tsx index 3a8e667df..0ece77778 100644 --- a/packages/web/src/app/(app)/repos/components/reposTable.tsx +++ b/packages/web/src/app/(app)/repos/components/reposTable.tsx @@ -75,11 +75,11 @@ type DisplayedRepo = Repo & { type SortOrder = "asc" | "desc"; type SortBy = "name" | "indexedAt"; -type StatusFilter = "all" | "failed" | "warning"; +type StatusFilter = "all" | "syncing" | "failed" | "warning"; type SyncAnnotation = "SYNCING" | "WARNING" | "FAILED" | null; const getStatusFilter = (value: string | null): StatusFilter => { - if (value === "failed" || value === "warning") { + if (value === "syncing" || value === "failed" || value === "warning") { return value; } @@ -873,11 +873,13 @@ export const ReposTable = ({ }); }; - const emptyMessage = statusFilter === "failed" - ? "No failed repositories." - : statusFilter === "warning" - ? "No repositories with warnings." - : "No repositories found."; + const emptyMessage = statusFilter === "syncing" + ? "No repositories are currently syncing." + : statusFilter === "failed" + ? "No failed repositories." + : statusFilter === "warning" + ? "No repositories with warnings." + : "No repositories found."; return (
@@ -914,6 +916,7 @@ export const ReposTable = ({ Filter by status + Syncing Failed Warning diff --git a/packages/web/src/app/(app)/repos/page.tsx b/packages/web/src/app/(app)/repos/page.tsx index 4b23e012e..1bd21ef62 100644 --- a/packages/web/src/app/(app)/repos/page.tsx +++ b/packages/web/src/app/(app)/repos/page.tsx @@ -17,7 +17,7 @@ const DEFAULT_PAGE_SIZE = 20; const pageSchema = z.coerce.number().int().positive(); const sortBySchema = z.enum(["name", "indexedAt"]); const sortOrderSchema = z.enum(["asc", "desc"]); -const statusSchema = z.enum(["failed", "warning"]); +const statusSchema = z.enum(["syncing", "failed", "warning"]); type ReposPageProps = { searchParams: Promise<{ @@ -44,9 +44,13 @@ export default authenticatedPage< const orderBy = sortBy === "indexedAt" ? [{ indexedAt: sortOrder }, { id: "asc" as const }] : [{ displayName: sortOrder }, { id: "asc" as const }]; - const failedJobIds = status === "all" - ? [] - : await getBullMQClient().getFailedJobIds(REPO_INDEX_QUEUE); + const bullMQClient = getBullMQClient(); + const syncingJobIds = status === "syncing" + ? await bullMQClient.getSyncingJobIds(REPO_INDEX_QUEUE) + : []; + const failedJobIds = status === "failed" || status === "warning" + ? await bullMQClient.getFailedJobIds(REPO_INDEX_QUEUE) + : []; const repositorySyncCounts = canRetry ? await getRepositorySyncCounts() : null; @@ -54,6 +58,29 @@ export default authenticatedPage< && !isServiceError(repositorySyncCounts) ? repositorySyncCounts.failedCount + repositorySyncCounts.warningCount : 0; + const getStatusWhereClause = (): Prisma.RepoWhereInput => { + switch (status) { + case "syncing": + return { + OR: [ + { latestIndexingJobId: { in: syncingJobIds } }, + { indexedAt: null }, + ], + }; + case "failed": + return { + latestIndexingJobId: { in: failedJobIds }, + indexedAt: null, + }; + case "warning": + return { + latestIndexingJobId: { in: failedJobIds }, + indexedAt: { not: null }, + }; + default: + return {}; + } + }; const where: Prisma.RepoWhereInput = { orgId: org.id, ...(search @@ -64,12 +91,7 @@ export default authenticatedPage< }, } : {}), - ...(status === "all" - ? {} - : { - latestIndexingJobId: { in: failedJobIds }, - indexedAt: status === "failed" ? null : { not: null }, - }), + ...getStatusWhereClause(), }; const [repos, totalCount] = await Promise.all([ From 03f70107a0e606b7cd497bf3a0e9ecd80731cf10 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 04:02:14 +0000 Subject: [PATCH 2/3] docs: add CHANGELOG entry for Syncing filter Co-authored-by: Michael Sukkarieh --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bc271528..1beec47cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added privacy-scoped setup wizard funnel and Docker startup-failure telemetry with deployment identity handoff, Node.js 20.20.0 support, and cross-platform end-to-end test coverage. [#1653](https://github.com/sourcebot-dev/sourcebot/pull/1653) +- Added Syncing filter option to the Repositories table status dropdown. [#1657](https://github.com/sourcebot-dev/sourcebot/pull/1657) ## [5.1.13] - 2026-09-12 From d3f819fa655a3998b1e8f421a31d8e914fe795f0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 16 Sep 2026 04:03:52 +0000 Subject: [PATCH 3/3] fix(web): exclude scheduled jobs and failed-first-index repos from Syncing filter Addresses review comments: 1. Syncing filter no longer overlaps with Failed filter: - Changed indexedAt:null leg to require firstIndexingJobFinishedAt:null - Repos whose first index failed now only appear in Failed, not Syncing 2. Pending reindex jobs for already-indexed repos are now visible: - Added getSyncingRepoIds() to extract repoId from pending jobs - Syncing filter now matches by repo ID in addition to job ID 3. Removed 'delayed' and 'paused' job states from syncing: - These states include scheduled future reindex jobs, not actual syncs - Only 'waiting', 'waiting-children', 'prioritized', 'active' are syncing Co-authored-by: Michael Sukkarieh --- packages/shared/src/bullmqClient.test.ts | 23 +++++++++++++++++++++-- packages/shared/src/bullmqClient.ts | 18 +++++++++++++++++- packages/web/src/app/(app)/repos/page.tsx | 15 +++++++++++---- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/bullmqClient.test.ts b/packages/shared/src/bullmqClient.test.ts index 5b8ed0888..370be4704 100644 --- a/packages/shared/src/bullmqClient.test.ts +++ b/packages/shared/src/bullmqClient.test.ts @@ -32,7 +32,7 @@ vi.mock("./jobLogger.js", () => ({ })); import { BullMQClient } from "./bullmqClient.js"; -import { CONNECTION_QUEUE, type QueueSpec } from "./queue.js"; +import { CONNECTION_QUEUE, REPO_INDEX_QUEUE, type QueueSpec } from "./queue.js"; describe("BullMQClient", () => { beforeEach(() => { @@ -189,7 +189,26 @@ describe("BullMQClient", () => { client.getSyncingJobIds(CONNECTION_QUEUE), ).resolves.toEqual(["syncing-1", "syncing-2"]); expect(mocks.listJobs).toHaveBeenCalledWith( - ["waiting", "waiting-children", "delayed", "prioritized", "paused", "active"], + ["waiting", "waiting-children", "prioritized", "active"], + 0, + -1, + true, + ); + }); + + test("lists syncing repo ids from job data", async () => { + mocks.listJobs.mockResolvedValue([ + { id: "job-1", data: { repoId: 1 } }, + { id: "job-2", data: { repoId: 2 } }, + { id: "job-3", data: {} }, + ]); + const client = new BullMQClient({} as Redis); + + await expect( + client.getSyncingRepoIds(REPO_INDEX_QUEUE), + ).resolves.toEqual([1, 2]); + expect(mocks.listJobs).toHaveBeenCalledWith( + ["waiting", "waiting-children", "prioritized", "active"], 0, -1, true, diff --git a/packages/shared/src/bullmqClient.ts b/packages/shared/src/bullmqClient.ts index c0815459f..0f7e1e063 100644 --- a/packages/shared/src/bullmqClient.ts +++ b/packages/shared/src/bullmqClient.ts @@ -141,7 +141,7 @@ export class BullMQClient { spec: QueueSpec, ): Promise { const jobs = await this.getQueue(spec).getJobs( - ["waiting", "waiting-children", "delayed", "prioritized", "paused", "active"], + ["waiting", "waiting-children", "prioritized", "active"], 0, -1, true, @@ -150,6 +150,22 @@ export class BullMQClient { return jobs.flatMap((job) => job.id ? [job.id] : []); } + async getSyncingRepoIds( + spec: QueueSpec<"repo-index">, + ): Promise { + const jobs = await this.getQueue(spec).getJobs( + ["waiting", "waiting-children", "prioritized", "active"], + 0, + -1, + true, + ); + + return jobs.flatMap((job) => { + const repoId = (job.data as { repoId?: number })?.repoId; + return typeof repoId === "number" ? [repoId] : []; + }); + } + async getJobLogs( spec: QueueSpec, jobId: string, diff --git a/packages/web/src/app/(app)/repos/page.tsx b/packages/web/src/app/(app)/repos/page.tsx index 1bd21ef62..ca763b6bd 100644 --- a/packages/web/src/app/(app)/repos/page.tsx +++ b/packages/web/src/app/(app)/repos/page.tsx @@ -45,9 +45,12 @@ export default authenticatedPage< ? [{ indexedAt: sortOrder }, { id: "asc" as const }] : [{ displayName: sortOrder }, { id: "asc" as const }]; const bullMQClient = getBullMQClient(); - const syncingJobIds = status === "syncing" - ? await bullMQClient.getSyncingJobIds(REPO_INDEX_QUEUE) - : []; + const [syncingJobIds, syncingRepoIds] = status === "syncing" + ? await Promise.all([ + bullMQClient.getSyncingJobIds(REPO_INDEX_QUEUE), + bullMQClient.getSyncingRepoIds(REPO_INDEX_QUEUE), + ]) + : [[], []]; const failedJobIds = status === "failed" || status === "warning" ? await bullMQClient.getFailedJobIds(REPO_INDEX_QUEUE) : []; @@ -64,7 +67,11 @@ export default authenticatedPage< return { OR: [ { latestIndexingJobId: { in: syncingJobIds } }, - { indexedAt: null }, + { id: { in: syncingRepoIds } }, + { + indexedAt: null, + firstIndexingJobFinishedAt: null, + }, ], }; case "failed":