Skip to content

Commit

Permalink
feat: action kill query
Browse files Browse the repository at this point in the history
  • Loading branch information
duyet committed Nov 22, 2023
1 parent 86669a4 commit 0e6da97
Show file tree
Hide file tree
Showing 14 changed files with 559 additions and 51 deletions.
3 changes: 2 additions & 1 deletion app/[name]/clickhouse-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ export const queries: Array<QueryConfig> = [
'readable_read_rows',
'readable_total_rows_approx',
'readable_memory_usage',
'query_id',
],
columnFormats: {
query: ColumnFormat.CodeToggle,
elapsed: ColumnFormat.Duration,
query_id: ColumnFormat.None,
query_id: [ColumnFormat.Action, ['kill-query']],
},
relatedCharts: [
[
Expand Down
89 changes: 89 additions & 0 deletions components/data-table/actions/action-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use client'

import { useState } from 'react'
import {
CheckCircledIcon,
ExclamationTriangleIcon,
UpdateIcon,
} from '@radix-ui/react-icons'

import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { useToast } from '@/components/ui/use-toast'

import { killQuery } from './actions'
import type { Action } from './types'

type Message = {
message: string
}

interface ActionButtonProps {
action: Action
value: any
}

export function ActionItem({ action, value }: ActionButtonProps) {
const { toast, dismiss } = useToast()
const [status, updateStatus] = useState<
'none' | 'loading' | 'success' | 'failed'
>('none')

const availableActions: {
[key: string]: { label: string; handler: (_: FormData) => Promise<Message> }
} = {
'kill-query': {
label: 'Kill Query',
handler: killQuery.bind(null, value),
},
}

const { label, handler } = availableActions[action]

return (
<form
action={async (formData: FormData) => {
updateStatus('loading')
toast({ title: 'Message', description: 'Loading...' })

try {
const msg: Message = await handler(formData)
console.debug('Action Response', msg)
updateStatus('success')
toast({ title: 'Message', description: msg.message })
} catch (e) {
updateStatus('failed')
toast({ title: 'Error', description: `${e}`, variant: 'destructive' })
} finally {
dismiss()
}
}}
>
<DropdownMenuItem>
{status == 'loading' && (
<span className="flex flex-row items-center gap-2">
<UpdateIcon className="h-4 w-4 animate-spin" /> {label}
</span>
)}

{status == 'failed' && (
<span className="flex flex-row items-center gap-2">
<ExclamationTriangleIcon className="h-4 w-4 text-orange-500" />{' '}
{label}
</span>
)}

{status == 'success' && (
<span className="flex flex-row items-center gap-2">
<CheckCircledIcon className="h-4 w-4 text-lime-600" /> {label}
</span>
)}

{status == 'none' && (
<button type="submit" className="m-0 border-none p-0">
{label}
</button>
)}
</DropdownMenuItem>
</form>
)
}
38 changes: 38 additions & 0 deletions components/data-table/actions/action-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { MoreHorizontal } from 'lucide-react'

import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'

import { ActionItem } from './action-item'
import type { Action } from './types'

interface ActionButtonProps {
value: any
actions: Action[]
}

export function ActionMenu({ value, actions }: ActionButtonProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
{actions.map((action) => (
<ActionItem key={action} value={value} action={action} />
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
13 changes: 13 additions & 0 deletions components/data-table/actions/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use server'

import { fetchData } from '@/lib/clickhouse'

export async function killQuery(queryId: string) {
console.log('Killing query', queryId)
const res = await fetchData(`KILL QUERY WHERE query_id = '${queryId}'`)
console.log('Killed query', queryId, res)

return {
message: `Killed query ${queryId}`,
}
}
1 change: 1 addition & 0 deletions components/data-table/actions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Action = 'kill-query'
44 changes: 16 additions & 28 deletions components/data-table/cell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CheckCircledIcon, CrossCircledIcon } from '@radix-ui/react-icons'
import { MoreHorizontal } from 'lucide-react'

import dayjs from '@/lib/dayjs'
import { formatReadableQuantity } from '@/lib/format-readable'
Expand All @@ -9,20 +8,19 @@ import {
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { ColumnFormat } from '@/components/data-table/columns'

import { ActionMenu } from './actions/action-menu'
import type { Action } from './actions/types'

const CODE_TRUNCATE_LENGTH = 50

export const formatCell = (row: any, value: any, format: ColumnFormat) => {
export const formatCell = (
row: any,
value: any,
format: ColumnFormat,
columnFormatOptions: Action[] = []
) => {
switch (format) {
case ColumnFormat.Code:
return <code>{value}</code>
Expand All @@ -39,7 +37,12 @@ export const formatCell = (row: any, value: any, format: ColumnFormat) => {
}

return (
<Accordion type="single" collapsible={row.getIsExpanded()}>
<Accordion
type="single"
defaultValue={row.getIsExpanded() ? 'code' : undefined}
collapsible={row.getIsExpanded()}
onValueChange={(value) => row.toggleExpanded(value === 'code')}
>
<AccordionItem value="code" className="border-0">
<AccordionTrigger className="py-0 hover:no-underline">
<code className="truncate break-words font-normal">
Expand Down Expand Up @@ -71,22 +74,7 @@ export const formatCell = (row: any, value: any, format: ColumnFormat) => {
)

case ColumnFormat.Action:
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Kill Query</DropdownMenuItem>
<DropdownMenuItem>Detail</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
return <ActionMenu value={value} actions={columnFormatOptions} />

case ColumnFormat.Badge:
return (
Expand Down
14 changes: 12 additions & 2 deletions components/data-table/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,16 @@ export const getColumnDefs = (config: QueryConfig): ColumnDef<ColumnType>[] => {

return configColumns.map((column) => {
const name = normalizeColumnName(column)
const columnFormat =
const format =
config.columnFormats?.[column] ||
config.columnFormats?.[name] ||
ColumnFormat.None

let [columnFormat, columnFormatOptions] = [format, undefined]
if (Array.isArray(format) && format.length === 2) {
;[columnFormat, columnFormatOptions] = format
}

return {
id: name,
accessorKey: column,
Expand All @@ -78,7 +83,12 @@ export const getColumnDefs = (config: QueryConfig): ColumnDef<ColumnType>[] => {

cell: ({ row, getValue }) => {
const value = getValue()
const formatted = formatCell(row, value, columnFormat)
const formatted = formatCell(
row,
value,
columnFormat,
columnFormatOptions
)

return formatted
},
Expand Down
2 changes: 0 additions & 2 deletions components/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ export function DataTable<TData extends RowData, TValue>({
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
initialColumnVisibility
)
console.log('columnVisibility', columnVisibility)
console.log('columnDefs', columnDefs)

// Sorting
const [sorting, setSorting] = useState<SortingState>([])
Expand Down
Loading

0 comments on commit 0e6da97

Please sign in to comment.