Skip to content

Commit

Permalink
Merge pull request #422 from duyet/feat/new-table-expolerer
Browse files Browse the repository at this point in the history
feat: update /database explorer; enhance ColoredBadgeFormat with `options.className`; introduce `ListSkeleton` component
  • Loading branch information
duyet authored Nov 18, 2024
2 parents 16c0ea0 + e50c92e commit ec82462
Show file tree
Hide file tree
Showing 15 changed files with 431 additions and 199 deletions.
4 changes: 2 additions & 2 deletions app/[host]/database/[database]/@nav/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MultiLineSkeleton } from '@/components/skeleton'
import { ListSkeleton } from '@/components/skeleton'

export default function Loading() {
return <MultiLineSkeleton className="w-[250px]" />
return <ListSkeleton className="w-full p-2" nrows={5} />
}
63 changes: 45 additions & 18 deletions app/[host]/database/[database]/@nav/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ErrorAlert } from '@/components/error-alert'
import { fetchData } from '@/lib/clickhouse'
import { cn } from '@/lib/utils'

import { cache } from 'react'
import { listDatabases } from '../../queries'
import { DatabaseBreadcrumb } from './breadcrumb'

Expand All @@ -21,25 +22,32 @@ interface DatabaseCount {
count: number
}

export const getListDatabaseCached = cache(async () => {
return fetchData({
query: listDatabases,
clickhouse_settings: {
use_query_cache: 1,
query_cache_system_table_handling: 'save',
query_cache_ttl: 300,
},
})
})

export const preload = async (host: number) => {
void (await getListDatabaseCached())
}

