|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { useMemo, useState } from "react"; |
| 3 | +import React, { useMemo, useState } from "react"; |
4 | 4 | import { |
5 | 5 | AreaChart, |
6 | 6 | Area, |
@@ -71,6 +71,8 @@ interface VersionEntry { |
71 | 71 | percentage: number; |
72 | 72 | } |
73 | 73 |
|
| 74 | +type VersionUsers = Record<string, Array<{ email: string; name: string }>>; |
| 75 | + |
74 | 76 | interface ModelEfficiencyEntry { |
75 | 77 | model: string; |
76 | 78 | users: number; |
@@ -113,6 +115,7 @@ interface InsightsData { |
113 | 115 | commands: CommandsEntry[]; |
114 | 116 | fileExtensions: FileExtEntry[]; |
115 | 117 | clientVersions: VersionEntry[]; |
| 118 | + versionUsers: VersionUsers; |
116 | 119 | modelEfficiency: ModelEfficiencyEntry[]; |
117 | 120 | planExhaustion: PlanExhaustionData; |
118 | 121 | } |
@@ -484,65 +487,134 @@ export function InsightsClient({ data }: { data: InsightsData }) { |
484 | 487 | </div> |
485 | 488 |
|
486 | 489 | {/* Client Versions - compact */} |
487 | | - <ChartCard title="Client Versions (Latest Day)"> |
488 | | - <div className="flex gap-4"> |
489 | | - <ResponsiveContainer width={160} height={160}> |
490 | | - <PieChart> |
491 | | - <Pie |
492 | | - data={data.clientVersions.slice(0, 6)} |
493 | | - dataKey="user_count" |
494 | | - nameKey="version" |
495 | | - cx="50%" |
496 | | - cy="50%" |
497 | | - outerRadius={55} |
498 | | - innerRadius={25} |
499 | | - paddingAngle={2} |
500 | | - > |
501 | | - {data.clientVersions.slice(0, 6).map((_, i) => ( |
502 | | - <Cell key={i} fill={COLORS[i % COLORS.length]} /> |
503 | | - ))} |
504 | | - </Pie> |
505 | | - <Tooltip |
506 | | - contentStyle={{ |
507 | | - backgroundColor: "#18181b", |
508 | | - border: "1px solid #3f3f46", |
509 | | - borderRadius: "6px", |
510 | | - fontSize: "11px", |
511 | | - }} |
512 | | - formatter={ |
513 | | - ((v: number, _: string, entry: { payload: VersionEntry }) => [ |
514 | | - `${v ?? 0} users (${entry.payload.percentage.toFixed(0)}%)`, |
515 | | - entry.payload.version, |
516 | | - ]) as never |
517 | | - } |
518 | | - /> |
519 | | - </PieChart> |
520 | | - </ResponsiveContainer> |
521 | | - <div className="flex-1 overflow-y-auto max-h-[160px]"> |
522 | | - <table className="w-full text-xs"> |
523 | | - <tbody> |
524 | | - {data.clientVersions.map((v, i) => ( |
525 | | - <tr key={v.version} className="border-b border-zinc-800/30"> |
526 | | - <td className="py-1"> |
527 | | - <span |
528 | | - className="inline-block w-2 h-2 rounded-full mr-1.5" |
529 | | - style={{ backgroundColor: COLORS[i % COLORS.length] }} |
530 | | - /> |
531 | | - <span className="font-mono text-zinc-300">{v.version}</span> |
532 | | - </td> |
533 | | - <td className="text-right py-1 text-zinc-400">{v.user_count}</td> |
534 | | - <td className="text-right py-1 text-zinc-500">{v.percentage.toFixed(0)}%</td> |
535 | | - </tr> |
536 | | - ))} |
537 | | - </tbody> |
538 | | - </table> |
539 | | - </div> |
540 | | - </div> |
541 | | - </ChartCard> |
| 490 | + <ClientVersionsSection |
| 491 | + clientVersions={data.clientVersions} |
| 492 | + versionUsers={data.versionUsers} |
| 493 | + /> |
542 | 494 | </div> |
543 | 495 | ); |
544 | 496 | } |
545 | 497 |
|
| 498 | +function ClientVersionsSection({ |
| 499 | + versionUsers, |
| 500 | +}: { |
| 501 | + clientVersions: VersionEntry[]; |
| 502 | + versionUsers: VersionUsers; |
| 503 | +}) { |
| 504 | + const [expandedVersion, setExpandedVersion] = useState<string | null>(null); |
| 505 | + |
| 506 | + const versions = useMemo(() => { |
| 507 | + const totalUsers = Object.values(versionUsers).reduce((sum, u) => sum + u.length, 0); |
| 508 | + return Object.entries(versionUsers) |
| 509 | + .map(([version, users]) => ({ |
| 510 | + version, |
| 511 | + user_count: users.length, |
| 512 | + percentage: totalUsers > 0 ? (users.length / totalUsers) * 100 : 0, |
| 513 | + users, |
| 514 | + })) |
| 515 | + .sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true })); |
| 516 | + }, [versionUsers]); |
| 517 | + |
| 518 | + const latestVersion = versions[0]?.version; |
| 519 | + |
| 520 | + return ( |
| 521 | + <ChartCard title="Client Versions"> |
| 522 | + <div className="flex gap-4"> |
| 523 | + <ResponsiveContainer width={160} height={160}> |
| 524 | + <PieChart> |
| 525 | + <Pie |
| 526 | + data={versions.slice(0, 6)} |
| 527 | + dataKey="user_count" |
| 528 | + nameKey="version" |
| 529 | + cx="50%" |
| 530 | + cy="50%" |
| 531 | + outerRadius={55} |
| 532 | + innerRadius={25} |
| 533 | + paddingAngle={2} |
| 534 | + > |
| 535 | + {versions.slice(0, 6).map((_, i) => ( |
| 536 | + <Cell key={i} fill={COLORS[i % COLORS.length]} /> |
| 537 | + ))} |
| 538 | + </Pie> |
| 539 | + <Tooltip |
| 540 | + contentStyle={{ |
| 541 | + backgroundColor: "#18181b", |
| 542 | + border: "1px solid #3f3f46", |
| 543 | + borderRadius: "6px", |
| 544 | + fontSize: "11px", |
| 545 | + }} |
| 546 | + formatter={ |
| 547 | + ((v: number, _: string, entry: { payload: (typeof versions)[number] }) => [ |
| 548 | + `${v ?? 0} users (${entry.payload.percentage.toFixed(0)}%)`, |
| 549 | + entry.payload.version, |
| 550 | + ]) as never |
| 551 | + } |
| 552 | + /> |
| 553 | + </PieChart> |
| 554 | + </ResponsiveContainer> |
| 555 | + <div className="flex-1 overflow-y-auto max-h-[400px]"> |
| 556 | + <table className="w-full text-xs"> |
| 557 | + <tbody> |
| 558 | + {versions.map((v, i) => { |
| 559 | + const isExpanded = expandedVersion === v.version; |
| 560 | + const isLatest = v.version === latestVersion; |
| 561 | + return ( |
| 562 | + <React.Fragment key={v.version}> |
| 563 | + <tr |
| 564 | + className={`border-b border-zinc-800/30 cursor-pointer hover:bg-zinc-800/40 transition-colors ${isExpanded ? "bg-zinc-800/30" : ""}`} |
| 565 | + onClick={() => setExpandedVersion(isExpanded ? null : v.version)} |
| 566 | + > |
| 567 | + <td className="py-1"> |
| 568 | + <span |
| 569 | + className="inline-block w-2 h-2 rounded-full mr-1.5" |
| 570 | + style={{ backgroundColor: COLORS[i % COLORS.length] }} |
| 571 | + /> |
| 572 | + <span className="font-mono text-zinc-300">{v.version}</span> |
| 573 | + {isLatest && ( |
| 574 | + <span className="ml-1.5 text-[10px] text-emerald-400 font-medium"> |
| 575 | + latest |
| 576 | + </span> |
| 577 | + )} |
| 578 | + </td> |
| 579 | + <td className="text-right py-1 text-zinc-400">{v.user_count}</td> |
| 580 | + <td className="text-right py-1 text-zinc-500">{v.percentage.toFixed(0)}%</td> |
| 581 | + <td className="text-right py-1 pl-1 text-zinc-600 w-4"> |
| 582 | + {v.users.length > 0 && (isExpanded ? "▾" : "▸")} |
| 583 | + </td> |
| 584 | + </tr> |
| 585 | + {isExpanded && v.users.length > 0 && ( |
| 586 | + <tr> |
| 587 | + <td colSpan={4} className="pb-1"> |
| 588 | + <div className="pl-5 py-1 space-y-0.5"> |
| 589 | + {v.users.map((u) => ( |
| 590 | + <a |
| 591 | + key={u.email} |
| 592 | + href={`/users/${encodeURIComponent(u.email)}`} |
| 593 | + className="block text-[11px] text-zinc-400 hover:text-blue-400 transition-colors" |
| 594 | + > |
| 595 | + {u.name} |
| 596 | + {!isLatest && ( |
| 597 | + <span className="ml-1 text-amber-500/60 text-[10px]"> |
| 598 | + needs update |
| 599 | + </span> |
| 600 | + )} |
| 601 | + </a> |
| 602 | + ))} |
| 603 | + </div> |
| 604 | + </td> |
| 605 | + </tr> |
| 606 | + )} |
| 607 | + </React.Fragment> |
| 608 | + ); |
| 609 | + })} |
| 610 | + </tbody> |
| 611 | + </table> |
| 612 | + </div> |
| 613 | + </div> |
| 614 | + </ChartCard> |
| 615 | + ); |
| 616 | +} |
| 617 | + |
546 | 618 | function PlanExhaustionSection({ data }: { data: PlanExhaustionData }) { |
547 | 619 | const [showUsers, setShowUsers] = useState(false); |
548 | 620 | const { summary, users } = data; |
|
0 commit comments