src/features/account/overview-page.tsx
Rama main · réplica de lectura del repositorio original.
import type { AccessRequestApproval, Agent } from '@shared/api/agent-api'
import { Link } from '@tanstack/react-router'
import { ArrowUpRight, Check, ChevronRight, CircleDot, Pause, ShieldCheck, Users } from 'lucide-react'
import { useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Status } from '@/components/ui/status'
import {
acceptAccountOrganizationInvitation,
activateAgent,
deactivateAgent,
decideAccountAgentResourceRequest,
deleteAgent,
rejectAccountOrganizationInvitation,
} from '@/lib/api/account'
import { tt } from '@/lib/i18n'
import * as Drawer from './account-drawer'
import { AccountEmptyState, AccountPageHeader, AccountRow, AccountRows } from './account-page'
import { AgentEnrollmentGuide } from './agent-enrollment-guide'
import { IdentityMark } from './identity-mark'
import { AgentDialog, AgentRequestDialog } from './planned-pages'
import { DestructiveConfirmationDialog, useDestructiveConfirmation } from './primitives'
import {
accountQueryKeys,
useAccountAccessRequests,
useAccountAgents,
useAccountMutation,
useAccountOrganizationInvitations,
} from './queries'
export function AccountOverviewPage() {
const agentsQuery = useAccountAgents()
const requestsQuery = useAccountAccessRequests()
const invitationsQuery = useAccountOrganizationInvitations()
const runMutation = useAccountMutation()
function mutate(label: string, operation: () => Promise<unknown>, options: Parameters<typeof runMutation>[2]) {
return runMutation(
label,
async () => {
await operation()
return true
},
options,
)
}
const [selected, setSelected] = useState<Agent | null>(null)
const [request, setRequest] = useState<AccessRequestApproval | null>(null)
const [invitationId, setInvitationId] = useState<string | null>(null)
const [confirmation, setConfirmation] = useDestructiveConfirmation()
const agents = agentsQuery.data?.items ?? []
const requests = requestsQuery.data?.items ?? []
const invitations = (invitationsQuery.data ?? []).filter((item) => item.status === 'pending')
const invitation = invitations.find((item) => item.id === invitationId)
const count = requests.length + invitations.length
return (
<>
<AccountPageHeader
title={tt('Workbench')}
description={tt('Manage your Agents and review access that needs your approval.')}
action={<AgentEnrollmentGuide />}
/>
<div className="accountWorkbench">
<section>
<div className="accountWorkbenchHeading">
<div>
<h2>{tt('Your Agents')}</h2>
<span>
{tt('{{count}} identities · {{active}} enabled', {
count: agents.length,
active: agents.filter((agent) => agent.status === 'active').length,
})}
</span>
</div>
<Link to="/agents">
{tt('Manage Agents')} <ArrowUpRight />
</Link>
</div>
{agentsQuery.error ? (
<Status tone="error">{agentsQuery.error.message}</Status>
) : agentsQuery.isLoading ? (
<Status>{tt('Loading Agents…')}</Status>
) : agents.length ? (
<div className="accountAgentCards">
{agents.slice(0, 3).map((agent) => {
const agentRequests = requests.filter((item) => item.agent.id === agent.id)
return (
<article className="accountAgentCard" key={agent.id}>
<button
type="button"
className="accountAgentCardMain"
onClick={() => setSelected(agent)}
aria-label={tt('Manage {{name}}', { name: agent.name })}
>
<div className="accountAgentCardTop">
<IdentityMark name={agent.name} />
<Badge variant={agent.status === 'active' ? 'secondary' : 'outline'}>
{tt(agent.status === 'active' ? 'Enabled' : 'Disabled')}
</Badge>
</div>
<h3>{agent.name}</h3>
<p>{agent.subject}</p>
<div className="accountAgentCardMeta">
<span>{tt('Agent identity')}</span>
<ArrowUpRight />
</div>
</button>
<div className="accountAgentCardBottom">
{agentRequests.length ? (
<button type="button" onClick={() => setRequest(agentRequests[0]!)}>
<CircleDot />
<span>{tt('{{count}} requests to review', { count: agentRequests.length })}</span>
<ChevronRight />
</button>
) : (
<span>
{agent.status === 'active' ? <Check /> : <Pause />}
{requestsQuery.error
? tt('Unable to load requests')
: requestsQuery.isLoading
? tt('Loading requests…')
: tt(agent.status === 'active' ? 'No pending requests' : 'Identity disabled')}
</span>
)}
</div>
</article>
)
})}
</div>
) : (
<div className="accountOnboarding">
<AccountEmptyState
title={tt('Start with your first Agent')}
description={tt(
'Give the enrollment instructions to your Agent. It completes setup in its own environment.',
)}
/>
<AgentEnrollmentGuide />
</div>
)}
</section>
<section>
<div className="accountWorkbenchHeading">
<div>
<h2>{tt('Needs your confirmation')}</h2>
<span>{tt('{{count}} items', { count })}</span>
</div>
</div>
<div className="accountInbox">
{requestsQuery.error || invitationsQuery.error ? (
<Status tone="error">{(requestsQuery.error ?? invitationsQuery.error)?.message}</Status>
) : null}
{requestsQuery.isLoading || invitationsQuery.isLoading ? <Status>{tt('Loading requests…')}</Status> : null}
{requests.map((item) => (
<div className="accountInboxRow" key={item.id}>
<span className="accountInboxIcon">
<ShieldCheck />
</span>
<div>
<strong>
{tt('{{agent}} requests access to {{resource}}', {
agent: item.agent.name,
resource: item.resourceServer.name,
})}
</strong>
<p>{item.authorizationDetail?.name ?? item.resourceServer.name}</p>
<code>{item.scopes.join(' ')}</code>
</div>
<Button onClick={() => setRequest(item)}>{tt('Review request')}</Button>
</div>
))}
{invitations.map((item) => (
<div className="accountInboxRow" key={item.id}>
<span className="accountInboxIcon">
<Users />
</span>
<div>
<strong>{tt('Join {{organization}}', { organization: item.organizationName })}</strong>
<p>
{tt('Organization invitation')} · {item.role}
</p>
</div>
<Button variant="outline" onClick={() => setInvitationId(item.id)}>
{tt('Review invitation')}
</Button>
</div>
))}
{!count &&
!requestsQuery.isLoading &&
!invitationsQuery.isLoading &&
!requestsQuery.error &&
!invitationsQuery.error ? (
<AccountEmptyState
title={tt("You're all caught up")}
description={tt('New access requests and organization invitations will appear here.')}
/>
) : null}
</div>
</section>
</div>
<AgentRequestDialog
request={request}
onClose={() => setRequest(null)}
onDecision={async (item, input) => {
const result = await mutate(
input.decision === 'approve' ? 'Request approved.' : 'Request denied.',
() => decideAccountAgentResourceRequest(item.id, input),
{ invalidate: [accountQueryKeys.accessRequests] },
)
if (result) setRequest(null)
}}
/>
<AgentDialog
agent={selected}
onClose={() => setSelected(null)}
onStatusChange={async (agent) => {
const result = await mutate(
'Agent updated.',
() => (agent.status === 'active' ? deactivateAgent(agent.id) : activateAgent(agent.id)),
{ invalidate: [accountQueryKeys.agents] },
)
if (result) setSelected(null)
}}
onDelete={(agent) =>
setConfirmation({
title: tt('Delete {{agent}}?', { agent: agent.name }),
description: tt(
'The Agent disappears from every interface. Hosts and active resource access stop immediately, and it cannot be restored.',
),
actionLabel: tt('Delete Agent'),
onConfirm: async () => {
const result = await mutate('Agent deleted.', () => deleteAgent(agent.id), {
invalidate: [accountQueryKeys.agents, accountQueryKeys.accessRequests],
})
if (result) setSelected(null)
},
})
}
/>
<Drawer.Dialog open={Boolean(invitation)} onOpenChange={(open) => !open && setInvitationId(null)}>
<Drawer.DialogContent>
<Drawer.DialogHeader>
<Drawer.DialogTitle>{tt('Organization invitation')}</Drawer.DialogTitle>
<Drawer.DialogDescription>{invitation?.organizationName}</Drawer.DialogDescription>
</Drawer.DialogHeader>
{invitation ? (
<>
<AccountRows>
<AccountRow label={tt('Organization')} value={invitation.organizationName} />
<AccountRow label={tt('Role')} value={invitation.role} />
</AccountRows>
<Drawer.DialogFooter>
<Button
variant="outline"
onClick={async () => {
const result = await mutate(
'Invitation declined.',
() => rejectAccountOrganizationInvitation(invitation.id),
{ invalidate: [accountQueryKeys.organizationInvitations] },
)
if (result) setInvitationId(null)
}}
>
{tt('Decline')}
</Button>
<Button
onClick={async () => {
const result = await mutate(
'Invitation accepted.',
() => acceptAccountOrganizationInvitation(invitation.id),
{
invalidate: [
accountQueryKeys.organizationInvitations,
accountQueryKeys.organizations,
accountQueryKeys.developerConsoleAccess,
],
},
)
if (result) setInvitationId(null)
}}
>
{tt('Accept')}
</Button>
</Drawer.DialogFooter>
</>
) : null}
</Drawer.DialogContent>
</Drawer.Dialog>
<DestructiveConfirmationDialog confirmation={confirmation} onClose={() => setConfirmation(null)} />
</>
)
}