Skip to content

Commit c3f4361

Browse files
Add button for re-indexing repos
1 parent a6ac545 commit c3f4361

File tree

6 files changed

+138
-27
lines changed

6 files changed

+138
-27
lines changed

packages/backend/src/api.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { PrismaClient, RepoIndexingJobType } from '@sourcebot/db';
2+
import { createLogger } from '@sourcebot/shared';
13
import express, { Request, Response } from 'express';
24
import 'express-async-errors';
3-
import { PromClient } from './promClient.js';
4-
import { ConnectionManager } from './connectionManager.js';
5-
import z from 'zod';
6-
import { PrismaClient } from '@sourcebot/db';
7-
import { createLogger } from '@sourcebot/shared';
85
import * as http from "http";
6+
import z from 'zod';
7+
import { ConnectionManager } from './connectionManager.js';
8+
import { PromClient } from './promClient.js';
9+
import { RepoIndexManager } from './repoIndexManager.js';
910

1011
const logger = createLogger('api');
1112
const PORT = 3060;
@@ -17,6 +18,7 @@ export class Api {
1718
promClient: PromClient,
1819
private prisma: PrismaClient,
1920
private connectionManager: ConnectionManager,
21+
private repoIndexManager: RepoIndexManager,
2022
) {
2123
const app = express();
2224
app.use(express.json());
@@ -30,6 +32,7 @@ export class Api {
3032
});
3133

3234
app.post('/api/sync-connection', this.syncConnection.bind(this));
35+
app.post('/api/index-repo', this.indexRepo.bind(this));
3336

