diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index 245a4080f0..dd3141b9ac 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -41,9 +41,9 @@ import { Helmet } from 'react-helmet'; import generatePath from 'lib/generatePath'; import { withRouter } from 'lib/withRouter'; import { get } from 'lib/AJAX'; +import BrowserFooter from './BrowserFooter.react'; // The initial and max amount of rows fetched by lazy loading -const MAX_ROWS_FETCHED = 200; const BROWSER_LAST_LOCATION = 'brower_last_location'; @subscribeTo('Schema', 'schema') @@ -54,6 +54,7 @@ class Browser extends DashboardView { this.section = 'Core'; this.subsection = 'Browser'; this.noteTimeout = null; + const limit = window.localStorage?.getItem('browserLimit'); this.state = { showCreateClassDialog: false, @@ -75,6 +76,8 @@ class Browser extends DashboardView { clp: {}, filters: new List(), ordering: '-createdAt', + skip: 0, + limit: limit ? parseInt(limit) : 100, selection: {}, exporting: false, exportingCount: 0, @@ -894,7 +897,7 @@ class Browser extends DashboardView { } async fetchParseData(source, filters) { - const { useMasterKey } = this.state; + const { useMasterKey, skip, limit } = this.state; const query = await queryFromFilters(source, filters); const sortDir = this.state.ordering[0] === '-' ? '-' : '+'; const field = this.state.ordering.substr(sortDir === '-' ? 1 : 0); @@ -904,8 +907,11 @@ class Browser extends DashboardView { } else { query.ascending(field); } + query.skip(skip); + query.limit(limit); + + localStorage?.setItem('browserLimit', limit); - query.limit(MAX_ROWS_FETCHED); this.excludeFields(query, source); let promise = query.find({ useMasterKey }); let isUnique = false; @@ -957,7 +963,7 @@ class Browser extends DashboardView { this.setState({ data: data, filters, - lastMax: MAX_ROWS_FETCHED, + lastMax: this.state.limit, filteredCounts: filteredCounts, }); } @@ -971,7 +977,7 @@ class Browser extends DashboardView { selection: {}, data, filters, - lastMax: MAX_ROWS_FETCHED, + lastMax: this.state.limit, }); } @@ -1024,7 +1030,7 @@ class Browser extends DashboardView { query.lessThan('createdAt', this.state.data[this.state.data.length - 1].get('createdAt')); query.addDescending('createdAt'); } - query.limit(MAX_ROWS_FETCHED); + query.limit(this.state.limit); this.excludeFields(query, source); const { useMasterKey } = this.state; @@ -1035,7 +1041,7 @@ class Browser extends DashboardView { })); } }); - this.setState({ lastMax: this.state.lastMax + MAX_ROWS_FETCHED }); + this.setState({ lastMax: this.state.lastMax + this.state.limit }); } updateFilters(filters) { @@ -1051,6 +1057,10 @@ class Browser extends DashboardView { // filters param change is making the fetch call this.props.navigate(generatePath(this.context, url)); } + + this.setState({ + skip: 0, + }); } saveFilters(filters, name) { @@ -1302,7 +1312,7 @@ class Browser extends DashboardView { this.state.counts[className] = 0; this.setState({ data: [], - lastMax: MAX_ROWS_FETCHED, + lastMax: this.state.limit, selection: {}, }); } @@ -1362,7 +1372,7 @@ class Browser extends DashboardView { // If after deletion, the remaining elements on the table is lesser than the maximum allowed elements // we fetch more data to fill the table - if (this.state.data.length < MAX_ROWS_FETCHED) { + if (this.state.data.length < this.state.limit) { this.prefetchData(this.props, this.context); } else { this.forceUpdate(); @@ -2005,80 +2015,96 @@ class Browser extends DashboardView { } } browser = ( - this.saveFilters(...args)} - onRemoveColumn={this.showRemoveColumn} - onDeleteRows={this.showDeleteRows} - onDropClass={this.showDropClass} - onExport={this.showExport} - onChangeCLP={this.handleCLPChange} - onRefresh={this.refresh} - onAttachRows={this.showAttachRowsDialog} - onAttachSelectedRows={this.showAttachSelectedRowsDialog} - onExecuteScriptRows={this.showExecuteScriptRowsDialog} - onCloneSelectedRows={this.showCloneSelectedRowsDialog} - onEditSelectedRow={this.showEditRowDialog} - onEditPermissions={this.onDialogToggle} - onExportSelectedRows={this.showExportSelectedRowsDialog} - onExportSchema={this.showExportSchemaDialog} - onSaveNewRow={this.saveNewRow} - onShowPointerKey={this.showPointerKeyDialog} - onAbortAddRow={this.abortAddRow} - onSaveEditCloneRow={this.saveEditCloneRow} - onAbortEditCloneRow={this.abortEditCloneRow} - onCancelPendingEditRows={this.cancelPendingEditRows} - currentUser={this.state.currentUser} - useMasterKey={this.state.useMasterKey} - login={this.login} - logout={this.logout} - toggleMasterKeyUsage={this.toggleMasterKeyUsage} - markRequiredFieldRow={this.state.markRequiredFieldRow} - requiredColumnFields={this.state.requiredColumnFields} - columns={columns} - className={className} - fetchNextPage={this.fetchNextPage} - maxFetched={this.state.lastMax} - selectRow={this.selectRow} - selection={this.state.selection} - data={this.state.data} - ordering={this.state.ordering} - newObject={this.state.newObject} - editCloneRows={this.state.editCloneRows} - relation={this.state.relation} - disableKeyControls={this.hasExtras()} - updateRow={this.updateRow} - updateOrdering={this.updateOrdering} - onPointerClick={this.handlePointerClick} - onPointerCmdClick={this.handlePointerCmdClick} - setRelation={this.setRelation} - onAddColumn={this.showAddColumn} - onAddRow={this.addRow} - onAddRowWithModal={this.addRowWithModal} - onAddClass={this.showCreateClass} - showNote={this.showNote} - onMouseDownRowCheckBox={this.onMouseDownRowCheckBox} - onMouseUpRowCheckBox={this.onMouseUpRowCheckBox} - onMouseOverRowCheckBox={this.onMouseOverRowCheckBox} - classes={this.classes} - classwiseCloudFunctions={this.state.classwiseCloudFunctions} - callCloudFunction={this.fetchAggregationPanelData} - isLoadingCloudFunction={this.state.isLoading} - setLoading={this.setLoading} - AggregationPanelData={this.state.AggregationPanelData} - setAggregationPanelData={this.setAggregationPanelData} - setErrorAggregatedData={this.setErrorAggregatedData} - errorAggregatedData={this.state.errorAggregatedData} - appName={this.props.params.appId} - /> + <> + this.saveFilters(...args)} + onRemoveColumn={this.showRemoveColumn} + onDeleteRows={this.showDeleteRows} + onDropClass={this.showDropClass} + onExport={this.showExport} + onChangeCLP={this.handleCLPChange} + onRefresh={this.refresh} + onAttachRows={this.showAttachRowsDialog} + onAttachSelectedRows={this.showAttachSelectedRowsDialog} + onExecuteScriptRows={this.showExecuteScriptRowsDialog} + onCloneSelectedRows={this.showCloneSelectedRowsDialog} + onEditSelectedRow={this.showEditRowDialog} + onEditPermissions={this.onDialogToggle} + onExportSelectedRows={this.showExportSelectedRowsDialog} + onExportSchema={this.showExportSchemaDialog} + onSaveNewRow={this.saveNewRow} + onShowPointerKey={this.showPointerKeyDialog} + onAbortAddRow={this.abortAddRow} + onSaveEditCloneRow={this.saveEditCloneRow} + onAbortEditCloneRow={this.abortEditCloneRow} + onCancelPendingEditRows={this.cancelPendingEditRows} + currentUser={this.state.currentUser} + useMasterKey={this.state.useMasterKey} + login={this.login} + logout={this.logout} + toggleMasterKeyUsage={this.toggleMasterKeyUsage} + markRequiredFieldRow={this.state.markRequiredFieldRow} + requiredColumnFields={this.state.requiredColumnFields} + columns={columns} + className={className} + fetchNextPage={this.fetchNextPage} + maxFetched={this.state.lastMax} + selectRow={this.selectRow} + selection={this.state.selection} + data={this.state.data} + ordering={this.state.ordering} + newObject={this.state.newObject} + editCloneRows={this.state.editCloneRows} + relation={this.state.relation} + disableKeyControls={this.hasExtras()} + updateRow={this.updateRow} + updateOrdering={this.updateOrdering} + onPointerClick={this.handlePointerClick} + onPointerCmdClick={this.handlePointerCmdClick} + setRelation={this.setRelation} + onAddColumn={this.showAddColumn} + onAddRow={this.addRow} + onAddRowWithModal={this.addRowWithModal} + onAddClass={this.showCreateClass} + showNote={this.showNote} + onMouseDownRowCheckBox={this.onMouseDownRowCheckBox} + onMouseUpRowCheckBox={this.onMouseUpRowCheckBox} + onMouseOverRowCheckBox={this.onMouseOverRowCheckBox} + classes={this.classes} + classwiseCloudFunctions={this.state.classwiseCloudFunctions} + callCloudFunction={this.fetchAggregationPanelData} + isLoadingCloudFunction={this.state.isLoading} + setLoading={this.setLoading} + AggregationPanelData={this.state.AggregationPanelData} + setAggregationPanelData={this.setAggregationPanelData} + setErrorAggregatedData={this.setErrorAggregatedData} + errorAggregatedData={this.state.errorAggregatedData} + appName={this.props.params.appId} + limit={this.state.limit} + /> + { + this.setState({ skip }); + this.updateOrdering(this.state.ordering); + }} + count={count} + limit={this.state.limit} + setLimit={limit => { + this.setState({ limit }); + this.updateOrdering(this.state.ordering); + }} + /> + ); } } @@ -2268,6 +2294,7 @@ class Browser extends DashboardView { confirmAttachSelectedRows={this.confirmAttachSelectedRows} schema={this.props.schema} useMasterKey={this.state.useMasterKey} + limit={this.state.limit} /> ); } else if (this.state.rowsToExport) { diff --git a/src/dashboard/Data/Browser/Browser.scss b/src/dashboard/Data/Browser/Browser.scss index 215f04a1a3..331e444ea4 100644 --- a/src/dashboard/Data/Browser/Browser.scss +++ b/src/dashboard/Data/Browser/Browser.scss @@ -12,7 +12,7 @@ top: 96px; left: 300px; right: 0; - bottom: 0; + bottom: 36px; overflow: auto; } diff --git a/src/dashboard/Data/Browser/BrowserFooter.react.js b/src/dashboard/Data/Browser/BrowserFooter.react.js new file mode 100644 index 0000000000..04f69474d7 --- /dev/null +++ b/src/dashboard/Data/Browser/BrowserFooter.react.js @@ -0,0 +1,109 @@ +import Button from 'components/Button/Button.react'; +import React from 'react'; +import styles from './BrowserFooter.scss'; + +class BrowserFooter extends React.Component { + state = { + pageInput: (Math.floor(this.props.skip / this.props.limit) + 1).toString(), + }; + + componentDidUpdate(prevProps) { + if (prevProps.count && this.props.count === 0 && prevProps.count !== 0) { + this.props.setSkip(0); + this.setState({ pageInput: '1' }); + } + + if (prevProps.skip !== this.props.skip) { + this.setState({ pageInput: (Math.floor(this.props.skip / this.props.limit) + 1).toString() }); + } + } + + handleLimitChange = event => { + const newLimit = parseInt(event.target.value, 10); + this.props.setLimit(newLimit); + this.props.setSkip(0); + this.setState({ pageInput: '1' }); + }; + + handlePageChange = newSkip => { + if (newSkip >= 0 && newSkip < this.props.count) { + this.props.setSkip(newSkip); + this.setState({ pageInput: (Math.floor(newSkip / this.props.limit) + 1).toString() }); + } + + const table = document.getElementById('browser-table'); + table.scrollTo({ top: 0 }); + }; + + handleInputChange = e => { + this.setState({ pageInput: e.target.value }); + }; + + validateAndApplyPage = () => { + const { limit, count } = this.props; + let newPage = parseInt(this.state.pageInput, 10); + + if (isNaN(newPage) || newPage < 1) { + newPage = 1; + } else if (newPage > Math.ceil(count / limit)) { + newPage = Math.ceil(count / limit); + } + + this.setState({ pageInput: newPage.toString() }); + this.handlePageChange((newPage - 1) * limit); + }; + + handleKeyDown = e => { + if (e.key === 'Enter') { + this.validateAndApplyPage(); + } + }; + + render() { + const { skip, count, limit } = this.props; + const totalPages = Math.ceil(count / limit); + + return ( +
+ {count?.toLocaleString() || 0} objects + | + + per page + | + Objects {(skip + 1).toLocaleString()} to {Math.min(count ?? limit, skip + limit).toLocaleString()} + + Page + + of {totalPages.toLocaleString()} + | +
+ ); + } +} + +export default BrowserFooter; diff --git a/src/dashboard/Data/Browser/BrowserFooter.scss b/src/dashboard/Data/Browser/BrowserFooter.scss new file mode 100644 index 0000000000..45f6fb9f02 --- /dev/null +++ b/src/dashboard/Data/Browser/BrowserFooter.scss @@ -0,0 +1,17 @@ +@import 'stylesheets/globals.scss'; + +.footer { + position: absolute; + width: calc(100% - 300px); + bottom: 0; + gap: 10px; + padding: 0px 15px; + height: 36px; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + font-size: 12px; + font-family: "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + border-top: 1px solid lightgrey; +} \ No newline at end of file diff --git a/src/dashboard/Data/Browser/BrowserTable.react.js b/src/dashboard/Data/Browser/BrowserTable.react.js index c710227e80..ea585e3280 100644 --- a/src/dashboard/Data/Browser/BrowserTable.react.js +++ b/src/dashboard/Data/Browser/BrowserTable.react.js @@ -17,8 +17,6 @@ import styles from 'dashboard/Data/Browser/Browser.scss'; import Button from 'components/Button/Button.react'; import { CurrentApp } from 'context/currentApp'; -const MAX_ROWS = 200; // Number of rows to render at any time -const ROWS_OFFSET = 160; const ROW_HEIGHT = 30; const READ_ONLY = ['objectId', 'createdAt', 'updatedAt']; @@ -34,7 +32,6 @@ export default class BrowserTable extends React.Component { isResizing: false, maxWidth: window.innerWidth - 300, }; - this.handleScroll = this.handleScroll.bind(this); this.tableRef = React.createRef(); this.handleResize = this.handleResize.bind(this); this.updateMaxWidth = this.updateMaxWidth.bind(this); @@ -60,12 +57,10 @@ export default class BrowserTable extends React.Component { } componentDidMount() { - this.tableRef.current.addEventListener('scroll', this.handleScroll); window.addEventListener('resize', this.updateMaxWidth); } componentWillUnmount() { - this.tableRef.current.removeEventListener('scroll', this.handleScroll); window.removeEventListener('resize', this.updateMaxWidth); } @@ -93,36 +88,6 @@ export default class BrowserTable extends React.Component { document.body.style.cursor = 'default'; } - handleScroll() { - if (!this.props.data || this.props.data.length === 0) { - return; - } - requestAnimationFrame(() => { - const currentScrollTop = this.tableRef.current.scrollTop; - let rowsAbove = Math.floor(currentScrollTop / ROW_HEIGHT); - let offset = this.state.offset; - const currentRow = rowsAbove - this.state.offset; - - // If the scroll is near the beginning or end of the offset, - // we need to update the table data with the previous/next offset - if (currentRow < 10 || currentRow >= ROWS_OFFSET) { - // Rounds the number of rows above - rowsAbove = Math.floor(rowsAbove / 10) * 10; - - offset = - currentRow < 10 - ? Math.max(0, rowsAbove - ROWS_OFFSET) // Previous set of rows - : rowsAbove - 10; // Next set of rows - } - if (this.state.offset !== offset) { - this.setState({ offset }); - this.tableRef.current.scrollTop = currentScrollTop; - } - if (this.props.maxFetched - offset <= ROWS_OFFSET * 1.4) { - this.props.fetchNextPage(); - } - }); - } updateMaxWidth = () => { this.setState({ maxWidth: window.innerWidth - 300 }); if (this.state.panelWidth > window.innerWidth - 300) { @@ -332,7 +297,7 @@ export default class BrowserTable extends React.Component { ); } const rows = []; - const end = Math.min(this.state.offset + MAX_ROWS, this.props.data.length); + const end = Math.min(this.state.offset + this.props.limit, this.props.data.length); for (let i = this.state.offset; i < end; i++) { const index = i - this.state.offset; const obj = this.props.data[i]; @@ -527,7 +492,7 @@ export default class BrowserTable extends React.Component { style={{ height: Math.max( 0, - (this.props.data.length - this.state.offset - MAX_ROWS) * ROW_HEIGHT + (this.props.data.length - this.state.offset - this.props.limit) * ROW_HEIGHT ), }} /> @@ -569,6 +534,7 @@ export default class BrowserTable extends React.Component { return (
this.handleChange(newValue, name, type, targetClass)} onCancel={() => this.toggleObjectPicker(name, false)} useMasterKey={useMasterKey} + limit={this.props.limit} /> ) : (
this.toggleObjectPicker(name, false)} useMasterKey={useMasterKey} + limit={this.props.limit} /> ) : ( selectedObject.id && ( diff --git a/src/dashboard/Data/Browser/ObjectPickerDialog.react.js b/src/dashboard/Data/Browser/ObjectPickerDialog.react.js index f77ccf5cd4..a722d5a5c7 100644 --- a/src/dashboard/Data/Browser/ObjectPickerDialog.react.js +++ b/src/dashboard/Data/Browser/ObjectPickerDialog.react.js @@ -16,7 +16,6 @@ import stylesFooter from 'components/Modal/Modal.scss'; import { CurrentApp } from 'context/currentApp'; // The initial and max amount of rows fetched by lazy loading -const MAX_ROWS_FETCHED = 200; const SELECTION_INPUT_ID = 'selectionInput'; export default class ObjectPickerDialog extends React.Component { @@ -90,7 +89,7 @@ export default class ObjectPickerDialog extends React.Component { this.setState({ data: data, filters, - lastMax: MAX_ROWS_FETCHED, + lastMax: this.props.limit, filteredCounts: filteredCounts, }); } @@ -107,7 +106,7 @@ export default class ObjectPickerDialog extends React.Component { query.ascending(field); } - query.limit(MAX_ROWS_FETCHED); + query.limit(this.props.limit); const promise = query.find({ useMasterKey }); @@ -167,7 +166,7 @@ export default class ObjectPickerDialog extends React.Component { query.lessThan('createdAt', this.state.data[this.state.data.length - 1].get('createdAt')); query.addDescending('createdAt'); } - query.limit(MAX_ROWS_FETCHED); + query.limit(this.props.limit); const { useMasterKey } = this.props; query.find({ useMasterKey: useMasterKey }).then(nextPage => { @@ -177,7 +176,7 @@ export default class ObjectPickerDialog extends React.Component { })); } }); - this.setState({ lastMax: this.state.lastMax + MAX_ROWS_FETCHED }); + this.setState({ lastMax: this.state.lastMax + this.props.limit }); } async updateFilters(filters) {