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
6 changes: 6 additions & 0 deletions platform/lib/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,9 @@ export const translations = {
repoList: {
empty: "No repositories yet.",
searchPlaceholder: "Search repositories...",
human: "Human",
ai: "AI",
sortByAi: "Most AI first",
},
},
investHere: {
Expand Down Expand Up @@ -1868,6 +1871,9 @@ export const translations = {
repoList: {
empty: "Nenhum repositório ainda.",
searchPlaceholder: "Buscar repositórios...",
human: "Humano",
ai: "IA",
sortByAi: "Mais IA primeiro",
},
},
investHere: {
Expand Down
151 changes: 76 additions & 75 deletions platform/src/app/[tenant]/dashboard/repo-list.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
'use client';
"use client";

import { useState } from 'react';
import { useState } from "react";

import Link from 'next/link';
import Link from "next/link";

import { ArrowUp, ArrowDown, Search, Trash2 } from 'lucide-react';

import { Sparkline } from '@/components/charts/Sparkline';
import { DeleteRepositoryDialog } from '@/components/repos/DeleteRepositoryDialog';
import { Button } from '@/components/ui/button';
import { useTranslation } from '@/hooks/useTranslation';
import { cn } from '@/lib/utils';
import type { RepoSummary } from '@/types/temporal';
import { healthIndicator } from '@/types/temporal';
import { ArrowDownWideNarrow, Search, Trash2 } from "lucide-react";

import { DeleteRepositoryDialog } from "@/components/repos/DeleteRepositoryDialog";
import { Button } from "@/components/ui/button";
import { useTranslation } from "@/hooks/useTranslation";
import { cn } from "@/lib/utils";
import type { RepoSummary } from "@/types/temporal";
import { healthIndicator } from "@/types/temporal";

interface RepoListProps {
repos: RepoSummary[];
Expand All @@ -24,10 +22,10 @@ interface RepoListProps {
}

const healthColors: Record<string, string> = {
green: 'bg-signal-purple',
yellow: 'bg-signal-yellow',
red: 'bg-signal-red',
gray: 'bg-signal-gray',
green: "bg-signal-purple",
yellow: "bg-signal-yellow",
red: "bg-signal-red",
gray: "bg-signal-gray",
};

export function RepoList({
Expand All @@ -38,15 +36,18 @@ export function RepoList({
showSearch = false,
}: RepoListProps) {
const { t } = useTranslation();
const [query, setQuery] = useState('');
const [query, setQuery] = useState("");
const [sortByAi, setSortByAi] = useState(false);
const showDeleteColumn = canDelete && !!organizationId;

if (repos.length === 0) {
return (
<div className="flex flex-col items-center gap-3 rounded-md border border-border p-8 text-center text-sm text-muted-foreground">
<p>{t('dashboard.repoList.empty')}</p>
<p>{t("dashboard.repoList.empty")}</p>
<Button asChild size="sm">
<Link href={`/${orgSlug}/connect`}>{t('connect.emptyStateLink')}</Link>
<Link href={`/${orgSlug}/connect`}>
{t("connect.emptyStateLink")}
</Link>
</Button>
</div>
);
Expand All @@ -56,25 +57,45 @@ export function RepoList({
? repos.filter((r) => r.name.toLowerCase().includes(query.toLowerCase()))
: repos;

// Repos without AI data sort to the end regardless of direction.
const sorted = sortByAi
? [...filtered].sort(
(a, b) =>
(b.ai_detection_coverage_pct ?? -1) -
(a.ai_detection_coverage_pct ?? -1),
)
: filtered;

return (
<div className="space-y-2">
{showSearch && repos.length > 5 && (
<div className="relative">
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
placeholder={t('dashboard.repoList.searchPlaceholder')}
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-md border border-border bg-card py-2 pl-9 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
/>
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
placeholder={t("dashboard.repoList.searchPlaceholder")}
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-md border border-border bg-card py-2 pl-9 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
<Button
type="button"
variant={sortByAi ? "default" : "outline"}
size="sm"
onClick={() => setSortByAi((v) => !v)}
className="flex-shrink-0"
>
<ArrowDownWideNarrow className="size-4" />
{t("dashboard.repoList.sortByAi")}
</Button>
</div>
)}
{filtered.map((repo) => {
{sorted.map((repo) => {
const color = healthIndicator(repo.health);
const hasDelta =
repo.stabilization_delta !== null && repo.stabilization_delta !== 0;
const deltaUp = hasDelta && repo.stabilization_delta! > 0;
const aiPct = repo.ai_detection_coverage_pct;
const humanPct = aiPct != null ? 100 - aiPct : null;

return (
<div
Expand All @@ -84,21 +105,23 @@ export function RepoList({
<Link
href={`/${orgSlug}/repos/${encodeURIComponent(repo.name)}`}
className={cn(
'flex min-w-0 flex-1 items-center justify-between gap-3 rounded-md p-3 transition-colors',
'hover:bg-muted/50 active:bg-muted/60',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
"flex min-w-0 flex-1 items-center justify-between gap-3 rounded-md p-3 transition-colors",
"hover:bg-muted/50 active:bg-muted/60",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
)}
>
<div className="flex min-w-0 items-center gap-3">
{/* Health dot */}
<div
className={cn(
'size-2.5 flex-shrink-0 rounded-full',
healthColors[color]
"size-2.5 flex-shrink-0 rounded-full",
healthColors[color],
)}
/>
<div className="min-w-0">
<p className="truncate font-mono text-sm font-medium">{repo.name}</p>
<p className="truncate font-mono text-sm font-medium">
{repo.name}
</p>
<p className="truncate text-xs text-muted-foreground">
{repo.runs_count} runs
{repo.last_run_at &&
Expand All @@ -107,42 +130,20 @@ export function RepoList({
</div>
</div>

<div className="flex flex-shrink-0 items-center gap-3 sm:gap-4">
{/* Sparkline — hidden on very narrow screens to prevent crowding */}
<div className="hidden sm:block">
<Sparkline data={repo.sparkline} />
</div>

{/* AI adoption badge — fixed width to keep alignment */}
<span className="hidden w-12 text-right text-xs text-primary sm:block">
{repo.ai_detection_coverage_pct != null && repo.ai_detection_coverage_pct > 0
? `AI ${repo.ai_detection_coverage_pct < 10
? repo.ai_detection_coverage_pct.toFixed(1)
: repo.ai_detection_coverage_pct.toFixed(0)}%`
: ''}
</span>

{/* Stabilization value + delta */}
<div className="flex items-center gap-1.5 text-right">
<span className="text-sm font-medium">
{repo.stabilization_ratio !== null
? `${(repo.stabilization_ratio * 100).toFixed(0)}%`
: '\u2014'}
</span>
<span
className={cn(
'flex w-4 items-center justify-center text-xs',
!hasDelta && 'invisible',
hasDelta && (deltaUp ? 'text-signal-purple' : 'text-signal-red'),
)}
>
{deltaUp ? (
<ArrowUp className="size-3" />
) : (
<ArrowDown className="size-3" />
)}
</span>
</div>
{/* Commit mix — share of commits by origin */}
<div className="flex flex-shrink-0 items-center gap-3 text-xs">
{humanPct !== null && aiPct !== null ? (
<>
<span className="text-muted-foreground">
{t("dashboard.repoList.human")} {humanPct.toFixed(0)}%
</span>
<span className="text-primary">
{t("dashboard.repoList.ai")} {aiPct.toFixed(0)}%
</span>
</>
) : (
<span className="text-muted-foreground">{"—"}</span>
)}
</div>
</Link>

Expand All @@ -157,7 +158,7 @@ export function RepoList({
variant="ghost"
size="icon"
className="size-8 text-muted-foreground hover:text-destructive"
aria-label={t('repos.deleteButton')}
aria-label={t("repos.deleteButton")}
>
<Trash2 className="size-4" />
</Button>
Expand Down
Loading