Forráskód Böngészése

Improve teams cockpit interactions

Jax Docker 1 hónapja
szülő
commit
7cb7569351
1 módosított fájl, 93 hozzáadás és 93 törlés
  1. 93 93
      web/src/pages/teams/TeamsPage.tsx

+ 93 - 93
web/src/pages/teams/TeamsPage.tsx

@@ -2,38 +2,32 @@ import * as React from "react";
 import { useTranslation } from "react-i18next";
 import {
   Activity,
-  Archive,
   Bot,
   CheckCircle2,
-  Copy,
-  FileCode2,
-  Network,
-  Play,
+  Clock,
   RefreshCw,
   Search,
   SlidersHorizontal,
   Users,
 } from "lucide-react";
-import { listTeamRuns, listTeamVersions, listTeams } from "@/api";
+import { listTeamConfigs, listTeamRuns, listTeams } from "@/api";
 import { ApiErrorState } from "@/components/shared/ApiErrorState";
 import { EmptyState } from "@/components/shared/EmptyState";
-import { EntityListItem } from "@/components/shared/EntityListItem";
 import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
 import { MetricCard } from "@/components/shared/MetricCard";
 import { PageHeader } from "@/components/shared/PageHeader";
 import { SearchInput } from "@/components/shared/SearchInput";
 import { StatusBadge } from "@/components/shared/StatusBadge";
+import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { Select } from "@/components/ui/select";
-import { Tabs } from "@/components/ui/tabs";
 import { toast } from "@/components/ui/toaster";
-import { copyToClipboard } from "@/lib/utils";
-import type { TeamDefinition, TeamRun, TeamStatus, TeamVersion } from "@/types";
+import { cn, relativeTime } from "@/lib/utils";
+import type { TeamConfig, TeamDefinition, TeamRun, TeamStatus } from "@/types";
 import { CreateTeamDialog } from "./components/CreateTeamDialog";
 import { TeamOverview } from "./components/TeamOverview";
 import { TeamRuns } from "./components/TeamRuns";
-import { TeamVersions } from "./components/TeamVersions";
 
 type StatusFilter = "all" | TeamStatus;
 type SortMode = "recent" | "name" | "status";
