diff --git a/.changeset/four-walls-warn.md b/.changeset/four-walls-warn.md new file mode 100644 index 000000000..5fe6f72e4 --- /dev/null +++ b/.changeset/four-walls-warn.md @@ -0,0 +1,5 @@ +--- +'@portaljs/components': patch +--- + +Created integration with datastore api for table component diff --git a/packages/components/src/components/Table.tsx b/packages/components/src/components/Table.tsx index c069273c3..d48f93919 100644 --- a/packages/components/src/components/Table.tsx +++ b/packages/components/src/components/Table.tsx @@ -6,6 +6,8 @@ import { getFilteredRowModel, getPaginationRowModel, getSortedRowModel, + PaginationState, + Table as ReactTable, useReactTable, } from '@tanstack/react-table'; @@ -25,12 +27,19 @@ import DebouncedInput from './DebouncedInput'; import loadData from '../lib/loadData'; import LoadingSpinner from './LoadingSpinner'; +export type TableData = { cols: {key: string, name: string}[]; data: any[]; total: number }; + export type TableProps = { data?: Array<{ [key: string]: number | string }>; cols?: Array<{ [key: string]: string }>; csv?: string; url?: string; fullWidth?: boolean; + datastoreConfig?: { + dataStoreURI: string; + rowsPerPage?: number; + dataMapperFn: (data) => Promise | TableData; + }; }; export const Table = ({ @@ -39,8 +48,28 @@ export const Table = ({ csv = '', url = '', fullWidth = false, + datastoreConfig, }: TableProps) => { const [isLoading, setIsLoading] = useState(false); + const [pageMap, setPageMap] = useState(new Map()); + const { + dataMapperFn, + dataStoreURI, + rowsPerPage = 10, + } = datastoreConfig ?? {}; + + const [globalFilter, setGlobalFilter] = useState(''); + const [isLoadingPage, setIsLoadingPage] = useState(false); + const [totalOfRows, setTotalOfRows] = useState(0); + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: rowsPerPage, + }); + + const [lastIndex, setLastIndex] = useState(pageSize); + const [startIndex, setStartIndex] = useState(0); + const [hasSorted, setHasSorted] = useState(false); if (csv) { const out = parseCsv(csv); @@ -62,21 +91,56 @@ export const Table = ({ ); }, [data, cols]); - const [globalFilter, setGlobalFilter] = useState(''); + let table: ReactTable; - const table = useReactTable({ - data, - columns: tableCols, - getCoreRowModel: getCoreRowModel(), - state: { - globalFilter, - }, - globalFilterFn: globalFilterFn, - onGlobalFilterChange: setGlobalFilter, - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - }); + if (datastoreConfig) { + useEffect(() => { + setIsLoading(true); + fetch(`${dataStoreURI}&limit=${rowsPerPage}&offset=0`) + .then((res) => res.json()) + .then(async (res) => { + const { data, cols, total } = await dataMapperFn(res); + setData(data); + setCols(cols); + setTotalOfRows(Math.ceil(total / rowsPerPage)); + pageMap.set(0, true); + }) + .finally(() => setIsLoading(false)); + }, [dataStoreURI]); + + table = useReactTable({ + data, + pageCount: totalOfRows, + columns: tableCols, + getCoreRowModel: getCoreRowModel(), + state: { + pagination: { pageIndex, pageSize }, + }, + getFilteredRowModel: getFilteredRowModel(), + manualPagination: true, + onPaginationChange: setPagination, + getSortedRowModel: getSortedRowModel(), + }); + + useEffect(() => { + if (!hasSorted) return; + queryDataByText(globalFilter); + }, [table.getState().sorting]); + } else { + table = useReactTable({ + data, + columns: tableCols, + getCoreRowModel: getCoreRowModel(), + state: { + globalFilter, + }, + globalFilterFn: globalFilterFn, + onGlobalFilterChange: setGlobalFilter, + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + } useEffect(() => { if (url) { @@ -91,6 +155,70 @@ export const Table = ({ } }, [url]); + const queryDataByText = (filter) => { + setIsLoadingPage(true); + const sortedParam = getSortParam(); + fetch( + `${dataStoreURI}&limit=${rowsPerPage}&offset=0&q=${filter}${sortedParam}` + ) + .then((res) => res.json()) + .then(async (res) => { + const { data, total = 0 } = await dataMapperFn(res); + setTotalOfRows(Math.ceil(total / rowsPerPage)); + setData(data); + const newMap = new Map(); + newMap.set(0, true); + setPageMap(newMap); + table.setPageIndex(0); + setStartIndex(0); + setLastIndex(pageSize); + }) + .finally(() => setIsLoadingPage(false)); + }; + + const getSortParam = () => { + const sort = table.getState().sorting; + return sort.length == 0 + ? `` + : '&sort=' + + sort + .map( + (x, i) => + `${x.id}${ + i === sort.length - 1 ? (x.desc ? ` desc` : ` asc`) : `,` + }` + ) + .reduce((x1, x2) => x1 + x2); + }; + + const queryPaginatedData = (newPageIndex) => { + let newStartIndex = newPageIndex * pageSize; + setStartIndex(newStartIndex); + setLastIndex(newStartIndex + pageSize); + + if (!pageMap.get(newPageIndex)) pageMap.set(newPageIndex, true); + else return; + + const sortedParam = getSortParam(); + + setIsLoadingPage(true); + fetch( + `${dataStoreURI}&limit=${rowsPerPage}&offset=${ + newStartIndex + pageSize + }&q=${globalFilter}${sortedParam}` + ) + .then((res) => res.json()) + .then(async (res) => { + const { data: responseData } = await dataMapperFn(res); + responseData.forEach((e) => { + data[newStartIndex] = e; + newStartIndex++; + }); + setData([...data]); + }) + .finally(() => setIsLoadingPage(false)); + }; + return isLoading ? (
@@ -99,7 +227,10 @@ export const Table = ({
setGlobalFilter(String(value))} + onChange={(value: any) => { + if (datastoreConfig) queryDataByText(String(value)); + setGlobalFilter(String(value)); + }} className="p-2 text-sm shadow border border-block" placeholder="Search all columns..." /> @@ -114,7 +245,10 @@ export const Table = ({ className: h.column.getCanSort() ? 'cursor-pointer select-none' : '', - onClick: h.column.getToggleSortingHandler(), + onClick: (v) => { + setHasSorted(true); + h.column.getToggleSortingHandler()(v); + }, }} > {flexRender(h.column.columnDef.header, h.getContext())} @@ -135,15 +269,28 @@ export const Table = ({ ))} - {table.getRowModel().rows.map((r) => ( - - {r.getVisibleCells().map((c) => ( - - {flexRender(c.column.columnDef.cell, c.getContext())} - - ))} + {datastoreConfig && isLoadingPage ? ( + + +
+ +
+ - ))} + ) : ( + (datastoreConfig + ? table.getRowModel().rows.slice(startIndex, lastIndex) + : table.getRowModel().rows + ).map((r) => ( + + {r.getVisibleCells().map((c) => ( + + {flexRender(c.column.columnDef.cell, c.getContext())} + + ))} + + )) + )}
@@ -151,7 +298,10 @@ export const Table = ({ className={`w-6 h-6 ${ !table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100' }`} - onClick={() => table.setPageIndex(0)} + onClick={() => { + if (datastoreConfig) queryPaginatedData(0); + table.setPageIndex(0); + }} disabled={!table.getCanPreviousPage()} > @@ -160,7 +310,12 @@ export const Table = ({ className={`w-6 h-6 ${ !table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100' }`} - onClick={() => table.previousPage()} + onClick={() => { + if (datastoreConfig) { + queryPaginatedData(table.getState().pagination.pageIndex - 1); + } + table.previousPage(); + }} disabled={!table.getCanPreviousPage()} > @@ -176,7 +331,11 @@ export const Table = ({ className={`w-6 h-6 ${ !table.getCanNextPage() ? 'opacity-25' : 'opacity-100' }`} - onClick={() => table.nextPage()} + onClick={() => { + if (datastoreConfig) + queryPaginatedData(table.getState().pagination.pageIndex + 1); + table.nextPage(); + }} disabled={!table.getCanNextPage()} > @@ -185,7 +344,11 @@ export const Table = ({ className={`w-6 h-6 ${ !table.getCanNextPage() ? 'opacity-25' : 'opacity-100' }`} - onClick={() => table.setPageIndex(table.getPageCount() - 1)} + onClick={() => { + const pageIndexToNavigate = table.getPageCount() - 1; + if (datastoreConfig) queryPaginatedData(pageIndexToNavigate); + table.setPageIndex(pageIndexToNavigate); + }} disabled={!table.getCanNextPage()} > diff --git a/packages/components/stories/Table.stories.ts b/packages/components/stories/Table.stories.ts index 26f8e60c9..2f551bd74 100644 --- a/packages/components/stories/Table.stories.ts +++ b/packages/components/stories/Table.stories.ts @@ -9,17 +9,22 @@ const meta: Meta = { tags: ['autodocs'], argTypes: { data: { - description: "Data to be displayed in the table, must also set \"cols\" to work." + description: + 'Data to be displayed in the table, must also set "cols" to work.', }, cols: { - description: "Columns to be displayed in the table, must also set \"data\" to work." + description: + 'Columns to be displayed in the table, must also set "data" to work.', }, csv: { - description: "CSV data as string.", + description: 'CSV data as string.', }, url: { - description: "Fetch the data from a CSV file remotely." - } + description: 'Fetch the data from a CSV file remotely.', + }, + datastoreConfig: { + description: `Configuration to use CKAN's datastore API extension integrated with the component`, + }, }, }; @@ -29,7 +34,7 @@ type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args export const FromColumnsAndData: Story = { - name: "Table from columns and data", + name: 'Table from columns and data', args: { data: [ { id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 }, @@ -49,21 +54,40 @@ export const FromColumnsAndData: Story = { }, }; +export const WithDataStoreIntegration: Story = { + name: 'Table with datastore integration', + args: { + datastoreConfig: { + dataStoreURI: `https://www.civicdata.com/api/action/datastore_search?resource_id=46ec0807-31ff-497f-bfa0-f31c796cdee8`, + dataMapperFn: ({ + result, + }: { + result: { fields: { id }[]; records: []; total: number }; + }) => { + return { + data: result.records, + cols: result.fields.map((x) => ({ key: x.id, name: x.id })), + total: result.total, + }; + }, + }, + }, +}; + export const FromRawCSV: Story = { - name: "Table from raw CSV", + name: 'Table from raw CSV', args: { csv: ` Year,Temp Anomaly 1850,-0.418 2020,0.923 - ` - } + `, + }, }; export const FromURL: Story = { - name: "Table from URL", + name: 'Table from URL', args: { - url: "https://raw.githubusercontent.com/datasets/finance-vix/main/data/vix-daily.csv" - } + url: 'https://raw.githubusercontent.com/datasets/finance-vix/main/data/vix-daily.csv', + }, }; -