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:
@@ -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 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,37 +102,76 @@ function HealthCheckPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Server Status */}
|
{/* Server Status */}
|
||||||
<Card>
|
{server && (
|
||||||
<CardHeader>
|
<Card>
|
||||||
<CardTitle className="flex items-center gap-2 font-display text-lg">
|
<CardHeader>
|
||||||
<Server className="h-5 w-5" />
|
<CardTitle className="flex items-center gap-2 font-display text-lg">
|
||||||
Server Status
|
<Server className="h-5 w-5" />
|
||||||
</CardTitle>
|
Server Status
|
||||||
</CardHeader>
|
</CardTitle>
|
||||||
<CardContent>
|
</CardHeader>
|
||||||
<div className="flex items-center gap-3">
|
<CardContent>
|
||||||
<CheckCircle2 className="h-5 w-5 text-status-success-dot" />
|
<div className="flex items-center gap-3">
|
||||||
<div>
|
<CheckCircle2 className="h-5 w-5 text-status-success-dot" />
|
||||||
<p className="text-sm font-medium">Running</p>
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-sm font-medium">Running</p>
|
||||||
Uptime: {formatUptime(server.uptime)}
|
<p className="text-xs text-muted-foreground">
|
||||||
{server.startedAt && (
|
Uptime: {formatUptime(server.uptime)}
|
||||||
<>
|
{server.startedAt && (
|
||||||
{' '}
|
<>
|
||||||
· Started{' '}
|
{' '}
|
||||||
{new Date(server.startedAt).toLocaleString()}
|
· Started{' '}
|
||||||
</>
|
{new Date(server.startedAt).toLocaleString()}
|
||||||
)}
|
</>
|
||||||
</p>
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
)}
|
||||||
|
|
||||||
{/* Accounts */}
|
{/* Accounts */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h2 className="font-display text-lg font-semibold">Accounts</h2>
|
<div className="flex items-center justify-between">
|
||||||
{accounts.length === 0 ? (
|
<h2 className="font-display text-lg font-semibold">Accounts</h2>
|
||||||
|
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user