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(pci-databases-analytics) - refactor data table and add filtering #14524

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"status-PENDING": "Ausstehend",
"status-UPDATING": "Update",
"status-ERROR": "Fehler",
"status-ERROR_INCONSISTENT_SPEC": "Fehler",
"status-ERROR_INCONSISTENT_SPEC": "SPEC-Fehler",
"status-READY": "Verfügbar"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"status-PENDING": "Queued",
"status-UPDATING": "Updating",
"status-ERROR": "Error",
"status-ERROR_INCONSISTENT_SPEC": "Error",
"status-ERROR_INCONSISTENT_SPEC": "spec error",
"status-READY": "Available"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"status-PENDING": "Pendiente",
"status-UPDATING": "Actualizada",
"status-ERROR": "En error",
"status-ERROR_INCONSISTENT_SPEC": "En error",
"status-ERROR_INCONSISTENT_SPEC": "Error spec",
"status-READY": "Disponible"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"status-PENDING": "En attente",
"status-UPDATING": "Mise à jour",
"status-ERROR": "En erreur",
"status-ERROR_INCONSISTENT_SPEC": "En erreur",
"status-ERROR_INCONSISTENT_SPEC": "Erreur spec",
"status-READY": "Disponible"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"status-PENDING": "En attente",
"status-UPDATING": "Mise à jour",
"status-ERROR": "En erreur",
"status-ERROR_INCONSISTENT_SPEC": "En erreur",
"status-ERROR_INCONSISTENT_SPEC": "Erreur spec",
"status-READY": "Disponible"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"status-PENDING": "In attesa",
"status-UPDATING": "Aggiornamento",
"status-ERROR": "In errore",
"status-ERROR_INCONSISTENT_SPEC": "In errore",
"status-ERROR_INCONSISTENT_SPEC": "Errore specifico",
"status-READY": "Disponibile"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"status-PENDING": "Oczekujące",
"status-UPDATING": "Aktualizacja",
"status-ERROR": "Błąd",
"status-ERROR_INCONSISTENT_SPEC": "Błąd",
"status-ERROR_INCONSISTENT_SPEC": "Błąd spec",
"status-READY": "Dostępny"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"status-PENDING": "Em espera",
"status-UPDATING": "Atualização",
"status-ERROR": "Erro",
"status-ERROR_INCONSISTENT_SPEC": "Erro",
"status-ERROR_INCONSISTENT_SPEC": "Erro spec",
"status-READY": "Disponível"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ import { PointerEvent } from './helpers/pointerEvent';
// it is requiered for DropdownMenus
// source: https://github.com/radix-ui/primitives/issues/856#issuecomment-928704064
window.PointerEvent = PointerEvent as any;

const originalConsoleError = console.error;