export async function Nav({ host, database, collapsible }: Props) {
preload(host)
let databases: DatabaseCount[] = []

try {
// List database names and number of tables
const data = (await fetchData({
query: listDatabases,
clickhouse_settings: {
use_query_cache: 1,
query_cache_system_table_handling: 'save',
query_cache_ttl: 300,
},
})) satisfies { data: DatabaseCount[] }

databases = data.data
const data = await getListDatabaseCached()
databases = data.data as DatabaseCount[]
} catch (e: any) {
return (
<ErrorAlert
title="Breadcrumb: could not getting list database"
title="Breadcrumb: could not get list database"
message={`${e}`}
query={listDatabases}
/>
Expand Down Expand Up @@ -93,8 +101,8 @@ function Sidebar({
isCollapsed?: boolean
}) {
return (
<div className="">
<div className="space-y-1">
<div className="flex h-full flex-col bg-sidebar p-2 text-sidebar-foreground">
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden">
{databases.map((db) => (
<Link
key={db.name}
Expand All @@ -104,17 +112,36 @@ function Sidebar({
'hover:bg-accent hover:text-accent-foreground',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
'disabled:pointer-events-none disabled:opacity-50',
db.name === current && 'bg-secondary'
db.name === current && 'bg-secondary font-bold'
)}
>
<div className="inline-flex w-full items-center justify-start gap-2 p-2">
<div className="inline-flex w-full items-center justify-start gap-2 p-1">
<Database className="m-0 h-4 w-4 flex-none p-0" />
<span className="flex-1 overflow-hidden truncate">{db.name}</span>
<span className="ml-auto overflow-hidden">({db.count})</span>
<span
className={cn(
'flex-1 overflow-hidden truncate',
db.name === current && 'font-semibold'
)}
>
{db.name}
</span>
<Count>{db.count}</Count>
</div>
</Link>
))}
</div>
</div>
)
}

const Count = ({ children }: { children: React.ReactNode }) => (
<span
className={cn(
'ml-auto overflow-hidden border-transparent',
'inline-flex items-center rounded-full border px-1.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'bg-gray-200 text-primary-foreground hover:bg-primary/80'
)}
>
{children}
</span>
)
18 changes: 8 additions & 10 deletions app/[host]/database/[database]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from '@/components/resizable'
import { cn } from '@/lib/utils'

type Params = Promise<{
host: number
Expand All @@ -23,22 +22,21 @@ export default async function TableListPage({ nav, children }: TableListProps) {
return (
<ResizablePanelGroup
direction="horizontal"
className="h-full min-h-screen items-stretch gap-4"
className="h-full min-h-screen items-stretch"
>
<ResizablePanel
defaultSize={10}
collapsible={true}
minSize={5}
maxSize={25}
className={cn('min-w-[35px] transition-all duration-300 ease-in-out')}
defaultSize={17}
minSize={17}
maxSize={35}
className="rounded-l bg-sidebar"
>
<div>{nav}</div>
{nav}
</ResizablePanel>

<ResizableHandle withHandle />

<ResizablePanel minSize={30} defaultSize={90}>
<div>{children}</div>
<ResizablePanel minSize={30} defaultSize={90} className="p-4">
{children}
</ResizablePanel>
</ResizablePanelGroup>
)
Expand Down
7 changes: 5 additions & 2 deletions app/[host]/database/[database]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ export default async function TableListPage({ params }: TableListProps) {
],
table: [
ColumnFormat.Link,
{ href: `/${host}/database/${database}/[table]` },
{
href: `/${host}/database/${database}/[table]`,
className: 'truncate max-w-48',
},
],
engine: ColumnFormat.ColoredBadge,
engine: [ColumnFormat.ColoredBadge, { className: 'truncate max-w-40' }],
readable_compressed: ColumnFormat.BackgroundBar,
readable_uncompressed: ColumnFormat.BackgroundBar,
readable_total_rows: ColumnFormat.BackgroundBar,
Expand Down
17 changes: 17 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@
--chart-orange-600: 20.5, 90.2%, 48.2%;
--chart-indigo-300: 230, 94%, 82%;
--chart-yellow: 45 100% 60%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;

}

.dark {
Expand Down Expand Up @@ -70,5 +79,13 @@
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
26 changes: 23 additions & 3 deletions components/data-table/cells/colored-badge-format.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,28 @@ describe('<ColoredBadgeFormat />', () => {
cy.get('span').should('not.exist')
})

it('applies additional className', () => {
cy.mount(<ColoredBadgeFormat value="Test" className="extra-class" />)
cy.get('span').should('have.class', 'extra-class')
describe('options.className', () => {
it('applies custom className from options', () => {
cy.mount(
<ColoredBadgeFormat
value="Test"
options={{ className: 'extra-class' }}
/>
)
cy.get('span').should('have.class', 'extra-class')
})

it('applies className from options with override', () => {
cy.mount(
<ColoredBadgeFormat
value="Test"
options={{ className: 'w-5 w-10 w-15' }}
/>
)
cy.get('span')
.should('not.have.class', 'w-5')
.and('not.have.class', 'w-10')
.and('have.class', 'w-15')
})
})
})
12 changes: 8 additions & 4 deletions components/data-table/cells/colored-badge-format.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { cn } from '@/lib/utils'

export interface ColoredBadgeOptions {
className?: string
}

interface ColoredBadgeFormatProps {
value: any
className?: string
options?: ColoredBadgeOptions
}

export function ColoredBadgeFormat({
value,
className,
options,
}: ColoredBadgeFormatProps) {
if (!value || value === '') {
return
Expand Down Expand Up @@ -36,9 +40,9 @@ export function ColoredBadgeFormat({
return (
<span
className={cn(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
'inline-block rounded-full px-2.5 py-0.5 text-xs font-medium',
pickedColor,
className
options?.className
)}
>
{value}
Expand Down
58 changes: 58 additions & 0 deletions components/data-table/cells/link-format.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,62 @@ describe('<LinkFormat />', () => {
cy.get('a').should('have.attr', 'href', 'https://duyet.net/item/789')
cy.contains('Non-string Href').should('be.visible')
})

describe('options.className', () => {
it('applies custom className from options', () => {
const row = { index: 0 } as Row<any>
const data = [{ id: '123' }]
const value = 'Custom Class'
const options = {
href: '/item/[id]',
className: 'custom-class text-red-500',
}

cy.mount(
<LinkFormat row={row} data={data} value={value} options={options} />
)

cy.get('a')
.should('have.class', 'custom-class')
.and('have.class', 'text-red-500')
})

it('merges custom className with default classes', () => {
const row = { index: 0 } as Row<any>
const data = [{ id: '123' }]
const value = 'Merged Classes'
const options = {
href: '/item/[id]',
className: 'custom-class',
}

cy.mount(
<LinkFormat row={row} data={data} value={value} options={options} />
)

cy.get('a')
.should('have.class', 'group')
.and('have.class', 'flex')
.and('have.class', 'custom-class')
})

it('applies custom className from options with override', () => {
const row = { index: 0 } as Row<any>
const data = [{ id: '123' }]
const value = 'Custom Class'
const options = {
href: '/item/[id]',
className: 'text-red-300 text-red-400 text-red-500',
}

cy.mount(
<LinkFormat row={row} data={data} value={value} options={options} />
)

cy.get('a')
.should('not.have.class', 'text-red-300')
.and('not.have.class', 'text-red-400')
.and('have.class', 'text-red-500')
})
})
})
13 changes: 9 additions & 4 deletions components/data-table/cells/link-format.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cn } from '@/lib/utils'
import { ArrowRightIcon } from '@radix-ui/react-icons'
import { Row, RowData } from '@tanstack/react-table'
import Link, { LinkProps } from 'next/link'
Expand All @@ -9,14 +10,14 @@ interface LinkFormatProps<
row: Row<TData>
data: TData[]
value: TValue
options?: LinkProps
options?: LinkProps & { className?: string }
}

export function LinkFormat<
TData extends RowData,
TValue extends React.ReactNode,
>({ row, data, value, options }: LinkFormatProps<TData, TValue>) {
let href = options?.href
let { href, className, ...rest } = options ?? {}

// No href provided, return value as is
if (!href) return value
Expand All @@ -43,8 +44,12 @@ export function LinkFormat<
}

return (
<Link href={hrefBinding} className="group flex flex-row items-center gap-1">
<span className="text-nowrap">{value}</span>
<Link
href={hrefBinding}
className={cn('group flex flex-row items-center gap-1', className)}
{...rest}
>
<span className="truncate text-nowrap">{value}</span>
<ArrowRightIcon className="size-3 text-transparent group-hover:text-current" />
</Link>
)
Expand Down
12 changes: 10 additions & 2 deletions components/data-table/format-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
CodeToggleFormat,
type CodeToggleOptions,
} from './cells/code-toggle-format'
import { ColoredBadgeFormat } from './cells/colored-badge-format'
import {
ColoredBadgeFormat,
ColoredBadgeOptions,
} from './cells/colored-badge-format'
import { DurationFormat } from './cells/duration-format'
import {
HoverCardFormat,
Expand Down Expand Up @@ -55,7 +58,12 @@ export const formatCell = <
)

case ColumnFormat.ColoredBadge:
return <ColoredBadgeFormat value={value} />
return (
<ColoredBadgeFormat
value={value}
options={columnFormatOptions as ColoredBadgeOptions}
/>
)

case ColumnFormat.Code:
return <code>{value as string}</code>
Expand Down
Loading

1 comment on commit ec82462

@vercel
Copy link

@vercel vercel bot commented on ec82462 Nov 18, 2024

Choose a reason for hiding this comment

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

Please sign in to comment.