Loading portal-gui/src/components/api-clients/api-client-columns.tsx +17 −1 Changes for portal-gui/src/components/api-clients/api-client-columns.tsx: 17 added lines, 1 removed line. Original line number Diff line number Diff line import type { ColumnDef } from '@tanstack/react-table'; import { actionsColumn } from '@/components/data-table/actions-column'; import { Badge } from '@/components/ui/badge'; import type { ApiClient } from '@/types/api-clients'; export const apiClientColumns: ColumnDef<ApiClient>[] = [ export function getApiClientColumns( onDelete: (client: ApiClient) => void, ): ColumnDef<ApiClient>[] { return [ { id: 'drag', header: () => null, Loading Loading @@ -67,4 +71,16 @@ export const apiClientColumns: ColumnDef<ApiClient>[] = [ : '—', size: 120, }, actionsColumn((client) => ({ id: client.id, label: client.name, actions: [ { label: 'Delete', destructive: true, onSelect: () => onDelete(client), }, ], })), ]; } portal-gui/src/features/api-clients/api-clients.tsx +39 −7 Changes for portal-gui/src/features/api-clients/api-clients.tsx: 39 added lines, 7 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { useMemo, useState } from 'react'; import { Plus } from 'lucide-react'; import { ApiClientsFootnote, ApiClientsNotice, } from '@/components/api-clients/api-clients-notices'; import { apiClientColumns } from '@/components/api-clients/api-client-columns'; import { getApiClientColumns } from '@/components/api-clients/api-client-columns'; import { ApiClientSecretDialog } from '@/components/api-clients/api-client-secret-dialog'; import { CreateApiClientDialog } from '@/components/api-clients/create-api-client-dialog'; import { Button } from '@/components/ui/button'; import { ConfirmActionDialog } from '@/components/common/confirm-action-dialog'; import { DataTable } from '@/components/data-table/data-table'; import { EmptyState } from '@/components/common/empty-state'; import { PageContent } from '@/components/common/page-content'; import { PageHeader } from '@/components/common/page-header'; import { PageState } from '@/components/common/page-state'; import { useApiClients } from '@/hooks/useApiClients'; import type { CreatedApiClient } from '@/types/api-clients'; import { deleteApiClient } from '@/services/api-clients/api-clients'; import type { ApiClient, CreatedApiClient } from '@/types/api-clients'; export function ApiClientsPage() { const { clients, loading, error, unavailable, retry } = useApiClients(); const { clients, loading, error, unavailable, retry, removeClient } = useApiClients(); const [creating, setCreating] = useState(false); const [created, setCreated] = useState<CreatedApiClient | null>(null); const [deleting, setDeleting] = useState<ApiClient | null>(null); const columns = useMemo(() => getApiClientColumns(setDeleting), []); return ( <> Loading @@ -36,14 +41,14 @@ export function ApiClientsPage() { /> <PageContent> <ApiClientsNotice /> {unavailable ? ( <EmptyState title="API clients are not available" description="This deployment is not set up for API clients." /> ) : ( <> <ApiClientsNotice /> <PageState loading={loading} error={error} Loading @@ -55,13 +60,14 @@ export function ApiClientsPage() { > <DataTable data={clients} columns={apiClientColumns} columns={columns} getRowId={(client) => client.id} noun="API clients" onRefresh={retry} /> <ApiClientsFootnote /> </PageState> </> )} </PageContent> Loading @@ -75,6 +81,32 @@ export function ApiClientsPage() { }} /> )} {deleting && ( <ConfirmActionDialog title="Delete API client?" description={ <> Are you sure you want to delete{' '} <span className="font-semibold text-foreground"> {deleting.name} </span> ? Its secret stops working at once and its products are revoked. This cannot be undone. To replace a leaked secret, delete the client and create a new one. </> } confirmLabel="Delete" pendingLabel="Deleting…" failureMessage="Failed to delete the API client." onOpenChange={(open) => { if (!open) setDeleting(null); }} onConfirm={async () => { await deleteApiClient(deleting.id); removeClient(deleting.id); }} /> )} {created && ( <ApiClientSecretDialog client={created} Loading portal-gui/src/hooks/useApiClients.ts +6 −1 Changes for portal-gui/src/hooks/useApiClients.ts: 6 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -41,5 +41,10 @@ export function useApiClients() { setReloadKey((key) => key + 1); }, []); return { clients, loading, error, unavailable, retry }; const removeClient = useCallback( (id: string) => setClients((rows) => rows.filter((row) => row.id !== id)), [], ); return { clients, loading, error, unavailable, retry, removeClient }; } portal-gui/src/services/api-clients/api-clients.ts +7 −1 Changes for portal-gui/src/services/api-clients/api-clients.ts: 7 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -27,7 +27,8 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> { problem?.detail ?? `Request failed (${response.status})`, ); } return (await response.json()) as T; // 204: nothing to read. return (response.status === 204 ? undefined : await response.json()) as T; } export const listApiClients = () => request<ApiClient[]>('/api/api-clients'); Loading @@ -37,6 +38,11 @@ export const listApiProducts = () => request<ApiProduct[]>('/api/api-products'); export const getConnection = () => request<Connection>('/api/api-clients/connection'); export const deleteApiClient = (id: string) => request<void>(`/api/api-clients/${encodeURIComponent(id)}`, { method: 'DELETE', }); export const createApiClient = (body: CreateApiClientRequest) => request<CreatedApiClient>('/api/api-clients', { method: 'POST', Loading Loading
portal-gui/src/components/api-clients/api-client-columns.tsx +17 −1 Changes for portal-gui/src/components/api-clients/api-client-columns.tsx: 17 added lines, 1 removed line. Original line number Diff line number Diff line import type { ColumnDef } from '@tanstack/react-table'; import { actionsColumn } from '@/components/data-table/actions-column'; import { Badge } from '@/components/ui/badge'; import type { ApiClient } from '@/types/api-clients'; export const apiClientColumns: ColumnDef<ApiClient>[] = [ export function getApiClientColumns( onDelete: (client: ApiClient) => void, ): ColumnDef<ApiClient>[] { return [ { id: 'drag', header: () => null, Loading Loading @@ -67,4 +71,16 @@ export const apiClientColumns: ColumnDef<ApiClient>[] = [ : '—', size: 120, }, actionsColumn((client) => ({ id: client.id, label: client.name, actions: [ { label: 'Delete', destructive: true, onSelect: () => onDelete(client), }, ], })), ]; }
portal-gui/src/features/api-clients/api-clients.tsx +39 −7 Changes for portal-gui/src/features/api-clients/api-clients.tsx: 39 added lines, 7 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { useMemo, useState } from 'react'; import { Plus } from 'lucide-react'; import { ApiClientsFootnote, ApiClientsNotice, } from '@/components/api-clients/api-clients-notices'; import { apiClientColumns } from '@/components/api-clients/api-client-columns'; import { getApiClientColumns } from '@/components/api-clients/api-client-columns'; import { ApiClientSecretDialog } from '@/components/api-clients/api-client-secret-dialog'; import { CreateApiClientDialog } from '@/components/api-clients/create-api-client-dialog'; import { Button } from '@/components/ui/button'; import { ConfirmActionDialog } from '@/components/common/confirm-action-dialog'; import { DataTable } from '@/components/data-table/data-table'; import { EmptyState } from '@/components/common/empty-state'; import { PageContent } from '@/components/common/page-content'; import { PageHeader } from '@/components/common/page-header'; import { PageState } from '@/components/common/page-state'; import { useApiClients } from '@/hooks/useApiClients'; import type { CreatedApiClient } from '@/types/api-clients'; import { deleteApiClient } from '@/services/api-clients/api-clients'; import type { ApiClient, CreatedApiClient } from '@/types/api-clients'; export function ApiClientsPage() { const { clients, loading, error, unavailable, retry } = useApiClients(); const { clients, loading, error, unavailable, retry, removeClient } = useApiClients(); const [creating, setCreating] = useState(false); const [created, setCreated] = useState<CreatedApiClient | null>(null); const [deleting, setDeleting] = useState<ApiClient | null>(null); const columns = useMemo(() => getApiClientColumns(setDeleting), []); return ( <> Loading @@ -36,14 +41,14 @@ export function ApiClientsPage() { /> <PageContent> <ApiClientsNotice /> {unavailable ? ( <EmptyState title="API clients are not available" description="This deployment is not set up for API clients." /> ) : ( <> <ApiClientsNotice /> <PageState loading={loading} error={error} Loading @@ -55,13 +60,14 @@ export function ApiClientsPage() { > <DataTable data={clients} columns={apiClientColumns} columns={columns} getRowId={(client) => client.id} noun="API clients" onRefresh={retry} /> <ApiClientsFootnote /> </PageState> </> )} </PageContent> Loading @@ -75,6 +81,32 @@ export function ApiClientsPage() { }} /> )} {deleting && ( <ConfirmActionDialog title="Delete API client?" description={ <> Are you sure you want to delete{' '} <span className="font-semibold text-foreground"> {deleting.name} </span> ? Its secret stops working at once and its products are revoked. This cannot be undone. To replace a leaked secret, delete the client and create a new one. </> } confirmLabel="Delete" pendingLabel="Deleting…" failureMessage="Failed to delete the API client." onOpenChange={(open) => { if (!open) setDeleting(null); }} onConfirm={async () => { await deleteApiClient(deleting.id); removeClient(deleting.id); }} /> )} {created && ( <ApiClientSecretDialog client={created} Loading
portal-gui/src/hooks/useApiClients.ts +6 −1 Changes for portal-gui/src/hooks/useApiClients.ts: 6 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -41,5 +41,10 @@ export function useApiClients() { setReloadKey((key) => key + 1); }, []); return { clients, loading, error, unavailable, retry }; const removeClient = useCallback( (id: string) => setClients((rows) => rows.filter((row) => row.id !== id)), [], ); return { clients, loading, error, unavailable, retry, removeClient }; }
portal-gui/src/services/api-clients/api-clients.ts +7 −1 Changes for portal-gui/src/services/api-clients/api-clients.ts: 7 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -27,7 +27,8 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> { problem?.detail ?? `Request failed (${response.status})`, ); } return (await response.json()) as T; // 204: nothing to read. return (response.status === 204 ? undefined : await response.json()) as T; } export const listApiClients = () => request<ApiClient[]>('/api/api-clients'); Loading @@ -37,6 +38,11 @@ export const listApiProducts = () => request<ApiProduct[]>('/api/api-products'); export const getConnection = () => request<Connection>('/api/api-clients/connection'); export const deleteApiClient = (id: string) => request<void>(`/api/api-clients/${encodeURIComponent(id)}`, { method: 'DELETE', }); export const createApiClient = (body: CreateApiClientRequest) => request<CreatedApiClient>('/api/api-clients', { method: 'POST', Loading