Skip to content

Commit 1eb09fa

Browse files
authored
feat: add logs for remote mcp (#1207)
* feat: enable redirect to logs for remote mcp * feat: handle remote logs with skeleton * refactor: move api call on component in order to leverage skeleton, using always fresh data * test: add use case for logs page * leftover * leftover
1 parent 045f8e8 commit 1eb09fa

File tree

5 files changed

+214
-36
lines changed

5 files changed

+214
-36
lines changed

renderer/src/features/mcp-servers/components/card-mcp-server/server-actions/items/logs-menu-item.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@ export function LogsMenuItem({ serverName, remote, group }: LogsMenuItemProps) {
1212
const groupName = group ?? 'default'
1313

1414
return (
15-
<DropdownMenuItem
16-
asChild
17-
className="flex cursor-pointer items-center"
18-
disabled={remote}
19-
>
15+
<DropdownMenuItem asChild className="flex cursor-pointer items-center">
2016
<Link
2117
to="/logs/$groupName/$serverName"
2218
params={{ serverName, groupName }}
19+
search={{ remote }}
2320
>
2421
<Text className="mr-2 h-4 w-4" />
2522
Logs
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { screen, waitFor } from '@testing-library/react'
2+
import { describe, it, expect, vi } from 'vitest'
3+
import { LogsPage } from '../index'
4+
import { createTestRouter } from '@/common/test/create-test-router'
5+
import { renderRoute } from '@/common/test/render-route'
6+
import {
7+
MCP_OPTIMIZER_GROUP_NAME,
8+
META_MCP_SERVER_NAME,
9+
} from '@/common/lib/constants'
10+
import { server } from '@/common/mocks/node'
11+
import { http, HttpResponse } from 'msw'
12+
import { mswEndpoint } from '@/common/mocks/customHandlers'
13+
import { getMockLogs } from '@/common/mocks/customHandlers/fixtures/servers'
14+
15+
describe('LogsPage Component', () => {
16+
describe('Loading State - Skeleton', () => {
17+
it('displays skeleton while loading logs', async () => {
18+
vi.spyOn(console, 'error').mockImplementation(() => {})
19+
20+
server.use(
21+
http.get(mswEndpoint('/api/v1beta/workloads/:name/logs'), async () => {
22+
return HttpResponse.text(getMockLogs('test-server'))
23+
})
24+
)
25+
26+
const router = createTestRouter(LogsPage, '/logs/$groupName/$serverName')
27+
router.navigate({
28+
to: '/logs/$groupName/$serverName',
29+
params: { serverName: 'test-server', groupName: 'default' },
30+
})
31+
renderRoute(router)
32+
33+
await waitFor(() => {
34+
expect(screen.getByTestId('skeleton-logs')).toBeVisible()
35+
})
36+
})
37+
})
38+
39+
describe('Basic Rendering', () => {
40+
it('displays server name as heading for regular server', async () => {
41+
const router = createTestRouter(LogsPage, '/logs/$groupName/$serverName')
42+
router.navigate({
43+
to: '/logs/$groupName/$serverName',
44+
params: { serverName: 'test-server', groupName: 'default' },
45+
})
46+
renderRoute(router)
47+
48+
await waitFor(() => {
49+
expect(
50+
screen.getByRole('heading', { name: 'test-server' })
51+
).toBeVisible()
52+
})
53+
})
54+
55+
it('renders search input', async () => {
56+
const router = createTestRouter(LogsPage, '/logs/$groupName/$serverName')
57+
router.navigate({
58+
to: '/logs/$groupName/$serverName',
59+
params: { serverName: 'test-server', groupName: 'default' },
60+
})
61+
renderRoute(router)
62+
63+
await waitFor(() => {
64+
expect(screen.getByPlaceholderText('Search log')).toBeVisible()
65+
})
66+
})
67+
68+
it('renders back button with correct link', async () => {
69+
const router = createTestRouter(LogsPage, '/logs/$groupName/$serverName')
70+
router.navigate({
71+
to: '/logs/$groupName/$serverName',
72+
params: { serverName: 'test-server', groupName: 'production' },
73+
})
74+
renderRoute(router)
75+
76+
await waitFor(() => {
77+
expect(
78+
screen.getByRole('heading', { name: 'test-server' })
79+
).toBeVisible()
80+
})
81+
82+
const backButton = screen.getByRole('button', { name: /back/i })
83+
expect(backButton.closest('a')).toHaveAttribute(
84+
'href',
85+
'/group/production'
86+
)
87+
})
88+
})
89+
90+
describe('MCP Optimizer Special Handling', () => {
91+
it('displays "MCP Optimizer" as title for optimizer group', async () => {
92+
const router = createTestRouter(LogsPage, '/logs/$groupName/$serverName')
93+
router.navigate({
94+
to: '/logs/$groupName/$serverName',
95+
params: {
96+
serverName: META_MCP_SERVER_NAME,
97+
groupName: MCP_OPTIMIZER_GROUP_NAME,
98+
},
99+
})
100+
renderRoute(router)
101+
102+
await waitFor(() => {
103+
expect(
104+
screen.getByRole('heading', { name: 'MCP Optimizer' })
105+
).toBeVisible()
106+
})
107+
})
108+
109+
it('back button links to /mcp-optimizer for optimizer group', async () => {
110+
const router = createTestRouter(LogsPage, '/logs/$groupName/$serverName')
111+
router.navigate({
112+
to: '/logs/$groupName/$serverName',
113+
params: {
114+
serverName: META_MCP_SERVER_NAME,
115+
groupName: MCP_OPTIMIZER_GROUP_NAME,
116+
},
117+
})
118+
renderRoute(router)
119+
120+
await waitFor(() => {
121+
expect(
122+
screen.getByRole('heading', { name: 'MCP Optimizer' })
123+
).toBeVisible()
124+
})
125+
126+
const backButton = screen.getByRole('button', { name: /back/i })
127+
expect(backButton.closest('a')).toHaveAttribute('href', '/mcp-optimizer')
128+
})
129+
})
130+
})

renderer/src/features/mcp-servers/sub-pages/logs-page/index.tsx

Lines changed: 67 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,64 @@
1-
import { useParams } from '@tanstack/react-router'
1+
import { useParams, useSearch } from '@tanstack/react-router'
22
import { Button } from '@/common/components/ui/button'
33
import { ChevronLeft } from 'lucide-react'
44
import { Separator } from '@/common/components/ui/separator'
55
import { useState } from 'react'
6-
import { useSuspenseQuery } from '@tanstack/react-query'
7-
import { getApiV1BetaWorkloadsByNameLogsOptions } from '@api/@tanstack/react-query.gen'
6+
import { useQuery } from '@tanstack/react-query'
7+
import {
8+
getApiV1BetaWorkloadsByNameLogsOptions,
9+
getApiV1BetaWorkloadsByNameProxyLogsOptions,
10+
} from '@api/@tanstack/react-query.gen'
811
import { RefreshButton } from '@/common/components/refresh-button'
912
import { LinkViewTransition } from '@/common/components/link-view-transition'
1013
import { InputSearch } from '@/common/components/ui/input-search'
1114
import { highlight } from './search'
1215
import { MCP_OPTIMIZER_GROUP_NAME } from '@/common/lib/constants'
16+
import { Skeleton } from '@/common/components/ui/skeleton'
17+
18+
function SkeletonLogs() {
19+
return (
20+
<div
21+
className="flex h-full w-full flex-1 flex-col gap-4 p-10"
22+
data-testid="skeleton-logs"
23+
>
24+
{Array.from({ length: 20 }).map((_, i) => {
25+
const numSkeletons = Math.floor(Math.random() * 6) + 1
26+
return (
27+
<div key={i} className="flex w-full gap-2">
28+
<Skeleton className="h-4 w-12 shrink-0" />
29+
{Array.from({ length: numSkeletons }).map((_, j) => (
30+
<Skeleton key={j} className="h-4 flex-1" />
31+
))}
32+
</div>
33+
)
34+
})}
35+
</div>
36+
)
37+
}
38+
39+
function useLogs() {
40+
const { serverName, groupName } = useParams({
41+
from: '/logs/$groupName/$serverName',
42+
})
43+
const { remote } = useSearch({ from: '/logs/$groupName/$serverName' })
44+
return useQuery({
45+
...(remote
46+
? getApiV1BetaWorkloadsByNameProxyLogsOptions({
47+
path: { name: serverName },
48+
})
49+
: getApiV1BetaWorkloadsByNameLogsOptions({ path: { name: serverName } })),
50+
51+
enabled: !!serverName && !!groupName,
52+
})
53+
}
1354

1455
export function LogsPage() {
1556
const { serverName, groupName } = useParams({
1657
from: '/logs/$groupName/$serverName',
1758
})
1859
const [search, setSearch] = useState('')
19-
20-
const { data: logs, refetch } = useSuspenseQuery(
21-
getApiV1BetaWorkloadsByNameLogsOptions({ path: { name: serverName } })
22-
)
60+
const { data: logs, refetch, isFetching, isLoading } = useLogs()
61+
const isLoadingState = isLoading || isFetching
2362

2463
const logLines =
2564
typeof logs === 'string'
@@ -72,23 +111,27 @@ export function LogsPage() {
72111
</div>
73112
</div>
74113
<div className="max-h-full flex-1 overflow-auto rounded-md border">
75-
<pre
76-
className="text-foreground min-h-full p-5 font-mono text-[13px]
77-
leading-[22px] font-normal"
78-
>
79-
{filteredLogs.length ? (
80-
filteredLogs.map((line, i) => (
81-
<span key={i}>
82-
{highlight(line, search)}
83-
{'\n'}
84-
</span>
85-
))
86-
) : (
87-
<div className="text-muted-foreground">
88-
{search ? 'No logs match your search' : 'No logs available'}
89-
</div>
90-
)}
91-
</pre>
114+
{isLoadingState ? (
115+
<SkeletonLogs />
116+
) : (
117+
<pre
118+
className="text-foreground min-h-full p-5 font-mono text-[13px]
119+
leading-[22px] font-normal"
120+
>
121+
{filteredLogs.length ? (
122+
filteredLogs.map((line, i) => (
123+
<span key={i}>
124+
{highlight(line, search)}
125+
{'\n'}
126+
</span>
127+
))
128+
) : (
129+
<div className="text-muted-foreground">
130+
{search ? 'No logs match your search' : 'No logs available'}
131+
</div>
132+
)}
133+
</pre>
134+
)}
92135
</div>
93136
</div>
94137
)

renderer/src/routes/__tests__/logs.$serverName.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ describe('Logs Route', () => {
8888
expect(screen.getByRole('heading', { name: serverName })).toBeVisible()
8989
})
9090

91+
await waitFor(() => {
92+
expect(screen.queryByTestId('skeleton-logs')).not.toBeInTheDocument()
93+
})
94+
9195
expect(
9296
screen.queryByText(/server .* started successfully/i)
9397
).toBeVisible()
@@ -171,6 +175,10 @@ describe('Logs Route', () => {
171175
expect(screen.getByRole('heading', { name: serverName })).toBeVisible()
172176
})
173177

178+
await waitFor(() => {
179+
expect(screen.queryByTestId('skeleton-logs')).not.toBeInTheDocument()
180+
})
181+
174182
expect(screen.getByText('No logs available')).toBeVisible()
175183
})
176184

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { createFileRoute } from '@tanstack/react-router'
22
import { LogsPage } from '@/features/mcp-servers/sub-pages/logs-page'
3-
import { getApiV1BetaWorkloadsByNameLogsOptions } from '@api/@tanstack/react-query.gen'
3+
4+
interface LogsSearch {
5+
remote?: boolean
6+
}
47

58
export const Route = createFileRoute('/logs/$groupName/$serverName')({
6-
loader: async ({ context: { queryClient }, params }) =>
7-
queryClient.ensureQueryData(
8-
getApiV1BetaWorkloadsByNameLogsOptions({
9-
path: { name: params.serverName },
10-
})
11-
),
9+
validateSearch: (search: Record<string, unknown>): LogsSearch => ({
10+
remote: search.remote === true || search.remote === 'true',
11+
}),
1212
component: LogsPage,
1313
})

0 commit comments

Comments
 (0)