-
Notifications
You must be signed in to change notification settings - Fork 0
control and data plane decoupling and refactor #11
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,103 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React, { Fragment } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { flexRender, type Table as ReactTable } from '@tanstack/react-table'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Skeleton } from '@/components/ui/skeleton'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface DataTableProps<TData> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table: ReactTable<TData>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| columnsLength: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isLoading?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dataLength: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emptyIcon?: React.ReactNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emptyTitle?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emptyDescription?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| renderExpandedRow?: (row: import('@tanstack/react-table').Row<TData>) => React.ReactNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function DataTable<TData>({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| columnsLength, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isLoading, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dataLength, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emptyIcon, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emptyTitle = "No Data", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emptyDescription, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| renderExpandedRow, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: DataTableProps<TData>) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isLoading) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="bg-white border border-slate-200/60 rounded-2xl shadow-sm overflow-hidden flex flex-col"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-8 space-y-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {[1, 2, 3].map((i) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Skeleton key={i} className="h-12 w-full rounded-xl bg-slate-50" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (dataLength === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="bg-white rounded-2xl border border-slate-200/60 shadow-sm p-12 text-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-16 h-16 mx-auto bg-slate-50 rounded-2xl border border-slate-100 flex items-center justify-center mb-4 text-slate-400 shadow-sm"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {emptyIcon} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h3 className="text-lg font-semibold text-slate-900 mb-1">{emptyTitle}</h3> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {emptyDescription && <p className="text-sm text-slate-500 mt-1">{emptyDescription}</p>} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="bg-white border border-slate-200/60 rounded-2xl shadow-sm overflow-hidden flex flex-col"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="overflow-x-auto"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <table className="w-full text-left border-collapse text-sm"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <thead> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {table.getHeaderGroups().map((headerGroup) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <tr key={headerGroup.id} className="border-b border-slate-200/60 bg-slate-50/50"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {headerGroup.headers.map((header) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <th key={header.id} className="px-6 py-4 text-xs font-semibold text-slate-500 uppercase tracking-wider text-left align-middle"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {header.isPlaceholder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : flexRender(header.column.columnDef.header, header.getContext())} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </th> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </tr> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </thead> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <tbody className="divide-y divide-slate-100"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {table.getRowModel().rows.map((row) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Fragment key={row.id}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <tr | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={`hover:bg-slate-50/50 transition-colors group ${renderExpandedRow ? 'cursor-pointer' : ''} ${row.getIsExpanded() ? 'bg-slate-50/50' : ''}`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={renderExpandedRow ? () => row.toggleExpanded() : undefined} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onKeyDown={renderExpandedRow ? (e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const target = e.target as HTMLElement; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (target.tagName === 'BUTTON' || target.tagName === 'A' || target.tagName === 'INPUT') return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.key === 'Enter' || e.key === ' ') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| row.toggleExpanded(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } : undefined} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tabIndex={renderExpandedRow ? 0 : undefined} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {row.getVisibleCells().map((cell) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <td key={cell.id} className="px-6 py-4 align-middle"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {flexRender(cell.column.columnDef.cell, cell.getContext())} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </td> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </tr> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Expandable rows are not keyboard-accessible. The row toggle is only wired to ♿ Proposed fix <tr
className={`hover:bg-slate-50/50 transition-colors group ${renderExpandedRow ? 'cursor-pointer' : ''} ${row.getIsExpanded() ? 'bg-slate-50/50' : ''}`}
onClick={renderExpandedRow ? () => row.toggleExpanded() : undefined}
+ role={renderExpandedRow ? 'button' : undefined}
+ tabIndex={renderExpandedRow ? 0 : undefined}
+ onKeyDown={renderExpandedRow ? (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ row.toggleExpanded();
+ }
+ } : undefined}
>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {renderExpandedRow && row.getIsExpanded() && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <tr> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <td colSpan={columnsLength} className="p-0"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {renderExpandedRow(row)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </td> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </tr> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Fragment> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </tbody> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </table> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import type { ReactNode } from 'react'; | ||
| import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; | ||
| import { Button } from '@/components/ui/button'; | ||
| import { Plus } from 'lucide-react'; | ||
|
|
||
| interface FormModalProps { | ||
| title: string; | ||
| triggerText: string; | ||
| triggerIcon?: ReactNode; | ||
| icon?: ReactNode; | ||
| isOpen: boolean; | ||
| onOpenChange: (open: boolean) => void; | ||
| onSubmit: (e: React.FormEvent<HTMLFormElement>) => void; | ||
| isPending: boolean; | ||
| submitText?: string; | ||
| children: ReactNode; | ||
| submitDisabled?: boolean; | ||
| maxWidth?: string; | ||
| footerContent?: ReactNode; | ||
| } | ||
|
|
||
| export function FormModal({ | ||
| title, | ||
| triggerText, | ||
| triggerIcon = <Plus className="h-4 w-4" />, | ||
| icon, | ||
| isOpen, | ||
| onOpenChange, | ||
| onSubmit, | ||
| isPending, | ||
| submitText = 'Save', | ||
| children, | ||
| submitDisabled = false, | ||
| maxWidth = 'sm:max-w-[600px]', | ||
| footerContent | ||
| }: FormModalProps) { | ||
| return ( | ||
| <Dialog open={isOpen} onOpenChange={onOpenChange}> | ||
| <DialogTrigger asChild> | ||
| <Button className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl shadow-sm"> | ||
| {triggerIcon} | ||
| {triggerText} | ||
| </Button> | ||
| </DialogTrigger> | ||
|
|
||
| <DialogContent className={`${maxWidth} rounded-2xl`} onPointerDownOutside={(e) => e.preventDefault()}> | ||
| <DialogHeader> | ||
| <DialogTitle className="text-xl flex items-center gap-2"> | ||
| {icon && ( | ||
| <div className="w-8 h-8 rounded-lg bg-indigo-50 flex items-center justify-center text-indigo-600"> | ||
| {icon} | ||
| </div> | ||
| )} | ||
| {title} | ||
| </DialogTitle> | ||
| </DialogHeader> | ||
|
|
||
| <form onSubmit={onSubmit} className="grid gap-6 py-4"> | ||
| {children} | ||
|
|
||
| <div className="flex justify-between items-center mt-2"> | ||
| <div> | ||
| {footerContent} | ||
| </div> | ||
| <Button | ||
| type="submit" | ||
| disabled={isPending || submitDisabled} | ||
| className="h-11 px-8 text-base font-semibold shadow-sm rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white disabled:opacity-50" | ||
| > | ||
| {isPending ? 'Saving...' : submitText} | ||
| </Button> | ||
| </div> | ||
| </form> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.