Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance query detail page with cluster support and improve UI components #431

Merged
merged 2 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions app/[host]/query/[query_id]/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export const config: QueryConfig = {
query_cache_usage,
query_duration_ms as duration_ms,
query_duration_ms / 1000 as duration,
event_time_microseconds,
query_start_time_microseconds,

-- The time in seconds since this stage started
(now() - query_start_time) as elapsed,
Expand Down Expand Up @@ -136,12 +138,14 @@ export const config: QueryConfig = {
toString(transaction_id) as transaction_id
FROM system.query_log
WHERE
query_id = {query_id: String}
ORDER BY query_start_time DESC
initial_query_id = {query_id: String}
ORDER BY event_time_microseconds
LIMIT 1000
`,
columns: [
'hostname',
'type',
'query_start_time_microseconds',
'readable_elapsed',
'duration_ms',
'readable_memory_usage',
Expand Down
108 changes: 108 additions & 0 deletions app/[host]/query/[query_id]/dropdown-cluster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { ErrorAlert } from '@/components/error-alert'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { fetchData } from '@/lib/clickhouse'

import { getHostIdCookie } from '@/lib/scoped-link'
import { CircleCheckIcon, CombineIcon } from 'lucide-react'
import { PageProps } from './types'

interface RowData {
cluster: string
replica_count: number
}

const sql = `
SELECT DISTINCT
cluster,
count(replica_num) AS replica_count
FROM system.clusters
GROUP BY 1
`

export async function DropdownCluster({
params,
searchParams,
className,
}: {
params: Awaited<PageProps['params']>
searchParams: Awaited<PageProps['searchParams']>
className?: string
}) {
const { query_id } = params
const { cluster } = searchParams
const hostId = await getHostIdCookie()
const path = `/${hostId}/query/${query_id}/`

try {
const { data } = await fetchData<RowData[]>({
query: sql,
format: 'JSONEachRow',
clickhouse_settings: {
use_query_cache: 0,
},
hostId,
})

if (!data.length) {
return null
}

return (
<div className={className}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
{!!cluster ? (
<span className="flex items-center gap-1">
<CircleCheckIcon className="size-3" />
{cluster}
</span>
) : (
<span className="flex items-center gap-1">
<CombineIcon className="size-3" />
Query Across Cluster
</span>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Query Across Cluster</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup value={cluster}>
{data.map((row) => (
<DropdownMenuRadioItem key={row.cluster} value={row.cluster}>
<a
href={
cluster === row.cluster
? path
: `${path}?cluster=${row.cluster}`
}
>
{row.cluster} ({row.replica_count} replicas)
</a>
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
} catch (error) {
return (
<ErrorAlert
title="ClickHouse Query Error"
message={`${error}`}
query={sql}
/>
)
}
}
14 changes: 13 additions & 1 deletion app/[host]/query/[query_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const revalidate = 300

export default async function Page({ params, searchParams }: PageProps) {
const { query_id } = await params
const { cluster } = await searchParams

// Binding the query_id to the config
const queryConfig = {
Expand All @@ -21,14 +22,25 @@ export default async function Page({ params, searchParams }: PageProps) {
},
}

if (cluster) {
queryConfig.sql = queryConfig.sql.replace(
'FROM system.query_log',
`FROM clusterAllReplicas('${cluster}', system.query_log)`
)
}

return (
<div className="flex flex-col gap-4">
<Suspense fallback={<ChartSkeleton />}>
<RelatedCharts relatedCharts={queryConfig.relatedCharts} />
</Suspense>

<Suspense fallback={<TableSkeleton />}>
<QueryDetail queryConfig={queryConfig} params={await params} />
<QueryDetail
queryConfig={queryConfig}
params={await params}
searchParams={await searchParams}
/>
</Suspense>

<Suspense fallback={<TableSkeleton />}>
Expand Down
58 changes: 58 additions & 0 deletions app/[host]/query/[query_id]/query-detail-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Badge } from '@/components/ui/badge'
import { fetchData } from '@/lib/clickhouse'
import { getHostIdCookie } from '@/lib/scoped-link'
import { QueryConfig } from '@/types/query-config'
import { type RowData } from './config'
import { PageProps } from './types'

export async function QueryDetailBadge({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Consider lifting data fetching to parent component to avoid duplicate queries

This component and QueryDetailCard are fetching the same data. Consider fetching once in a parent component and passing the data down to both children to improve performance.

export function QueryDetailBadge({
  queryConfig,
  params,
  queryData,
}: {
  queryConfig: RowData
  params: PageProps
  queryData: any
}) {

queryConfig,
params,
}: {
queryConfig: QueryConfig
params: Awaited<PageProps['params']>
searchParams: Awaited<PageProps['searchParams']>
}) {
try {
const queryParams = {
...queryConfig.defaultParams,
...params,
}
const { data } = await fetchData<RowData[]>({
query: queryConfig.sql,
format: 'JSONEachRow',
query_params: queryParams,
clickhouse_settings: {
use_query_cache: 0,
...queryConfig.clickhouseSettings,
},
hostId: await getHostIdCookie(),
})

if (!data.length) {
return <div className="text-xs text-muted-foreground">No data</div>
}

const { user } = data[0]
const finalType = data[data.length - 1].type
const query_duration_ms = data
.map((row) => parseInt(row.duration_ms))
.reduce((a, b) => a + b, 0)

return (
<>
<Badge className="ml-2" variant="outline" title="Query Duration (ms)">
{query_duration_ms} ms
</Badge>
<Badge className="ml-2" variant="outline" title="Query Type">
{finalType || 'Unknown'}
</Badge>
<Badge className="ml-2" variant="outline" title="User">
{user || 'Unknown'}
</Badge>
</>
)
} catch (error) {
return null
}
}
Loading
Loading