Internal storage - Pagination for Database table (#5040)

* init

* footer component with pagintion ui basic styles

* pagination:  apply a limit and offset rows through query params

* open create row drawer from footer button

* border color for dark theme footer button

* cleaned

* pagination

* fixes: input value

* moved functions to component level
This commit is contained in:
Arpit 2022-12-26 17:09:12 +05:30 committed by GitHub
parent bfceb37da9
commit 6712bd2fb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 265 additions and 14 deletions

View file

@ -0,0 +1,8 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.390524 1.21767C0.640573 0.967625 0.979711 0.827148 1.33333 0.827148H10.6667C11.0203 0.827148 11.3594 0.967624 11.6095 1.21767C11.8595 1.46772 12 1.80686 12 2.16048V4.82715C12 5.18077 11.8595 5.51991 11.6095 5.76996C11.3594 6.02001 11.0203 6.16048 10.6667 6.16048H1.33333C0.979711 6.16048 0.640573 6.02001 0.390524 5.76996C0.140476 5.51991 0 5.18077 0 4.82715V2.16048C0 1.80686 0.140476 1.46772 0.390524 1.21767ZM10.6667 2.16048H1.33333L1.33333 4.82715H10.6667V2.16048ZM6 7.49381C6.36819 7.49381 6.66667 7.79229 6.66667 8.16048V8.82715H7.33333C7.70152 8.82715 8 9.12562 8 9.49381C8 9.862 7.70152 10.1605 7.33333 10.1605H6.66667V10.8271C6.66667 11.1953 6.36819 11.4938 6 11.4938C5.63181 11.4938 5.33333 11.1953 5.33333 10.8271V10.1605H4.66667C4.29848 10.1605 4 9.862 4 9.49381C4 9.12562 4.29848 8.82715 4.66667 8.82715H5.33333V8.16048C5.33333 7.79229 5.63181 7.49381 6 7.49381Z"
fill="#3E63DD"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-left" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<polyline points="15 6 9 12 15 18" />
</svg>

After

Width:  |  Height:  |  Size: 329 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-right" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<polyline points="9 6 15 12 9 18" />
</svg>

After

Width:  |  Height:  |  Size: 329 B

View file

@ -1,13 +1,12 @@
import React, { useState, useContext } from 'react';
import React, { useContext } from 'react';
import Drawer from '@/_ui/Drawer';
import { toast } from 'react-hot-toast';
import CreateRowForm from '../../Forms/RowForm';
import { TooljetDatabaseContext } from '../../index';
import { tooljetDatabaseService } from '@/_services';
const CreateRowDrawer = () => {
const CreateRowDrawer = ({ isCreateRowDrawerOpen, setIsCreateRowDrawerOpen }) => {
const { organizationId, selectedTable, setSelectedTableData } = useContext(TooljetDatabaseContext);
const [isCreateRowDrawerOpen, setIsCreateRowDrawerOpen] = useState(false);
return (
<>

View file

@ -0,0 +1,119 @@
import React, { useState } from 'react';
import { Button } from '@/_ui/LeftSidebar';
import Select from '@/_ui/Select';
import Pagination from './Paginations';
const Footer = ({ darkMode, openCreateRowDrawer, totalRecords, fetchTableData }) => {
const selectOptions = [
{ label: '50 records', value: '50 per page' },
{ label: '100 records', value: '100 per page' },
{ label: '200 records', value: '200 per page' },
{ label: '500 records', value: '500 per page' },
{ label: '1000 records', value: '1000 per page' },
];
const RecordEnum = Object.freeze({
'50 per page': 50,
'100 per page': 100,
'200 per page': 200,
'500 per page': 500,
'1000 per page': 1000,
});
const [selectedOption, setSelectedOption] = useState('50 per page');
const [pageCount, setPageCount] = useState(1);
const [pageSize, setPageSize] = useState(RecordEnum[selectedOption]);
const handleSelectChange = (value) => {
setSelectedOption(value);
setPageSize(RecordEnum[value]);
setPageCount(1);
fetchTableData(`?limit=${RecordEnum[value]}&offset=0`, RecordEnum[value], 1);
};
const handlePageCountChange = (value) => {
setPageCount(value);
const limit = RecordEnum[selectedOption];
const offset = value === 1 ? 0 : (value - 1) * RecordEnum[selectedOption];
fetchTableData(`?limit=${limit}&offset=${offset}`, limit, value);
};
const gotoNextPage = (fromInput = false, value = null) => {
if (fromInput && value) {
return handlePageCountChange(value);
}
setPageCount((prev) => {
return prev + 1;
});
const limit = RecordEnum[selectedOption];
const offset = pageCount * RecordEnum[selectedOption];
fetchTableData(`?limit=${limit}&offset=${offset}`, limit, pageCount + 1);
};
const gotoPreviousPage = () => {
setPageCount((prev) => {
if (prev - 1 < 1) {
return prev;
}
return prev - 1;
});
const limit = RecordEnum[selectedOption];
const offset = (pageCount - 2) * RecordEnum[selectedOption];
fetchTableData(`?limit=${limit}&offset=${offset}`, limit, pageCount - 1);
};
return (
<div className="toojet-db-table-footer card-footer d-flex align-items-center jet-table-footer justify-content-center">
<div className="table-footer row gx-0">
<div className="col-5">
<Button
onClick={openCreateRowDrawer}
darkMode={darkMode}
size="sm"
styles={{ width: '118px', fontSize: '12px', fontWeight: 700, borderColor: darkMode && 'transparent' }}
>
<Button.Content title={'Add new row'} iconSrc={'assets/images/icons/add-row.svg'} direction="right" />
</Button>
</div>
<div className="col d-flex align-items-center justify-content-end">
<div className="col">
<Pagination
darkMode={darkMode}
gotoNextPage={gotoNextPage}
gotoPreviousPage={gotoPreviousPage}
currentPage={pageCount}
totalPage={pageSize}
/>
</div>
<div className="col mx-2">
<Select
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
options={selectOptions}
value={selectedOption}
search={false}
onChange={(value) => handleSelectChange(value)}
placeholder={'Select page'}
useMenuPortal={false}
menuPlacement="top"
/>
</div>
<div className="col-4 mx-2">
<span>
{pageCount}-{pageSize} of {totalRecords} Records
</span>
</div>
</div>
</div>
</div>
);
};
export default Footer;

View file

@ -0,0 +1,65 @@
import React, { useEffect } from 'react';
import { Button } from '@/_ui/LeftSidebar';
const Pagination = ({ darkMode, gotoNextPage, gotoPreviousPage, currentPage, totalPage }) => {
const [currentPageNumber, setCurrentPageNumber] = React.useState(currentPage);
const handleOnChange = (value) => {
const parsedValue = parseInt(value, 10);
if (parsedValue > 0 && parsedValue <= totalPage && parsedValue !== currentPage) {
gotoNextPage(true, event.target.value);
}
};
useEffect(() => {
setCurrentPageNumber(currentPage);
}, [currentPage]);
return (
<div className="tooljet-db-pagination-container d-flex">
<Button.UnstyledButton
onClick={(event) => {
event.stopPropagation();
gotoPreviousPage();
}}
classNames={darkMode ? 'dark' : 'nothing'}
styles={{ height: '20px', width: '20px' }}
disabled={currentPage === 1}
>
<Button.Content iconSrc={'assets/images/icons/chevron-left.svg'} />
</Button.UnstyledButton>
<div className="d-flex">
<input
type="text"
className="form-control mx-1"
value={currentPageNumber}
onKeyDown={(event) => {
if (event.key === 'Enter') {
handleOnChange(event.target.value);
}
}}
onChange={(event) => {
setCurrentPageNumber(event.target.value);
}}
/>
<span className="mx-1">/ {totalPage}</span>
</div>
<Button.UnstyledButton
onClick={(event) => {
event.stopPropagation();
gotoNextPage();
}}
classNames={darkMode && 'dark'}
styles={{ height: '20px', width: '20px' }}
disabled={currentPage === totalPage}
>
<Button.Content iconSrc={'assets/images/icons/chevron-right.svg'} />
</Button.UnstyledButton>
</div>
);
};
export default Pagination;

View file

@ -1,4 +1,3 @@
/* eslint-disable react/jsx-key */
import React, { useEffect, useState, useContext } from 'react';
import cx from 'classnames';
import { useTable, useRowSelect } from 'react-table';
@ -11,14 +10,17 @@ import Skeleton from 'react-loading-skeleton';
import IndeterminateCheckbox from '@/_ui/IndeterminateCheckbox';
import Drawer from '@/_ui/Drawer';
import EditColumnForm from '../Forms/ColumnForm';
import TableFooter from './Footer';
const Table = () => {
const Table = ({ openCreateRowDrawer }) => {
const { organizationId, columns, selectedTable, selectedTableData, setSelectedTableData, setColumns } =
useContext(TooljetDatabaseContext);
const [isEditColumnDrawerOpen, setIsEditColumnDrawerOpen] = useState(false);
const [selectedColumn, setSelectedColumn] = useState();
const [loading, setLoading] = useState(false);
const [totalRecords, setTotalRecords] = useState(0);
const fetchTableMetadata = () => {
tooljetDatabaseService.viewTable(organizationId, selectedTable).then(({ data = [], error }) => {
if (error) {
@ -40,15 +42,18 @@ const Table = () => {
});
};
const fetchTableData = () => {
const fetchTableData = (queryParams = '', pagesize = 50, pagecount = 1) => {
const defaultQueryParams = `limit=${pagesize}&offset=${(pagecount - 1) * pagesize}`;
const params = queryParams ? queryParams : defaultQueryParams;
setLoading(true);
tooljetDatabaseService.findOne(organizationId, selectedTable).then(({ data = [], error }) => {
tooljetDatabaseService.findOne(organizationId, selectedTable, params).then(({ headers, data = [], error }) => {
setLoading(false);
if (error) {
toast.error(error?.message ?? `Error fetching table "${selectedTable}" data`);
return;
}
const totalRecords = headers['content-range'].split('/')[1] || 0;
setTotalRecords(totalRecords);
setSelectedTableData(data);
});
};
@ -58,6 +63,7 @@ const Table = () => {
fetchTableData();
fetchTableMetadata();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTable]);
const tableData = React.useMemo(
@ -214,9 +220,14 @@ const Table = () => {
prepareRow(row);
return (
<tr {...row.getRowProps()} key={index}>
{row.cells.map((cell) => {
{row.cells.map((cell, index) => {
return (
<td title={cell.value || ''} className="table-cell" {...cell.getCellProps()}>
<td
key={`cell.value-${index}`}
title={cell.value || ''}
className="table-cell"
{...cell.getCellProps()}
>
{isBoolean(cell?.value) ? cell?.value?.toString() : cell.render('Cell')}
</td>
);
@ -226,6 +237,12 @@ const Table = () => {
})}
</tbody>
</table>
<TableFooter
darkMode={darkMode}
openCreateRowDrawer={openCreateRowDrawer}
totalRecords={totalRecords}
fetchTableData={fetchTableData}
/>
</div>
<Drawer isOpen={isEditColumnDrawerOpen} onClose={() => setIsEditColumnDrawerOpen(false)} position="right">
<EditColumnForm

View file

@ -1,4 +1,4 @@
import React, { useContext, useRef } from 'react';
import React, { useState, useContext, useRef } from 'react';
import cx from 'classnames';
import { toast } from 'react-hot-toast';
import { isEmpty } from 'lodash';
@ -68,6 +68,7 @@ const TooljetDatabasePage = () => {
};
const darkMode = localStorage.getItem('darkMode') === 'true';
const [isCreateRowDrawerOpen, setIsCreateRowDrawerOpen] = useState(false);
return (
<div className="row gx-0">
@ -88,7 +89,10 @@ const TooljetDatabasePage = () => {
<>
<Filter onClose={handleBuildFilterQuery} />
<Sort onClose={handleBuildSortQuery} />
<CreateRowDrawer />
<CreateRowDrawer
isCreateRowDrawerOpen={isCreateRowDrawerOpen}
setIsCreateRowDrawerOpen={setIsCreateRowDrawerOpen}
/>
</>
)}
</div>
@ -96,7 +100,7 @@ const TooljetDatabasePage = () => {
</div>
</div>
<div className={cx('col')}>
<Table />
<Table openCreateRowDrawer={() => setIsCreateRowDrawerOpen(true)} />
</div>
</>
)}

View file

@ -58,6 +58,14 @@ $btn-dark-color: #FFFFFF;
line-height: 20px;
}
.unstyled-button.dark {
color: #FFFFFF;
img {
filter: brightness(0) invert(1);
}
}
.page-handle-button-container {
border-radius: $base-border-radius;

View file

@ -8488,3 +8488,22 @@ tbody {
// ONBOARDING-SELF-HOST STYLES END------->
//ONBOARD STYLES END---------------------------->>>>>
.toojet-db-table-footer {
position: fixed;
bottom: 0px;
width: -webkit-fill-available;
}
.tooljet-db-pagination-container {
display: flex;
padding: 0px;
height: 20px;
.form-control {
padding: 0 4px;
width: fit-content;
max-width: 30px;
text-align: center;
}
}

View file

@ -17,6 +17,7 @@ export const SelectComponent = ({ options = [], value, onChange, ...restProps })
useMenuPortal = true, // todo: deperecate this prop, use menuPortalTarget instead
maxMenuHeight = 250,
menuPortalTarget = null,
menuPlacement = 'auto',
} = restProps;
const customStyles = defaultStyles(darkMode, width, height, styles);
@ -56,7 +57,7 @@ export const SelectComponent = ({ options = [], value, onChange, ...restProps })
placeholder={placeholder}
styles={customStyles}
formatOptionLabel={(option) => renderCustomOption(option)}
menuPlacement="auto"
menuPlacement={menuPlacement}
maxMenuHeight={maxMenuHeight}
menuPortalTarget={useMenuPortal ? document.body : menuPortalTarget}
/>

View file

@ -16,6 +16,9 @@ export class PostgrestProxyService {
const authToken = 'Bearer ' + this.signJwtPayload(this.configService.get<string>('PG_USER'));
req.headers = {};
req.headers['Authorization'] = authToken;
req.headers['Prefer'] = 'count=exact'; // To get the total count of records
res.set('Access-Control-Expose-Headers', 'Content-Range');
return this.httpProxy(req, res, next);
}