@@ -41,35 +35,34 @@ type SortMode = "recent" | "name" | "status";
 export function TeamsPage() {
   const { t } = useTranslation();
   const [teams, setTeams] = React.useState<TeamDefinition[]>([]);
-  const [versions, setVersions] = React.useState<TeamVersion[]>([]);
+  const [configs, setConfigs] = React.useState<TeamConfig[]>([]);
   const [runs, setRuns] = React.useState<TeamRun[]>([]);
   const [selectedTeamId, setSelectedTeamId] = React.useState<string>();
   const [search, setSearch] = React.useState("");
   const [statusFilter, setStatusFilter] = React.useState<StatusFilter>("all");
   const [sortMode, setSortMode] = React.useState<SortMode>("recent");
-  const [activeTab, setActiveTab] = React.useState("overview");
   const [loading, setLoading] = React.useState(true);
   const [error, setError] = React.useState<string>();
   const [relatedLoading, setRelatedLoading] = React.useState(false);
   const [createOpen, setCreateOpen] = React.useState(false);
 
   const selectedTeam = teams.find((team) => team.id === selectedTeamId);
-  const sortedVersions = React.useMemo(
-    () => [...versions].sort((a, b) => b.version_no - a.version_no),
-    [versions],
+  const sortedConfigs = React.useMemo(
+    () => [...configs].sort((a, b) => b.version_no - a.version_no),
+    [configs],
   );
   const sortedRuns = React.useMemo(
     () => [...runs].sort((a, b) => new Date(b.created_time).getTime() - new Date(a.created_time).getTime()),
     [runs],
   );
-  const latestVersion = sortedVersions[0];
+  const activeConfig = sortedConfigs[0];
   const failedRunCount = sortedRuns.filter((run) => run.status === "failed").length;
   const activeTeams = teams.filter((team) => team.status === "active").length;
   const draftTeams = teams.filter((team) => team.status === "draft").length;
 
   const filtered = teams
     .filter((team) => {
-      const text = `${team.name} ${team.code} ${team.team_type} ${team.description ?? ""}`.toLowerCase();
+      const text = `${team.name} ${team.team_type} ${team.description ?? ""}`.toLowerCase();
       const matchesSearch = text.includes(search.toLowerCase());
       const matchesStatus = statusFilter === "all" || team.status === statusFilter;
       return matchesSearch && matchesStatus;
@@ -99,11 +92,11 @@ export function TeamsPage() {
   const reloadDetails = React.useCallback(async () => {
     if (!selectedTeamId) return;
     setRelatedLoading(true);
-    setVersions([]);
+    setConfigs([]);
     setRuns([]);
     try {
-      const [versionData, runData] = await Promise.all([listTeamVersions(selectedTeamId), listTeamRuns(selectedTeamId)]);
-      setVersions(versionData);
+      const [configData, runData] = await Promise.all([listTeamConfigs(selectedTeamId), listTeamRuns(selectedTeamId)]);
+      setConfigs(configData);
       setRuns(runData);
     } catch {
       toast.error(t("errors.failedToLoad"));
@@ -122,12 +115,6 @@ export function TeamsPage() {
     setSortMode("recent");
   }
 
-  async function copyTeamCode() {
-    if (!selectedTeam) return;
-    await copyToClipboard(selectedTeam.code);
-    toast.success(t("teams.teamCodeCopied"));
-  }
-
   if (loading) return <LoadingSpinner label={t("common.loading")} />;
   if (error) return <ApiErrorState message={error} onRetry={() => void load()} />;
 
@@ -148,29 +135,28 @@ export function TeamsPage() {
         }
       />
 
-      <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
+      <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
         <MetricCard label={t("teams.title")} value={teams.length} icon={Users} />
         <MetricCard label={t("common.active")} value={activeTeams} icon={CheckCircle2} />
-        <MetricCard label={t("common.draft")} value={draftTeams} icon={Archive} />
-        <MetricCard label={t("common.versions")} value={versions.length} icon={Network} />
+        <MetricCard label={t("common.runs")} value={sortedRuns.length} icon={Activity} />
         <MetricCard label={t("teams.failedRuns")} value={failedRunCount} icon={Activity} />
       </div>
 
-      <div className="grid gap-6 xl:grid-cols-[440px_1fr]">
-        <Card>
+      <div className="grid gap-6 xl:grid-cols-[460px_1fr]">
+        <Card className="overflow-hidden">
           <CardHeader>
             <div className="flex items-start justify-between gap-3">
               <div>
                 <CardTitle>{t("teams.teamDirectory")}</CardTitle>
                 <CardDescription>
-                  {t("teams.teamsShown", { count: teams.length })} {filtered.length}
+                  {filtered.length} / {teams.length} - {draftTeams} {t("common.draft")}
                 </CardDescription>
               </div>
               <SlidersHorizontal className="mt-1 h-4 w-4 text-muted-foreground" />
             </div>
           </CardHeader>
           <CardContent className="space-y-4">
-            <SearchInput value={search} onChange={setSearch} placeholder={t("teams.searchByNameCodeType")} />
+            <SearchInput value={search} onChange={setSearch} placeholder="Search by name, type, or description" />
             <div className="grid gap-3 sm:grid-cols-2">
               <Select
                 aria-label={t("teams.filterByStatus")}
@@ -201,15 +187,14 @@ export function TeamsPage() {
             ) : null}
 
             {filtered.length ? (
-              <div className="space-y-2">
+              <div className="space-y-3">
                 {filtered.map((team) => (
-                  <EntityListItem
+                  <TeamDirectoryItem
                     key={team.id}
+                    team={team}
                     active={team.id === selectedTeamId}
-                    title={team.name}
-                    subtitle={`${readableLabel(team.team_type)} - ${team.code}`}
-                    meta={<StatusBadge status={team.status} />}
-                    onClick={() => { setSelectedTeamId(team.id); setActiveTab("overview"); }}
+                    t={t}
+                    onClick={() => setSelectedTeamId(team.id)}
                   />
                 ))}
               </div>
@@ -219,73 +204,45 @@ export function TeamsPage() {
           </CardContent>
         </Card>
 
-        <Card>
-          <CardHeader>
+        <Card className="min-w-0 overflow-hidden">
+          <CardHeader className="border-b border-border bg-muted/15 p-5">
             <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
               <div className="min-w-0">
-                <div className="flex flex-wrap items-center gap-2">
+                <div className="flex min-w-0 flex-wrap items-center gap-2">
                   <CardTitle className="truncate text-lg">{selectedTeam?.name ?? t("teams.teamCockpit")}</CardTitle>
                   {selectedTeam ? <StatusBadge status={selectedTeam.status} /> : null}
                 </div>
                 <CardDescription className="mt-1">
-                  {selectedTeam ? `${readableLabel(selectedTeam.team_type)} - ${selectedTeam.code}` : t("teams.selectTeamInspect")}
+                  {selectedTeam ? readableLabel(selectedTeam.team_type) : t("teams.selectTeamInspect")}
                 </CardDescription>
               </div>
               <div className="flex flex-wrap items-center gap-2">
-                <Button variant="outline" size="sm" disabled={!selectedTeam} onClick={() => void copyTeamCode()}>
-                  <Copy className="h-4 w-4" /> {t("teams.copyCode")}
-                </Button>
-                <Button variant="secondary" size="sm" disabled={!selectedTeam} onClick={() => setCreateOpen(true)}>
-                  <FileCode2 className="h-4 w-4" /> {t("teams.newVersion")}
-                </Button>
-                {selectedTeam && latestVersion ? (
-                  <Button size="sm" onClick={() => setActiveTab("runs")}>
-                    <Play className="h-4 w-4" /> {t("teams.run")}
-                  </Button>
+                {selectedTeam && activeConfig ? (
+                  <Badge className="border-border bg-surface-elevated text-muted-foreground">
+                    {sortedRuns.length} {t("common.runs")}
+                  </Badge>
                 ) : null}
               </div>
             </div>
           </CardHeader>
-          <CardContent>
+          <CardContent className="p-4">
             {selectedTeam ? (
-              <Tabs
-                value={activeTab}
-                onChange={setActiveTab}
-                tabs={[
-                  {
-                    value: "overview",
-                    label: t("common.overview"),
-                    content: (
-                      <TeamOverview
-                        team={selectedTeam}
-                        latestVersion={latestVersion}
-                        versionCount={versions.length}
-                        runCount={sortedRuns.length}
-                        failedRunCount={failedRunCount}
-                        latestRun={sortedRuns[0]}
-                      />
-                    ),
-                  },
-                  {
-                    value: "runs",
-                    label: t("common.runs"),
-                    content: (
-                      <TeamRuns
-                        teamId={selectedTeam.id}
-                        versions={sortedVersions}
-                        runs={sortedRuns}
-                        loading={relatedLoading}
-                        onRunCreated={(run) => { setRuns((current) => [run, ...current]); void reloadDetails(); }}
-                      />
-                    ),
-                  },
-                  {
-                    value: "versions",
-                    label: t("common.versions"),
-                    content: <TeamVersions versions={versions} loading={relatedLoading} />,
-                  },
-                ]}
-              />
+              <div className="grid min-w-0 gap-4 2xl:grid-cols-[minmax(0,1fr)_380px]">
+                <TeamOverview
+                  team={selectedTeam}
+                  activeConfig={activeConfig}
+                  runCount={sortedRuns.length}
+                  failedRunCount={failedRunCount}
+                  latestRun={sortedRuns[0]}
+                />
+                <TeamRuns
+                  teamId={selectedTeam.id}
+                  configs={sortedConfigs}
+                  runs={sortedRuns}
+                  loading={relatedLoading}
+                  onRunCreated={(run) => { setRuns((current) => [run, ...current]); void reloadDetails(); }}
+                />
+              </div>
             ) : (
               <EmptyState icon={Bot} title={t("teams.noTeams")} description={t("teams.createTeamStart")} actionLabel={t("teams.newTeam")} onAction={() => setCreateOpen(true)} />
             )}
@@ -306,6 +263,49 @@ export function TeamsPage() {
   );
 }
 
+function TeamDirectoryItem({
+  team,
+  active,
+  t,
+  onClick,
+}: {
+  team: TeamDefinition;
+  active?: boolean;
+  t: (key: string, options?: Record<string, unknown>) => string;
+  onClick: () => void;
+}) {
+  return (
+    <button
+      type="button"
+      onClick={onClick}
+      className={cn(
+        "w-full rounded-md border border-border bg-muted/30 p-4 text-left transition hover:bg-muted/55 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary",
+        active && "border-primary/45 bg-primary/10 shadow-glow",
+      )}
+    >
+      <div className="flex items-start justify-between gap-3">
+        <div className="min-w-0">
+          <div className="flex items-center gap-2">
+            <Users className="h-4 w-4 text-primary" />
+            <p className="truncate text-sm font-semibold">{team.name}</p>
+          </div>
+          <p className="mt-1 truncate text-xs text-muted-foreground">{readableLabel(team.team_type)}</p>
+        </div>
+        <StatusBadge status={team.status} />
+      </div>
+      <p className="mt-3 line-clamp-2 text-sm leading-6 text-muted-foreground">
+        {team.description ?? t("teams.noDescription")}
+      </p>
+      <div className="mt-3 flex flex-wrap items-center gap-2">
+        <Badge className="border-border bg-surface-elevated text-muted-foreground">{readableLabel(team.team_type)}</Badge>
+        <span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
+          <Clock className="h-3.5 w-3.5" /> {relativeTime(team.created_time)}
+        </span>
+      </div>
+    </button>
+  );
+}
+
 function readableLabel(value: string) {
   return value.split(/[_-]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" ");
 }