console.error = (...args) => {
if (typeof args[0] === 'string' && args[0].includes('connect ECONNREFUSED')) {
return;
}
originalConsoleError(...args);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { ReactElement, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { flexRender } from '@tanstack/react-table';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useDataTableContext } from './DataTable.context';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '../ui/button';

export const MENU_COLUMN_ID = 'actions_menu_column';

interface DatatableProps<TData> {
renderRowExpansion?: (row: TData) => ReactElement | null;
}

export function DataTable<TData>({
renderRowExpansion,
}: DatatableProps<TData>) {
const { table, rows } = useDataTableContext();
const { t } = useTranslation('pci-databases-analytics/components/data-table');
const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({});

const toggleRowExpansion = (rowId: string) => {
setExpandedRows((prev) => ({
...prev,
[rowId]: !prev[rowId],
}));
};

const headerGroups = table.getHeaderGroups();
return (
<Table>
<TableHeader className="border bg-gray-50">
{headerGroups.map((headerGroup) => (
<TableRow key={headerGroup.id}>
{renderRowExpansion && (
<TableHead className="border-r-0 w-6"></TableHead>
)}
{headerGroup.headers.map((header, index) => {
const isEmptyHeader = header.id === MENU_COLUMN_ID;
// Get a reference to the previous header
const isEmptyNextHeader =
headerGroup.headers[index + 1]?.id === MENU_COLUMN_ID;
return (
<TableHead
key={header.id}
className={`border font-semibold text-primary-800 ${
isEmptyHeader || renderRowExpansion
? 'border-l-0' // Remove left border for empty headers and row extend column
: ''
} ${
isEmptyNextHeader
? 'border-r-0' // Remove right border from current column if next header is empty
: ''
}`}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody className="border">
{rows?.length ? (
rows.map((row) => (
<React.Fragment key={row.id}>
<TableRow data-state={row.getIsSelected() && 'selected'}>
{renderRowExpansion && (
<TableCell>
<Button
variant="ghost"
onClick={() => toggleRowExpansion(row.id)}
data-testid="table-row-expand-button"
>
{expandedRows[row.id] ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</TableCell>
)}
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
{expandedRows[row.id] && renderRowExpansion && (
<TableRow>
<TableCell colSpan={headerGroups[0].headers.length + 1}>
{renderRowExpansion(row.original as TData)}
</TableCell>
</TableRow>
)}
</React.Fragment>
))
) : (
<TableRow>
<TableCell
colSpan={headerGroups[0].headers.length}
className="h-24 text-center"
>
{t('noResult')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
ColumnDef,
Row,
SortingState,
Table,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { ReactNode, createContext, useContext, useMemo, useState } from 'react';
import { useColumnFilters } from './useColumnFilters.hook';
import { applyFilters } from '@/lib/filters';
import { ColumnFilter } from './DatatableDefaultFilterButton.component';
import { DataTable } from './DataTable.component';
import { DataTablePagination } from './DatatablePagination.component';

interface DataTableProviderProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageSize?: number;
itemNumber?: number;
filtersDefinition?: ColumnFilter[];
children?: ReactNode;
}

interface DataTableContextValue<TData> {
table: Table<TData>;
filtersDefinition?: ColumnFilter[];
columnFilters: ReturnType<typeof useColumnFilters>;
globalFilter: string;
data: TData[];
filteredData: TData[];
sorting: SortingState;
rows: Row<TData>[];
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const DataTableContext = createContext<DataTableContextValue<any> | null>(null);

export function DataTableProvider<TData, TValue>({
columns,
data,
pageSize,
filtersDefinition,
children,
}: DataTableProviderProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([
{
id: columns[0]?.id as string,
desc: false,
},
]);
const [globalFilter, setGlobalFilter] = useState<string>('');
const columnFilters = useColumnFilters();

const filteredData = useMemo(
() => applyFilters(data || [], columnFilters.filters) as TData[],
[columnFilters.filters, data],
);
const table = useReactTable({
data: filteredData,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
state: {
sorting,
globalFilter,
},
initialState: {
pagination: { pageSize: pageSize ?? 5 },
},
onGlobalFilterChange: (e) => {
setGlobalFilter(e);
},
globalFilterFn: 'auto',
});

const rows = useMemo(() => table.getRowModel()?.rows, [
table,
globalFilter,
columnFilters.filters,
data,
sorting,
]);

const contextValue: DataTableContextValue<TData> = {
table,
filtersDefinition,
columnFilters,
globalFilter,
data,
filteredData,
sorting,
rows,
};

return (
<DataTableContext.Provider value={contextValue}>
{children || (
<>
<DataTable />
<DataTablePagination />
</>
)}
</DataTableContext.Provider>
);
}

export function useDataTableContext<TData>() {
const context = useContext<DataTableContextValue<TData>>(DataTableContext);
if (!context) {
throw new Error(
'useDataTableContext must be used within a DataTableProvider',
);
}
return context as DataTableContextValue<TData>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ReactNode } from 'react';

export function DatatableAction({ children }: { children: ReactNode }) {
return <>{children || <></>}</>;
}
Loading
Loading