diff --git a/admin/src/components/events/events-table-virtualized-lazy-loading.js b/admin/src/components/events/events-table-virtualized-lazy-loading.js new file mode 100644 index 0000000..d5ff516 --- /dev/null +++ b/admin/src/components/events/events-table-virtualized-lazy-loading.js @@ -0,0 +1,181 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { + fetchEventsWithPagination, + toggleSelection as handleSelect, + eventListSelector, + loadedSelector, + loadingSelector, + loadedRowCount, + loadingRowCount, + loadedRowsMap, + clearData, + changeLoadingInfo +} from '../../ducks/events' +import Loader from '../common/loader' +import { InfiniteLoader, AutoSizer, List } from 'react-virtualized' +import 'react-virtualized/styles.css' + +export const width = 900 +export const columnWidth = 300 +export const height = 420 +export const rowHeight = 40 + +const STATUS_LOADING = 1 +const STATUS_LOADED = 2 + +export class EventsTable extends Component { + // state = { + // // loadedRowCount: 0, + // loadedRowsMap: {} + // // loadingRowCount: 0 + // } + static propTypes = {} + + componentDidMount() { + this.props.fetchEventsWithPagination && + this.props.fetchEventsWithPagination() + } + + componentWillUnmount() { + Object.keys(this.timeoutIdMap).forEach((timeoutId) => { + clearTimeout(timeoutId) + }) + } + + render() { + const { + loading, + // loaded, + events, + clearData, + loadedRowCount, + loadingRowCount + } = this.props + + return ( +
+
+ +
+
+ +
+ {loadingRowCount} loading, {loadedRowCount} loaded +
+
+
+
+ {loading && !loadedRowCount ? ( + + ) : ( + + {({ onRowsRendered, registerChild }) => ( + + {({ width }) => ( + + )} + + )} + + )} +
+ ) + } + + handleRowClick = (event) => () => { + this.props.handleSelect(event.uid) + } + + timeoutIdMap = () => {} + + isRowLoaded = ({ index }) => { + const { loadedRowsMap } = this.props + return !!loadedRowsMap[index] // STATUS_LOADING or STATUS_LOADED + } + + loadMoreRows = ({ startIndex, stopIndex }) => { + const { loadedRowsMap, loadingRowCount } = this.props + const increment = stopIndex - startIndex + 1 + + for (var i = startIndex; i <= stopIndex; i++) { + loadedRowsMap[i] = STATUS_LOADING + } + + this.props.changeLoadingInfo({ + loadingRowCount: loadingRowCount + increment + }) + + const timeoutId = setTimeout(() => { + const { loadedRowCount, loadingRowCount } = this.props + delete this.timeoutIdMap[timeoutId] + + for (var i = startIndex; i <= stopIndex; i++) { + loadedRowsMap[i] = STATUS_LOADED + } + + this.props.changeLoadingInfo({ + loadingRowCount: loadingRowCount - increment, + loadedRowCount: loadedRowCount + increment + }) + + promiseResolver({ + size: loadingRowCount, + startAt: startIndex, + endAt: stopIndex + }) + }, 1000 + Math.round(Math.random() * 2000)) + + this.timeoutIdMap[timeoutId] = true + + let promiseResolver + + return new Promise((resolve) => { + promiseResolver = (pagination) => + resolve(this.props.fetchEventsWithPagination(pagination)) + }) + } + + rowRenderer = ({ index, key, style }) => { + const { events, loadedRowsMap } = this.props + + const row = events[index] + let content + + if (loadedRowsMap[index] === STATUS_LOADED) { + content = row.title + } else { + content =
+ } + + return ( +
+ {content} +
+ ) + } +} + +export default connect( + (state) => ({ + events: eventListSelector(state), + loading: loadingSelector(state), + loaded: loadedSelector(state), + loadedRowCount: loadedRowCount(state), + loadingRowCount: loadingRowCount(state), + loadedRowsMap: loadedRowsMap(state) + }), + { fetchEventsWithPagination, handleSelect, clearData, changeLoadingInfo } +)(EventsTable) diff --git a/admin/src/components/events/events-table-virtualized.js b/admin/src/components/events/events-table-virtualized.js index eb5ba6c..aba42c5 100644 --- a/admin/src/components/events/events-table-virtualized.js +++ b/admin/src/components/events/events-table-virtualized.js @@ -11,6 +11,11 @@ import Loader from '../common/loader' import { Table, Column } from 'react-virtualized' import 'react-virtualized/styles.css' +export const width = 900 +export const columnWidth = 300 +export const height = 420 +export const rowHeight = 40 + export class EventsTable extends Component { static propTypes = {} @@ -24,20 +29,35 @@ export class EventsTable extends Component { return ( - - - + ( +
+ {cellData} +
+ )} + dataKey="title" + width={columnWidth} + /> + +
) } rowGetter = ({ index }) => this.props.events[index] + + handleRowClick = (event) => () => { + this.props.handleSelect(event.uid) + } } export default connect( diff --git a/admin/src/components/events/events-table-virtualized.test.js b/admin/src/components/events/events-table-virtualized.test.js new file mode 100644 index 0000000..1400434 --- /dev/null +++ b/admin/src/components/events/events-table-virtualized.test.js @@ -0,0 +1,75 @@ +import React from 'react' +import { + EventsTable, + width, + height, + rowHeight, + columnWidth +} from './events-table-virtualized' +import { shallow, mount } from 'enzyme' +import { Table, Column } from 'react-virtualized' + +import Loader from '../common/loader' +import eventsMocks from '../../mocks/conferences' + +const defaultOverscanRowCount = 1 + +const events = eventsMocks.map((event) => ({ + ...event, + uid: Math.random().toString() +})) + +describe('EventsVirtualizedTable', () => { + it('should render loader', () => { + const container = shallow() + + expect(container.contains()).toBe(true) + }) + + it('should render visible events', () => { + const container = mount() + + expect(container.find('.test--event-list_item').length).toEqual( + Math.round(height / rowHeight) + defaultOverscanRowCount + ) + }) + + it('should render 3 colunms', () => { + const container = shallow() + + expect(container.children().length).toEqual(3) + }) + + it('should render first column with key title', () => { + const container = shallow() + + expect( + container + .children() + .find({ dataKey: 'title' }) + .exists() + ).toEqual(true) + }) + + it('should fetch all events', (done) => { + shallow( done()} />) + }) + + it('should select an event', () => { + let selectedEventId = null + + const container = mount( + (selectedEventId = id)} + /> + ) + + container + .find('.test--event-list_item') + .first() + .simulate('click') + + expect(selectedEventId).toEqual(events[0].uid) + }) +}) diff --git a/admin/src/config.js b/admin/src/config.js deleted file mode 100644 index aa22b3b..0000000 --- a/admin/src/config.js +++ /dev/null @@ -1,16 +0,0 @@ -import firebase from 'firebase/app' -import 'firebase/auth' -import 'firebase/database' - -export const appName = 'advreact-10-05' - -export const config = { - apiKey: 'AIzaSyCbMQM0eQUSQ0SuLVAu9ZNPUcm4rdbiB8U', - authDomain: `${appName}.firebaseapp.com`, - databaseURL: `https://${appName}.firebaseio.com`, - projectId: appName, - storageBucket: '', - messagingSenderId: '1094825197832' -} - -firebase.initializeApp(config) diff --git a/admin/src/ducks/events.js b/admin/src/ducks/events.js index 74107b8..d5eb771 100644 --- a/admin/src/ducks/events.js +++ b/admin/src/ducks/events.js @@ -14,7 +14,12 @@ const prefix = `${appName}/${moduleName}` export const FETCH_ALL_REQUEST = `${prefix}/FETCH_ALL_REQUEST` export const FETCH_ALL_START = `${prefix}/FETCH_ALL_START` export const FETCH_ALL_SUCCESS = `${prefix}/FETCH_ALL_SUCCESS` +export const FETCH_PAGINATION_EVENTS_REQUEST = `${prefix}/FETCH_PAGINATION_EVENTS_REQUEST` +export const FETCH_PAGINATION_EVENTS_START = `${prefix}/FETCH_PAGINATION_EVENTS_START` +export const FETCH_PAGINATION_EVENTS_SUCCESS = `${prefix}/FETCH_PAGINATION_EVENTS_SUCCESS` export const TOGGLE_SELECTION = `${prefix}/TOGGLE_SELECTION` +export const CLEAR_DATA = `${prefix}/CLEAR_DATA` +export const CHANGE_LOADING_INFO = `${prefix}/CHANGE_LOADING_INFO` /** * Reducer @@ -23,7 +28,10 @@ export const ReducerRecord = Record({ loading: false, loaded: false, selected: new OrderedSet(), - entities: new OrderedMap() + entities: new OrderedMap(), + loadedRowCount: 0, + loadingRowCount: 0, + loadedRowsMap: {} }) export const EventRecord = Record({ @@ -41,9 +49,14 @@ export default function reducer(state = new ReducerRecord(), action) { switch (type) { case FETCH_ALL_START: + case FETCH_PAGINATION_EVENTS_START: return state.set('loading', true) + case CHANGE_LOADING_INFO: + return state.merge(payload) + case FETCH_ALL_SUCCESS: + case FETCH_PAGINATION_EVENTS_SUCCESS: return state .set('loading', false) .set('loaded', true) @@ -58,6 +71,13 @@ export default function reducer(state = new ReducerRecord(), action) { : selected.add(payload.uid) ) + case CLEAR_DATA: + return state + .set('loadedRowCount', 0) + .set('loadingRowCount', 0) + .set('loading', false) + .set('loaded', false) + default: return state } @@ -80,6 +100,18 @@ export const loadedSelector = createSelector( stateSelector, (state) => state.loaded ) +export const loadedRowCount = createSelector( + stateSelector, + (state) => state.loadedRowCount +) +export const loadingRowCount = createSelector( + stateSelector, + (state) => state.loadingRowCount +) +export const loadedRowsMap = createSelector( + stateSelector, + (state) => state.loadedRowsMap +) export const eventListSelector = createSelector(entitiesSelector, (entities) => entities.valueSeq().toArray() ) @@ -103,6 +135,14 @@ export function fetchAllEvents() { type: FETCH_ALL_REQUEST } } +export function fetchEventsWithPagination( + { startAt, endAt } = { startAt: 0, endAt: 10 } +) { + return { + type: FETCH_PAGINATION_EVENTS_REQUEST, + payload: { startAt, endAt } + } +} export function toggleSelection(uid) { return { @@ -111,6 +151,19 @@ export function toggleSelection(uid) { } } +export function changeLoadingInfo(data) { + return { + type: CHANGE_LOADING_INFO, + payload: { ...data } + } +} + +export function clearData() { + return { + type: CLEAR_DATA + } +} + /** * Sagas * */ @@ -129,7 +182,32 @@ export function* fetchAllSaga() { payload: snapshot.val() }) } +export function* fetchWithPaginationSaga({ payload }) { + const { startAt, endAt } = payload + console.log('pagination', startAt, endAt) + const ref = firebase + .database() + .ref('events') + .orderByKey() + // .startAt(startAt) + // .endAt(endAt) + + yield put({ + type: FETCH_ALL_START + }) + + const snapshot = yield call([ref, ref.once], 'value') + console.log('snapshot', snapshot.val()) + + yield put({ + type: FETCH_ALL_SUCCESS, + payload: snapshot.val() + }) +} export function* saga() { - yield all([takeEvery(FETCH_ALL_REQUEST, fetchAllSaga)]) + yield all([ + takeEvery(FETCH_ALL_REQUEST, fetchAllSaga), + takeEvery(FETCH_PAGINATION_EVENTS_REQUEST, fetchWithPaginationSaga) + ]) } diff --git a/admin/src/routes/events-page.js b/admin/src/routes/events-page.js index 2472593..a74e7bb 100644 --- a/admin/src/routes/events-page.js +++ b/admin/src/routes/events-page.js @@ -1,5 +1,5 @@ import React, { Component } from 'react' -import EventsTable from '../components/events/events-table-virtualized' +import EventsTable from '../components/events/events-table-virtualized-lazy-loading' import SelectedEvents from '../components/events/selected-events' class EventsPage extends Component {