diff --git a/README.md b/README.md index 7f845e20..f420e33e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Build and Test](https://github.com/duyet/clickhouse-monitoring/actions/workflows/ci.yml/badge.svg)](https://github.com/duyet/clickhouse-monitoring/actions/workflows/ci.yml) [![All-time uptime](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fduyet%2Fuptime%2FHEAD%2Fapi%2Fclickhouse-monitoring-vercel-app%2Fuptime.json)](https://duyet.github.io/uptime/history/clickhouse-monitoring-vercel-app) - The simple Next.js dashboard that relies on `system.*` tables to help monitor and provide an overview of your ClickHouse cluster. Features: diff --git a/app/[host]/[query]/queries/running-queries.ts b/app/[host]/[query]/queries/running-queries.ts index 2680c925..726209c5 100644 --- a/app/[host]/[query]/queries/running-queries.ts +++ b/app/[host]/[query]/queries/running-queries.ts @@ -5,7 +5,7 @@ export const runningQueriesConfig: QueryConfig = { name: 'running-queries', sql: ` SELECT *, - multiIf (elapsed < 30, 'a few seconds', + multiIf (elapsed < 30, format('{} seconds', round(elapsed, 1)), elapsed < 90, 'a minute', formatReadableTimeDelta(elapsed, 'days', 'minutes')) as readable_elapsed, round(100 * elapsed / max(elapsed) OVER ()) AS pct_elapsed, diff --git a/components/charts/merge-count.tsx b/components/charts/merge-count.tsx index 3f30b8ef..e0179109 100644 --- a/components/charts/merge-count.tsx +++ b/components/charts/merge-count.tsx @@ -39,6 +39,7 @@ export async function ChartMergeCount({ diff --git a/components/charts/replication-queue-count.tsx b/components/charts/replication-queue-count.tsx index 003331ea..a461f833 100644 --- a/components/charts/replication-queue-count.tsx +++ b/components/charts/replication-queue-count.tsx @@ -1,6 +1,6 @@ import { type ChartProps } from '@/components/charts/chart-props' +import { CardMultiMetrics } from '@/components/generic-charts/card-multi-metrics' import { ChartCard } from '@/components/generic-charts/chart-card' -import { CardMultiMetrics } from '@/components/tremor/card-multi-metrics' import { fetchData } from '@/lib/clickhouse' import { cn } from '@/lib/utils' diff --git a/components/charts/summary-used-by-merges.tsx b/components/charts/summary-used-by-merges.tsx index a550de09..28cccbab 100644 --- a/components/charts/summary-used-by-merges.tsx +++ b/components/charts/summary-used-by-merges.tsx @@ -2,11 +2,11 @@ import { ArrowRightIcon } from '@radix-ui/react-icons' import Link from 'next/link' import { type ChartProps } from '@/components/charts/chart-props' -import { ChartCard } from '@/components/generic-charts/chart-card' import { CardMultiMetrics, type CardMultiMetricsProps, -} from '@/components/tremor/card-multi-metrics' +} from '@/components/generic-charts/card-multi-metrics' +import { ChartCard } from '@/components/generic-charts/chart-card' import { fetchData } from '@/lib/clickhouse' import { getScopedLink } from '@/lib/scoped-link' @@ -181,13 +181,15 @@ export async function ChartSummaryUsedByMerges({
- {rowsReadWritten.readable_rows_read} rows read,{' '} - {used.readable_memory_usage} memory used for merges - - - - +
+
{rowsReadWritten.readable_rows_read} rows read
+
+ {used.readable_memory_usage} memory used for merges + + + +
+
} items={items} className="p-2" diff --git a/components/charts/summary-used-by-mutations.tsx b/components/charts/summary-used-by-mutations.tsx index 219ee551..e82aa63d 100644 --- a/components/charts/summary-used-by-mutations.tsx +++ b/components/charts/summary-used-by-mutations.tsx @@ -1,9 +1,6 @@ import { type ChartProps } from '@/components/charts/chart-props' +import { CardMultiMetrics } from '@/components/generic-charts/card-multi-metrics' import { ChartCard } from '@/components/generic-charts/chart-card' -import { - CardMultiMetrics, - type CardMultiMetricsProps, -} from '@/components/tremor/card-multi-metrics' import { fetchData } from '@/lib/clickhouse' export async function ChartSummaryUsedByMutations({ @@ -22,8 +19,6 @@ export async function ChartSummaryUsedByMutations({ >({ query }) const count = data?.[0] || { running_count: 0 } - const items: CardMultiMetricsProps['items'] = [] - return (
@@ -33,7 +28,6 @@ export async function ChartSummaryUsedByMutations({ {count.running_count} running mutations } - items={items} className="p-2" />
diff --git a/components/charts/summary-used-by-running-queries.tsx b/components/charts/summary-used-by-running-queries.tsx index 7a6ef58f..4341b3ae 100644 --- a/components/charts/summary-used-by-running-queries.tsx +++ b/components/charts/summary-used-by-running-queries.tsx @@ -2,11 +2,11 @@ import { ArrowRightIcon } from '@radix-ui/react-icons' import Link from 'next/link' import { type ChartProps } from '@/components/charts/chart-props' -import { ChartCard } from '@/components/generic-charts/chart-card' import { CardMultiMetrics, type CardMultiMetricsProps, -} from '@/components/tremor/card-multi-metrics' +} from '@/components/generic-charts/card-multi-metrics' +import { ChartCard } from '@/components/generic-charts/chart-card' import { fetchData } from '@/lib/clickhouse' import { formatReadableQuantity } from '@/lib/format-readable' import { getScopedLink } from '@/lib/scoped-link' @@ -158,16 +158,18 @@ export async function ChartSummaryUsedByRunningQueries({
- {first.query_count} queries, {first.readable_memory_usage} memory - used for running queries - - - - +
+
{first.query_count} queries
+
+ {first.readable_memory_usage} memory used for running queries + + + +
+
} items={items} className="p-2" diff --git a/components/charts/zookeeper-uptime.tsx b/components/charts/zookeeper-uptime.tsx index a2b60fab..fc3d939d 100644 --- a/components/charts/zookeeper-uptime.tsx +++ b/components/charts/zookeeper-uptime.tsx @@ -4,7 +4,7 @@ import { fetchData } from '@/lib/clickhouse' import { cn } from '@/lib/utils' import { ArrowUpIcon } from '@radix-ui/react-icons' -import { CardMultiMetrics } from '../tremor/card-multi-metrics' +import { CardMultiMetrics } from '../generic-charts/card-multi-metrics' export async function ChartZookeeperUptime({ title = 'Zookeeper Uptime', diff --git a/components/data-table/column-defs.tsx b/components/data-table/column-defs.tsx index 9c8e354d..0f6dad79 100644 --- a/components/data-table/column-defs.tsx +++ b/components/data-table/column-defs.tsx @@ -8,16 +8,24 @@ import type { ColumnDef, Row, RowData, Table } from '@tanstack/react-table' import { formatCell } from '@/components/data-table/format-cell' import { Button } from '@/components/ui/button' import { ColumnFormat, ColumnFormatOptions } from '@/types/column-format' +import { type Icon } from '@/types/icon' import { type QueryConfig } from '@/types/query-config' export type ColumnType = { [key: string]: string } -const formatHeader = (name: string, format: ColumnFormat) => { +const formatHeader = (name: string, format: ColumnFormat, icon?: Icon) => { + const CustomIcon = icon + switch (format) { case ColumnFormat.Action: return
action
default: - return
{name}
+ return ( +
+ {CustomIcon ? : null} + {name} +
+ ) } } @@ -65,7 +73,7 @@ export const getColumnDefs = < variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')} > - {formatHeader(name, columnFormat)} + {formatHeader(name, columnFormat, config.columnIcons?.[name])} {column.getIsSorted() === false ? ( diff --git a/components/generic-charts/card-multi-metrics.cy.tsx b/components/generic-charts/card-multi-metrics.cy.tsx new file mode 100644 index 00000000..154a1835 --- /dev/null +++ b/components/generic-charts/card-multi-metrics.cy.tsx @@ -0,0 +1,93 @@ +import { CardMultiMetrics } from './card-multi-metrics' + +describe('', () => { + const mockItems = [ + { + current: 100, + target: 200, + currentReadable: '100 users', + targetReadable: '200 users', + }, + { + current: 50, + target: 150, + currentReadable: '50 sessions', + targetReadable: '150 sessions', + }, + ] + + it('renders with default props', () => { + cy.mount() + + // Should have aria-description + cy.get('[aria-description="card-metrics"]').should('exist') + + // Should not render labels when no items + cy.contains('Current').should('not.exist') + cy.contains('Total').should('not.exist') + }) + + it('renders with primary content', () => { + cy.mount() + + cy.get('div.text-xl').contains('Dashboard Metrics').should('be.visible') + }) + + it('renders with custom labels and items', () => { + cy.mount( + + ) + + // Labels should be visible when items exist + cy.contains('Active').should('be.visible') + cy.contains('Maximum').should('be.visible') + }) + + it('renders with items data', () => { + cy.mount() + + // Check if readable values are displayed + cy.contains('100 users').should('be.visible') + cy.contains('200 users').should('be.visible') + cy.contains('50 sessions').should('be.visible') + cy.contains('150 sessions').should('be.visible') + + // Check for dotted separators + cy.get('hr.border-dotted').should('have.length', mockItems.length) + }) + + it('renders with custom className', () => { + const customClass = 'custom-test-class' + cy.mount() + + cy.get('[aria-description="card-metrics"]').should( + 'have.class', + customClass + ) + }) + + it('renders with ReactNode as primary content', () => { + cy.mount( + Custom Primary
} + /> + ) + + cy.get('.test-primary').contains('Custom Primary').should('be.visible') + }) + + it('handles empty items array', () => { + cy.mount() + + // Should not render labels when items array is empty + cy.contains('Current').should('not.exist') + cy.contains('Total').should('not.exist') + + // Should not render any separators + cy.get('hr.border-dotted').should('not.exist') + }) +}) diff --git a/components/generic-charts/card-multi-metrics.tsx b/components/generic-charts/card-multi-metrics.tsx new file mode 100644 index 00000000..9bb6e59d --- /dev/null +++ b/components/generic-charts/card-multi-metrics.tsx @@ -0,0 +1,56 @@ +import { cn } from '@/lib/utils' + +export interface CardMultiMetricsProps { + primary?: string | number | React.ReactNode + items?: { + current: number + target: number + currentReadable?: string + targetReadable?: string + }[] + currentLabel?: string + targetLabel?: string + className?: string +} + +export function CardMultiMetrics({ + primary, + items = [], + currentLabel = 'Current', + targetLabel = 'Total', + className, +}: CardMultiMetricsProps) { + return ( +
+
{primary}
+ +
+ {items.length ? ( +
+ {currentLabel} + {targetLabel} +
+ ) : null} + + {items.map((item, i) => { + const _percent = (item.current / item.target) * 100 + + return ( +
+
+ + {item.currentReadable} + +
+ {item.targetReadable} +
+
+ ) + })} +
+
+ ) +} diff --git a/components/tremor/card-multi-metrics.tsx b/components/tremor/card-multi-metrics.tsx deleted file mode 100644 index 236ca10e..00000000 --- a/components/tremor/card-multi-metrics.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' - -import { Metric, ProgressBar, Text } from '@tremor/react' - -export interface CardMultiMetricsProps { - primary?: string | number | React.ReactNode - items: { - current: number - target: number - currentReadable?: string - targetReadable?: string - }[] - className?: string -} - -export function CardMultiMetrics({ - primary, - items, - className, -}: CardMultiMetricsProps) { - return ( -
- {primary} - -
- {items.map((item, i) => { - const percent = (item.current / item.target) * 100 - - return ( -
-
- {item.currentReadable} - {item.targetReadable} -
- -
- ) - })} -
-
- ) -} diff --git a/types/query-config.ts b/types/query-config.ts index f9b170e2..b34d24b0 100644 --- a/types/query-config.ts +++ b/types/query-config.ts @@ -27,6 +27,22 @@ export interface QueryConfig { columnFormats?: { [key: string]: ColumnFormat | ColumnFormatWithArgs } + /** + * Column icons can be specified as React Component name. + * + * Example: + * ```ts + * import { CalendarIcon } from 'lucide-react' + * import { GlobeIcon } from '@radix-ui/react-icons' + * columnIcons: { + * date: CalendarIcon, + * language: GlobeIcon + * } + * ``` + */ + columnIcons?: { + [key: string]: Icon + } relatedCharts?: | string[] | [string, ChartProps][]