Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 20 additions & 4 deletions components/Admin/AdminLineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type ChartConfig,
} from "@/components/ui/chart";

interface SecondLine {
interface ExtraLine {
data: Array<{ date: string; count: number }>;
label: string;
color?: string;
Expand All @@ -20,14 +20,16 @@ interface AdminLineChartProps {
title: string;
data: Array<{ date: string; count: number }>;
label?: string;
secondLine?: SecondLine;
secondLine?: ExtraLine;
thirdLine?: ExtraLine;
}

export default function AdminLineChart({
title,
data,
label = "Count",
secondLine,
thirdLine,
}: AdminLineChartProps) {
if (data.length === 0) return null;

Expand All @@ -36,14 +38,19 @@ export default function AdminLineChart({
...(secondLine
? { count2: { label: secondLine.label, color: secondLine.color ?? "#6B8E93" } }
: {}),
...(thirdLine
? { count3: { label: thirdLine.label, color: thirdLine.color ?? "#4A90A4" } }
: {}),
} satisfies ChartConfig;

// Merge primary and secondary data by date
// Merge primary, secondary, and tertiary data by date
const secondMap = new Map(secondLine?.data.map((d) => [d.date, d.count]) ?? []);
const thirdMap = new Map(thirdLine?.data.map((d) => [d.date, d.count]) ?? []);
const mergedData = data.map((d) => ({
date: d.date,
count: d.count,
...(secondLine ? { count2: secondMap.get(d.date) ?? 0 } : {}),
...(thirdLine ? { count3: thirdMap.get(d.date) ?? 0 } : {}),
}));

return (
Expand Down Expand Up @@ -77,7 +84,7 @@ export default function AdminLineChart({
/>
}
/>
{secondLine && <ChartLegend content={<ChartLegendContent />} />}
{(secondLine || thirdLine) && <ChartLegend content={<ChartLegendContent />} />}
<Line
dataKey="count"
type="monotone"
Expand All @@ -94,6 +101,15 @@ export default function AdminLineChart({
dot={{ fill: "var(--color-count2)", r: 4 }}
/>
)}
{thirdLine && (
<Line
dataKey="count3"
type="monotone"
stroke="var(--color-count3)"
strokeWidth={2}
dot={{ fill: "var(--color-count3)", r: 4 }}
/>
)}
</LineChart>
</ChartContainer>
</div>
Expand Down
33 changes: 27 additions & 6 deletions components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useState } from "react";
import PageBreadcrumb from "@/components/Sandboxes/PageBreadcrumb";
import ApiDocsLink from "@/components/ApiDocs/ApiDocsLink";
import { useSlackTags } from "@/hooks/useSlackTags";
import { useCodingPrStatus } from "@/hooks/useCodingPrStatus";
import SlackTagsTable from "./SlackTagsTable";
import AdminLineChart from "@/components/Admin/AdminLineChart";
import { getTagsByDate } from "@/lib/coding-agent/getTagsByDate";
Expand All @@ -15,6 +16,10 @@ import type { AdminPeriod } from "@/types/admin";
export default function CodingAgentSlackTagsPage() {
const [period, setPeriod] = useState<AdminPeriod>("all");
const { data, isLoading, error } = useSlackTags(period);
const { data: mergedPrUrls } = useCodingPrStatus(data?.tags);

const tagsByDate = data ? getTagsByDate(data.tags, mergedPrUrls) : [];
const totalMergedPrs = mergedPrUrls?.size ?? 0;

return (
<main className="mx-auto max-w-6xl px-4 py-10">
Expand Down Expand Up @@ -47,6 +52,16 @@ export default function CodingAgentSlackTagsPage() {
<span className="font-semibold text-gray-900 dark:text-gray-100">{data.total_pull_requests}</span>{" "}
total PRs
</span>
<span>
<span className="font-semibold text-gray-900 dark:text-gray-100">{totalMergedPrs}</span>{" "}
merged PRs
</span>
<span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{data.total > 0 ? Math.round((totalMergedPrs / data.total) * 100) : 0}%
</span>{" "}
conversion
</span>
</div>
)}
</div>
Expand Down Expand Up @@ -74,17 +89,23 @@ export default function CodingAgentSlackTagsPage() {
<>
<AdminLineChart
title="Tags & Pull Requests Over Time"
data={getTagsByDate(data.tags).map((d) => ({ date: d.date, count: d.count }))}
data={tagsByDate.map((d) => ({ date: d.date, count: d.count }))}
label="Tags"
secondLine={{
data: getTagsByDate(data.tags).map((d) => ({
date: d.date,
count: d.pull_request_count,
})),
data: tagsByDate.map((d) => ({ date: d.date, count: d.pull_request_count })),
label: "Tags with PRs",
}}
thirdLine={
mergedPrUrls
? {
data: tagsByDate.map((d) => ({ date: d.date, count: d.merged_pr_count })),
label: "PRs Merged",
color: "#22863a",
}
: undefined
}
/>
<SlackTagsTable tags={data.tags} />
<SlackTagsTable tags={data.tags} mergedPrUrls={mergedPrUrls} />
</>
)}
</main>
Expand Down
75 changes: 0 additions & 75 deletions components/CodingAgentSlackTags/SlackTagsColumns.tsx

This file was deleted.

11 changes: 7 additions & 4 deletions components/CodingAgentSlackTags/SlackTagsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,24 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { slackTagsColumns } from "./SlackTagsColumns";
import { createSlackTagsColumns } from "./createSlackTagsColumns";
import type { SlackTag } from "@/types/coding-agent";

interface SlackTagsTableProps {
tags: SlackTag[];
mergedPrUrls?: Set<string>;
}

export default function SlackTagsTable({ tags }: SlackTagsTableProps) {
export default function SlackTagsTable({ tags, mergedPrUrls }: SlackTagsTableProps) {
const [sorting, setSorting] = useState<SortingState>([
{ id: "timestamp", desc: true },
]);

const columns = createSlackTagsColumns(mergedPrUrls);

const table = useReactTable({
data: tags,
columns: slackTagsColumns,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
Expand Down Expand Up @@ -66,7 +69,7 @@ export default function SlackTagsTable({ tags }: SlackTagsTableProps) {
))
) : (
<TableRow>
<TableCell colSpan={slackTagsColumns.length} className="h-24 text-center">
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
Expand Down
81 changes: 81 additions & 0 deletions components/CodingAgentSlackTags/createSlackTagsColumns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { type ColumnDef } from "@tanstack/react-table";
import { SortableHeader } from "@/components/SandboxOrgs/SortableHeader";
import type { SlackTag } from "@/types/coding-agent";

export function createSlackTagsColumns(mergedPrUrls?: Set<string>): ColumnDef<SlackTag>[] {
return [
{
id: "user_name",
accessorKey: "user_name",
header: "Tagged By",
cell: ({ row }) => {
const tag = row.original;
return (
<div className="flex items-center gap-2">
{tag.user_avatar && (
<img

Check warning on line 16 in components/CodingAgentSlackTags/createSlackTagsColumns.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={tag.user_avatar}
alt={tag.user_name}
className="h-6 w-6 rounded-full"
/>
)}
<span className="font-medium">{tag.user_name}</span>
</div>
);
},
},
{
id: "prompt",
accessorKey: "prompt",
header: "Prompt",
cell: ({ getValue }) => (
<span className="max-w-md truncate block text-sm text-gray-700 dark:text-gray-300">
{getValue<string>()}
</span>
),
},
{
id: "channel_name",
accessorKey: "channel_name",
header: "Channel",
cell: ({ getValue }) => (
<span className="text-sm text-gray-500 dark:text-gray-400">#{getValue<string>()}</span>
),
},
{
id: "pull_requests",
accessorKey: "pull_requests",
header: "Pull Requests",
cell: ({ getValue }) => {
const prs = getValue<string[]>();
if (!prs?.length) return <span className="text-sm text-gray-400">—</span>;
return (
<div className="flex flex-col gap-1">
{prs.map((url) => {
const isMerged = mergedPrUrls?.has(url);
return (
<a
key={url}
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:underline dark:text-blue-400"
>
{isMerged ? "🚢 " : ""}
{url.match(/github\.com\/[^/]+\/([^/]+)\/pull\/(\d+)/)?.slice(1).join("#")}
</a>
);
})}
</div>
);
},
},
{
id: "timestamp",
accessorKey: "timestamp",
header: ({ column }) => <SortableHeader column={column} label="Timestamp" />,
cell: ({ getValue }) => new Date(getValue<string>()).toLocaleString(),
sortingFn: "basic",
},
];
}
27 changes: 27 additions & 0 deletions hooks/useCodingPrStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import { usePrivy } from "@privy-io/react-auth";
import { fetchCodingPrStatus } from "@/lib/recoup/fetchCodingPrStatus";
import type { SlackTag } from "@/types/coding-agent";

/**
* Fetches the merged status for all PR URLs found in the given Slack tags.
* Returns a Set of merged PR URLs for fast lookup.
*/
export function useCodingPrStatus(tags: SlackTag[] | undefined) {
const { ready, authenticated, getAccessToken } = usePrivy();

const allPrUrls = [...new Set(tags?.flatMap((tag) => tag.pull_requests ?? []) ?? [])];

return useQuery({
queryKey: ["admin", "coding-agent", "pr-status", allPrUrls],
queryFn: async () => {
const token = await getAccessToken();
if (!token) throw new Error("Not authenticated");
const res = await fetchCodingPrStatus(token, allPrUrls);
return new Set(res.pull_requests.filter((pr) => pr.status === "merged").map((pr) => pr.url));
},
enabled: ready && authenticated && allPrUrls.length > 0,
});
}
Loading
Loading