3437
this.server = app.listen(PORT, () => {
3538
logger.info(`API server is running on port ${PORT}`);
@@ -64,6 +67,30 @@ export class Api {
6467
res.status(200).json({ jobId });
6568
}
6669

70+
private async indexRepo(req: Request, res: Response) {
71+
const schema = z.object({
72+
repoId: z.number(),
73+
}).strict();
74+
75+
const parsed = schema.safeParse(req.body);
76+
if (!parsed.success) {
77+
res.status(400).json({ error: parsed.error.message });
78+
return;
79+
}
80+
81+
const { repoId } = parsed.data;
82+
const repo = await this.prisma.repo.findUnique({
83+
where: { id: repoId },
84+
});
85+
86+
if (!repo) {
87+
res.status(404).json({ error: 'Repo not found' });
88+
return;
89+
}
90+
91+
const [jobId] = await this.repoIndexManager.createJobs([repo], RepoIndexingJobType.INDEX);
92+
res.status(200).json({ jobId });
93+
}
6794

6895
public async dispose() {
6996
return new Promise<void>((resolve, reject) => {

packages/backend/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@ else if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement(
7474
accountPermissionSyncer.startScheduler();
7575
}
7676

77-
const api = new Api(promClient, prisma, connectionManager);
77+
const api = new Api(
78+
promClient,
79+
prisma,
80+
connectionManager,
81+
repoIndexManager,
82+
);
7883

7984
logger.info('Worker started.');
8085

packages/backend/src/repoIndexManager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export class RepoIndexManager {
192192
}
193193
}
194194

195-
private async createJobs(repos: Repo[], type: RepoIndexingJobType) {
195+
public async createJobs(repos: Repo[], type: RepoIndexingJobType) {
196196
// @note: we don't perform this in a transaction because
197197
// we want to avoid the situation where a job is created and run
198198
// prior to the transaction being committed.
@@ -221,6 +221,8 @@ export class RepoIndexManager {
221221
const jobTypeLabel = getJobTypePrometheusLabel(type);
222222
this.promClient.pendingRepoIndexJobs.inc({ repo: job.repo.name, type: jobTypeLabel });
223223
}
224+
225+
return jobs.map(job => job.id);
224226
}
225227

226228
private async runJob(job: ReservedJob<JobPayload>) {

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { sew } from "@/actions"
1+
import { getCurrentUserRole, sew } from "@/actions"
22
import { Badge } from "@/components/ui/badge"
33
import { Button } from "@/components/ui/button"
44
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
@@ -19,6 +19,7 @@ import { BackButton } from "../../components/backButton"
1919
import { DisplayDate } from "../../components/DisplayDate"
2020
import { RepoBranchesTable } from "../components/repoBranchesTable"
2121
import { RepoJobsTable } from "../components/repoJobsTable"
22+
import { OrgRole } from "@sourcebot/db"
2223

2324
export default async function RepoDetailPage({ params }: { params: Promise<{ id: string }> }) {
2425
const { id } = await params
@@ -51,6 +52,11 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
5152

5253
const repoMetadata = repoMetadataSchema.parse(repo.metadata);
5354

55+
const userRole = await getCurrentUserRole(SINGLE_TENANT_ORG_DOMAIN);
56+
if (isServiceError(userRole)) {
57+
throw new ServiceErrorException(userRole);
58+
}
59+
5460
return (
5561
<>
5662
<div className="mb-6">
@@ -172,7 +178,11 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
172178
</CardHeader>
173179
<CardContent>
174180
<Suspense fallback={<Skeleton className="h-96 w-full" />}>
175-
<RepoJobsTable data={repo.jobs} />
181+
<RepoJobsTable
182+
data={repo.jobs}
183+
repoId={repo.id}
184+
isIndexButtonVisible={userRole === OrgRole.OWNER}
185+
/>
176186
</Suspense>
177187
</CardContent>
178188
</Card>

packages/web/src/app/[domain]/repos/components/repoJobsTable.tsx

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ import {
1818
useReactTable,
1919
} from "@tanstack/react-table"
2020
import { cva } from "class-variance-authority"
21-
import { AlertCircle, ArrowUpDown, RefreshCwIcon } from "lucide-react"
21+
import { AlertCircle, ArrowUpDown, PlusCircleIcon, RefreshCwIcon } from "lucide-react"
2222
import * as React from "react"
2323
import { CopyIconButton } from "../../components/copyIconButton"
2424
import { useMemo } from "react"
2525
import { LightweightCodeHighlighter } from "../../components/lightweightCodeHighlighter"
2626
import { useRouter } from "next/navigation"
2727
import { useToast } from "@/components/hooks/use-toast"
2828
import { DisplayDate } from "../../components/DisplayDate"
29+
import { LoadingButton } from "@/components/ui/loading-button"
30+
import { indexRepo } from "@/features/workerApi/actions"
31+
import { isServiceError } from "@/lib/utils"
2932

3033
// @see: https://v0.app/chat/repo-indexing-status-uhjdDim8OUS
3134

@@ -129,7 +132,7 @@ export const columns: ColumnDef<RepoIndexingJob>[] = [
129132
</Button>
130133
)
131134
},
132-
cell: ({ row }) => <DisplayDate date={row.getValue("createdAt") as Date} className="ml-3"/>,
135+
cell: ({ row }) => <DisplayDate date={row.getValue("createdAt") as Date} className="ml-3" />,
133136
},
134137
{
135138
accessorKey: "completedAt",
@@ -147,7 +150,7 @@ export const columns: ColumnDef<RepoIndexingJob>[] = [
147150
return "-";
148151
}
149152

150-
return <DisplayDate date={completedAt} className="ml-3"/>
153+
return <DisplayDate date={completedAt} className="ml-3" />
151154
},
152155
},
153156
{
@@ -176,13 +179,41 @@ export const columns: ColumnDef<RepoIndexingJob>[] = [
176179
},
177180
]
178181

179-
export const RepoJobsTable = ({ data }: { data: RepoIndexingJob[] }) => {
182+
export const RepoJobsTable = ({
183+
data,
184+
repoId,
185+
isIndexButtonVisible,
186+
}: {
187+
data: RepoIndexingJob[],
188+
repoId: number,
189+
isIndexButtonVisible: boolean,
190+
}) => {
180191
const [sorting, setSorting] = React.useState<SortingState>([{ id: "createdAt", desc: true }])
181192
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
182193
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
183194
const router = useRouter();
184195
const { toast } = useToast();
185196

197+
const [isIndexSubmitting, setIsIndexSubmitting] = React.useState(false);
198+
const onIndexButtonClick = React.useCallback(async () => {
199+
setIsIndexSubmitting(true);
200+
const response = await indexRepo(repoId);
201+
202+
if (!isServiceError(response)) {
203+
const { jobId } = response;
204+
toast({
205+
description: `✅ Repository indexed successfully. Job ID: ${jobId}`,
206+
})
207+
router.refresh();
208+
} else {
209+
toast({
210+
description: `❌ Failed to index repository. ${response.message}`,
211+
});
212+
}
213+
214+
setIsIndexSubmitting(false);
215+
}, [repoId, router, toast]);
216+
186217
const table = useReactTable({
187218
data,
188219
columns,
@@ -247,19 +278,31 @@ export const RepoJobsTable = ({ data }: { data: RepoIndexingJob[] }) => {
247278
</SelectContent>
248279
</Select>
249280

250-
<Button
251-
variant="outline"
252-
className="ml-auto"
253-
onClick={() => {
254-
router.refresh();
255-
toast({
256-
description: "Page refreshed",
257-
});
258-
}}
259-
>
260-
<RefreshCwIcon className="w-3 h-3" />
261-
Refresh
262-
</Button>
281+
<div className="ml-auto flex items-center gap-2">
282+
<Button
283+
variant="outline"
284+
onClick={() => {
285+
router.refresh();
286+
toast({
287+
description: "Page refreshed",
288+
});
289+
}}
290+
>
291+
<RefreshCwIcon className="w-3 h-3" />
292+
Refresh
293+
</Button>
294+
295+
{isIndexButtonVisible && (
296+
<LoadingButton
297+
onClick={onIndexButtonClick}
298+
loading={isIndexSubmitting}
299+
variant="outline"
300+
>
301+
<PlusCircleIcon className="w-3 h-3" />
302+
Index now
303+
</LoadingButton>
304+
)}
305+
</div>
263306
</div>
264307

265308
<div className="rounded-md border">

packages/web/src/features/workerApi/actions.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,28 @@ export const syncConnection = async (connectionId: number) => sew(() =>
3232
return schema.parse(data);
3333
})
3434
)
35-
);
35+
);
36+
37+
export const indexRepo = async (repoId: number) => sew(() =>
38+
withAuthV2(({ role }) =>
39+
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
40+
const response = await fetch(`${WORKER_API_URL}/api/index-repo`, {
41+
method: 'POST',
42+
body: JSON.stringify({ repoId }),
43+
headers: {
44+
'Content-Type': 'application/json',
45+
},
46+
});
47+
48+
if (!response.ok) {
49+
return unexpectedError('Failed to index repo');
50+
}
51+
52+
const data = await response.json();
53+
const schema = z.object({
54+
jobId: z.string(),
55+
});
56+
return schema.parse(data);
57+
})
58+
)
59+
);

0 commit comments

Comments
 (0)