feat: Wire AddAccountDialog and UpdateCredentialsDialog into health page and AccountCard

- health.tsx: Add Account button + AddAccountDialog, Refresh button with
  tooltip and spinner calling refreshAccounts mutation, inline account
  error state instead of full-page error, updated empty state text, and
  active-agent warning on delete confirm
- AccountCard.tsx: Show KeyRound button when credentials/token invalid,
  opens UpdateCredentialsDialog pre-populated with account identity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas May
2026-03-06 13:35:14 +01:00
parent b4baba67a5
commit a94e72ccbc
2 changed files with 125 additions and 49 deletions

View File

@@ -1,7 +1,9 @@
import { CheckCircle2, XCircle, AlertTriangle, Trash2 } from "lucide-react"; import { useState } from "react";
import { CheckCircle2, XCircle, AlertTriangle, Trash2, KeyRound } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { UpdateCredentialsDialog } from "./UpdateCredentialsDialog";
function formatResetTime(isoDate: string): string { function formatResetTime(isoDate: string): string {
const now = Date.now(); const now = Date.now();
@@ -100,6 +102,7 @@ export function AccountCard({
account: AccountData; account: AccountData;
onDelete?: (e: React.MouseEvent) => void; onDelete?: (e: React.MouseEvent) => void;
}) { }) {
const [updateCredOpen, setUpdateCredOpen] = useState(false);
const hasWarning = account.credentialsValid && !account.isExhausted && account.error; const hasWarning = account.credentialsValid && !account.isExhausted && account.error;
const statusIcon = !account.credentialsValid ? ( const statusIcon = !account.credentialsValid ? (
@@ -123,6 +126,7 @@ export function AccountCard({
const usage = account.usage; const usage = account.usage;
return ( return (
<>
<Card> <Card>
<CardContent className="space-y-3 py-4"> <CardContent className="space-y-3 py-4">
{/* Header row */} {/* Header row */}
@@ -147,6 +151,17 @@ export function AccountCard({
<span>{statusText}</span> <span>{statusText}</span>
</div> </div>
</div> </div>
{(!account.credentialsValid || !account.tokenValid) && (
<Button
variant="ghost"
size="icon"
className="shrink-0 text-muted-foreground hover:text-foreground"
onClick={() => setUpdateCredOpen(true)}
title="Update credentials"
>
<KeyRound className="h-4 w-4" />
</Button>
)}
{onDelete && ( {onDelete && (
<Button <Button
variant="ghost" variant="ghost"
@@ -220,5 +235,11 @@ export function AccountCard({
)} )}
</CardContent> </CardContent>
</Card> </Card>
<UpdateCredentialsDialog
open={updateCredOpen}
onOpenChange={setUpdateCredOpen}
account={{ id: account.id, email: account.email, provider: account.provider }}
/>
</>
); );
} }

View File

@@ -1,9 +1,13 @@
import { useState } from 'react'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { import {
CheckCircle2, CheckCircle2,
XCircle, XCircle,
RefreshCw, RefreshCw,
Server, Server,
Plus,
RotateCcw,
Loader2,
} from 'lucide-react' } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { trpc } from '@/lib/trpc' import { trpc } from '@/lib/trpc'
@@ -11,6 +15,8 @@ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/Skeleton' import { Skeleton } from '@/components/Skeleton'
import { AccountCard } from '@/components/AccountCard' import { AccountCard } from '@/components/AccountCard'
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
import { AddAccountDialog } from '@/components/AddAccountDialog'
export const Route = createFileRoute('/settings/health')({ export const Route = createFileRoute('/settings/health')({
component: HealthCheckPage, component: HealthCheckPage,
@@ -45,7 +51,23 @@ function HealthCheckPage() {
}, },
}) })
const { data, isLoading, isError, error, refetch } = healthQuery const [addAccountOpen, setAddAccountOpen] = useState(false)
const refreshAccounts = trpc.refreshAccounts.useMutation({
onSuccess: (data) => {
const msg =
data.cleared === 0
? 'No expired flags to clear.'
: `Cleared ${data.cleared} expired exhaustion flag(s).`
toast.success(msg)
void utils.systemHealthCheck.invalidate()
},
onError: (err) => {
toast.error(`Failed to refresh: ${err.message}`)
},
})
const { data, isLoading, isError, refetch } = healthQuery
// Loading state // Loading state
if (isLoading) { if (isLoading) {
@@ -61,24 +83,9 @@ function HealthCheckPage() {
) )
} }
// Error state const server = data?.server
if (isError) { const accounts = data?.accounts ?? []
return ( const projects = data?.projects ?? []
<div className="flex flex-col items-center justify-center gap-4 py-12">
<XCircle className="h-8 w-8 text-status-error-fg" />
<p className="text-sm text-status-error-fg">
Failed to load health check: {error?.message ?? 'Unknown error'}
</p>
<Button variant="outline" size="sm" onClick={() => void refetch()}>
Retry
</Button>
</div>
)
}
if (!data) return null
const { server, accounts, projects } = data
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -95,6 +102,7 @@ function HealthCheckPage() {
</div> </div>
{/* Server Status */} {/* Server Status */}
{server && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 font-display text-lg"> <CardTitle className="flex items-center gap-2 font-display text-lg">
@@ -121,11 +129,49 @@ function HealthCheckPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
)}
{/* Accounts */} {/* Accounts */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between">
<h2 className="font-display text-lg font-semibold">Accounts</h2> <h2 className="font-display text-lg font-semibold">Accounts</h2>
{accounts.length === 0 ? ( <div className="flex items-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() => refreshAccounts.mutate()}
disabled={refreshAccounts.isPending}
>
{refreshAccounts.isPending
? <Loader2 className="h-4 w-4 animate-spin" />
: <RotateCcw className="h-4 w-4" />}
<span className="sr-only">Refresh</span>
</Button>
</TooltipTrigger>
<TooltipContent>Clear expired exhaustion flags</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
size="sm"
onClick={() => setAddAccountOpen(true)}
>
<Plus className="mr-2 h-4 w-4" />
Add Account
</Button>
</div>
</div>
{isError ? (
<Card>
<CardContent className="py-6">
<p className="text-center text-sm text-status-error-fg">
Could not load account status. Retrying
</p>
</CardContent>
</Card>
) : accounts.length === 0 ? (
<Card> <Card>
<CardContent className="py-6"> <CardContent className="py-6">
<p className="text-center text-sm text-muted-foreground"> <p className="text-center text-sm text-muted-foreground">
@@ -133,7 +179,7 @@ function HealthCheckPage() {
<code className="rounded bg-muted px-1 py-0.5 text-xs"> <code className="rounded bg-muted px-1 py-0.5 text-xs">
cw account add cw account add
</code>{' '} </code>{' '}
to register one. to register one, or click 'Add Account' above.
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -143,7 +189,14 @@ function HealthCheckPage() {
key={account.id} key={account.id}
account={account} account={account}
onDelete={(e) => { onDelete={(e) => {
if (e.shiftKey || window.confirm(`Remove account "${account.email}"?`)) { if (
e.shiftKey ||
(account.activeAgentCount > 0
? window.confirm(
`This account has ${account.activeAgentCount} active agent(s). Deleting it will not stop those agents but they will lose their account association. Continue?`
)
: window.confirm(`Remove account "${account.email}"?`))
) {
removeAccount.mutate({ id: account.id }) removeAccount.mutate({ id: account.id })
} }
}} }}
@@ -186,6 +239,8 @@ function HealthCheckPage() {
)) ))
)} )}
</div> </div>
<AddAccountDialog open={addAccountOpen} onOpenChange={setAddAccountOpen} />
</div> </div>
) )
} }