diff --git a/src/common/rootReducer.js b/src/common/rootReducer.js
index ddc73c2..28d7f60 100644
--- a/src/common/rootReducer.js
+++ b/src/common/rootReducer.js
@@ -3,6 +3,7 @@ import { routerReducer } from 'react-router-redux';
import homeReducer from '../features/home/redux/reducer';
import commonReducer from '../features/common/redux/reducer';
import userAdminReducer from '../features/user-admin/redux/reducer';
+import roomAdminReducer from '../features/room-admin/redux/reducer';
// NOTE 1: DO NOT CHANGE the 'reducerMap' name and the declaration pattern.
// This is used for Rekit cmds to register new features, remove features, etc.
@@ -14,6 +15,7 @@ const reducerMap = {
home: homeReducer,
common: commonReducer,
userAdmin: userAdminReducer,
+ roomAdmin: roomAdminReducer,
};
export default combineReducers(reducerMap);
diff --git a/src/common/routeConfig.js b/src/common/routeConfig.js
index ed85646..5688d67 100644
--- a/src/common/routeConfig.js
+++ b/src/common/routeConfig.js
@@ -4,6 +4,7 @@ import _ from 'lodash';
import commonRoute from '../features/common/route';
import homeRoute from '../features/home/route';
import userAdminRoute from '../features/user-admin/route';
+import roomAdminRoute from '../features/room-admin/route';
// NOTE: DO NOT CHANGE the 'childRoutes' name and the declaration pattern.
// This is used for Rekit cmds to register routes config for new features, and remove config when remove features, etc.
@@ -11,6 +12,7 @@ const childRoutes = [
homeRoute,
commonRoute,
userAdminRoute,
+ roomAdminRoute,
];
const routes = [{
diff --git a/src/features/common/Layout.js b/src/features/common/Layout.js
index 66db472..9ed3018 100644
--- a/src/features/common/Layout.js
+++ b/src/features/common/Layout.js
@@ -80,6 +80,10 @@ class Layout extends Component {
component={Link} to="/user-admin">
+
+
+
);
diff --git a/src/features/room-admin/List.js b/src/features/room-admin/List.js
new file mode 100644
index 0000000..f424e50
--- /dev/null
+++ b/src/features/room-admin/List.js
@@ -0,0 +1,61 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import { Redirect } from 'react-router-dom'
+import { Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core';
+import * as actions from './redux/actions';
+
+export class List extends Component {
+ static propTypes = {
+ roomAdmin: PropTypes.object.isRequired,
+ actions: PropTypes.object.isRequired,
+ };
+
+ componentDidMount() {
+ const { fetchPublicRooms } = this.props.actions;
+ fetchPublicRooms();
+ }
+
+ render() {
+ const { mtx } = this.props;
+ const { roomList, fetchRoomsPending } = this.props.roomAdmin;
+
+ return (
+
+ {(!mtx || !mtx.clientRunning) &&
}
+ {fetchRoomsPending &&

}
+
+
+
+ NameRoom-IDMembers
+
+
+
+ {roomList.map(item => ({item.name}{item.room_id}{item.num_joined_members}))}
+
+
+
+ );
+ }
+}
+
+/* istanbul ignore next */
+function mapStateToProps(state) {
+ return {
+ mtx: state.common.mtx,
+ roomAdmin: state.roomAdmin,
+ };
+}
+
+/* istanbul ignore next */
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({ ...actions }, dispatch)
+ };
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(List);
diff --git a/src/features/room-admin/List.scss b/src/features/room-admin/List.scss
new file mode 100644
index 0000000..0cacc6b
--- /dev/null
+++ b/src/features/room-admin/List.scss
@@ -0,0 +1,9 @@
+@import '../../styles/mixins';
+
+.room-admin-list {
+ th {
+ background-color: #EEE;
+ font-size: 1rem;
+ font-weight: bold;
+ }
+}
diff --git a/src/features/room-admin/index.js b/src/features/room-admin/index.js
new file mode 100644
index 0000000..2d01c86
--- /dev/null
+++ b/src/features/room-admin/index.js
@@ -0,0 +1 @@
+export { default as List } from './List';
diff --git a/src/features/room-admin/redux/actions.js b/src/features/room-admin/redux/actions.js
new file mode 100644
index 0000000..dfa3047
--- /dev/null
+++ b/src/features/room-admin/redux/actions.js
@@ -0,0 +1 @@
+export { fetchPublicRooms, dismissFetchPublicRoomsError } from './fetchPublicRooms';
diff --git a/src/features/room-admin/redux/constants.js b/src/features/room-admin/redux/constants.js
new file mode 100644
index 0000000..eccaf57
--- /dev/null
+++ b/src/features/room-admin/redux/constants.js
@@ -0,0 +1,4 @@
+export const ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN = 'ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN';
+export const ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS = 'ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS';
+export const ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE = 'ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE';
+export const ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR = 'ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR';
diff --git a/src/features/room-admin/redux/fetchPublicRooms.js b/src/features/room-admin/redux/fetchPublicRooms.js
new file mode 100644
index 0000000..82f36ec
--- /dev/null
+++ b/src/features/room-admin/redux/fetchPublicRooms.js
@@ -0,0 +1,88 @@
+import {
+ ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN,
+ ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS,
+ ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE,
+ ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR,
+} from './constants';
+
+// Rekit uses redux-thunk for async actions by default: https://github.com/gaearon/redux-thunk
+// If you prefer redux-saga, you can use rekit-plugin-redux-saga: https://github.com/supnate/rekit-plugin-redux-saga
+export function fetchPublicRooms(args = {}) {
+ return (dispatch, getState) => {
+ dispatch({
+ type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN,
+ });
+
+ const promise = new Promise((resolve, reject) => {
+ const mtx = getState().common.mtx;
+ const doRequest = mtx.publicRooms();
+ doRequest.then(
+ (res) => {
+ dispatch({
+ type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS,
+ data: res,
+ });
+ resolve(res);
+ },
+ // Use rejectHandler as the second argument so that render errors won't be caught.
+ (err) => {
+ dispatch({
+ type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE,
+ data: { error: err },
+ });
+ reject(err);
+ },
+ );
+ });
+
+ return promise;
+ };
+}
+
+// Async action saves request error by default, this method is used to dismiss the error info.
+// If you don't want errors to be saved in Redux store, just ignore this method.
+export function dismissFetchPublicRoomsError() {
+ return {
+ type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR,
+ };
+}
+
+export function reducer(state, action) {
+ switch (action.type) {
+ case ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN:
+ // Just after a request is sent
+ return {
+ ...state,
+ fetchPublicRoomsPending: true,
+ fetchPublicRoomsError: null,
+ };
+
+ case ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS:
+ // The request is success
+ return {
+ ...state,
+ roomList: action.data.chunk,
+ fetchPublicRoomsPending: false,
+ fetchPublicRoomsError: null,
+ //roomList: action.data.chunk,
+ };
+
+ case ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE:
+ // The request is failed
+ return {
+ ...state,
+ fetchPublicRoomsPending: false,
+ fetchPublicRoomsError: action.data.error,
+ };
+
+ case ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR:
+ // Dismiss the request failure error
+ return {
+ ...state,
+ fetchPublicRoomsError: null,
+ };
+
+ default:
+ return state;
+ }
+}
diff --git a/src/features/room-admin/redux/initialState.js b/src/features/room-admin/redux/initialState.js
new file mode 100644
index 0000000..f3860da
--- /dev/null
+++ b/src/features/room-admin/redux/initialState.js
@@ -0,0 +1,14 @@
+// Initial state is the place you define all initial values for the Redux store of the feature.
+// In the 'standard' way, initialState is defined in reducers: http://redux.js.org/docs/basics/Reducers.html
+// But when application grows, there will be multiple reducers files, it's not intuitive what data is managed by the whole store.
+// So Rekit extracts the initial state definition into a separate module so that you can have
+// a quick view about what data is used for the feature, at any time.
+
+// NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions.
+const initialState = {
+ fetchPublicRoomsPending: false,
+ fetchPublicRoomsError: null,
+ roomList: [],
+};
+
+export default initialState;
diff --git a/src/features/room-admin/redux/reducer.js b/src/features/room-admin/redux/reducer.js
new file mode 100644
index 0000000..3da8f75
--- /dev/null
+++ b/src/features/room-admin/redux/reducer.js
@@ -0,0 +1,25 @@
+// This is the root reducer of the feature. It is used for:
+// 1. Load reducers from each action in the feature and process them one by one.
+// Note that this part of code is mainly maintained by Rekit, you usually don't need to edit them.
+// 2. Write cross-topic reducers. If a reducer is not bound to some specific action.
+// Then it could be written here.
+// Learn more from the introduction of this approach:
+// https://medium.com/@nate_wang/a-new-approach-for-managing-redux-actions-91c26ce8b5da.
+
+import initialState from './initialState';
+import { reducer as fetchPublicRoomsReducer } from './fetchPublicRooms';
+
+const reducers = [
+ fetchPublicRoomsReducer,
+];
+
+export default function reducer(state = initialState, action) {
+ let newState;
+ switch (action.type) {
+ // Handle cross-topic actions here
+ default:
+ newState = state;
+ break;
+ }
+ return reducers.reduce((s, r) => r(s, action), newState);
+}
diff --git a/src/features/room-admin/route.js b/src/features/room-admin/route.js
new file mode 100644
index 0000000..77c3a8d
--- /dev/null
+++ b/src/features/room-admin/route.js
@@ -0,0 +1,16 @@
+// This is the JSON way to define React Router rules in a Rekit app.
+// Learn more from: http://rekit.js.org/docs/routing.html
+
+import {
+ List,
+} from './';
+import { Layout } from '../common';
+
+export default {
+ path: 'room-admin',
+ name: 'Room admin',
+ component: Layout,
+ childRoutes: [
+ { path: 'list', name: 'List', component: List, isIndex: true },
+ ],
+};
diff --git a/src/features/room-admin/style.scss b/src/features/room-admin/style.scss
new file mode 100644
index 0000000..9c6d373
--- /dev/null
+++ b/src/features/room-admin/style.scss
@@ -0,0 +1,2 @@
+@import '../../styles/mixins';
+@import './List';
diff --git a/src/styles/index.scss b/src/styles/index.scss
index fd4a49a..1c1af56 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -3,3 +3,4 @@
@import '../features/home/style';
@import '../features/common/style';
@import '../features/user-admin/style';
+@import '../features/room-admin/style';
diff --git a/tests/features/room-admin/List.test.js b/tests/features/room-admin/List.test.js
new file mode 100644
index 0000000..efed3c0
--- /dev/null
+++ b/tests/features/room-admin/List.test.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { List } from '../../../src/features/room-admin/List';
+
+describe('room-admin/List', () => {
+ it('renders node with correct class name', () => {
+ const props = {
+ roomAdmin: {},
+ actions: {},
+ };
+ const renderedComponent = shallow(
+
+ );
+
+ expect(
+ renderedComponent.find('.room-admin-list').length
+ ).toBe(1);
+ });
+});
diff --git a/tests/features/room-admin/redux/fetchPublicRooms.test.js b/tests/features/room-admin/redux/fetchPublicRooms.test.js
new file mode 100644
index 0000000..910761c
--- /dev/null
+++ b/tests/features/room-admin/redux/fetchPublicRooms.test.js
@@ -0,0 +1,97 @@
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import nock from 'nock';
+
+import {
+ ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN,
+ ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS,
+ ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE,
+ ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR,
+} from '../../../../src/features/room-admin/redux/constants';
+
+import {
+ fetchPublicRooms,
+ dismissFetchPublicRoomsError,
+ reducer,
+} from '../../../../src/features/room-admin/redux/fetchPublicRooms';
+
+const middlewares = [thunk];
+const mockStore = configureMockStore(middlewares);
+
+describe('room-admin/redux/fetchPublicRooms', () => {
+ afterEach(() => {
+ nock.cleanAll();
+ });
+
+ it('dispatches success action when fetchPublicRooms succeeds', () => {
+ const store = mockStore({});
+
+ return store.dispatch(fetchPublicRooms())
+ .then(() => {
+ const actions = store.getActions();
+ expect(actions[0]).toHaveProperty('type', ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN);
+ expect(actions[1]).toHaveProperty('type', ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS);
+ });
+ });
+
+ it('dispatches failure action when fetchPublicRooms fails', () => {
+ const store = mockStore({});
+
+ return store.dispatch(fetchPublicRooms({ error: true }))
+ .catch(() => {
+ const actions = store.getActions();
+ expect(actions[0]).toHaveProperty('type', ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN);
+ expect(actions[1]).toHaveProperty('type', ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE);
+ expect(actions[1]).toHaveProperty('data.error', expect.anything());
+ });
+ });
+
+ it('returns correct action by dismissFetchPublicRoomsError', () => {
+ const expectedAction = {
+ type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR,
+ };
+ expect(dismissFetchPublicRoomsError()).toEqual(expectedAction);
+ });
+
+ it('handles action type ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN correctly', () => {
+ const prevState = { fetchPublicRoomsPending: false };
+ const state = reducer(
+ prevState,
+ { type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN }
+ );
+ expect(state).not.toBe(prevState); // should be immutable
+ expect(state.fetchPublicRoomsPending).toBe(true);
+ });
+
+ it('handles action type ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS correctly', () => {
+ const prevState = { fetchPublicRoomsPending: true };
+ const state = reducer(
+ prevState,
+ { type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS, data: {} }
+ );
+ expect(state).not.toBe(prevState); // should be immutable
+ expect(state.fetchPublicRoomsPending).toBe(false);
+ });
+
+ it('handles action type ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE correctly', () => {
+ const prevState = { fetchPublicRoomsPending: true };
+ const state = reducer(
+ prevState,
+ { type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE, data: { error: new Error('some error') } }
+ );
+ expect(state).not.toBe(prevState); // should be immutable
+ expect(state.fetchPublicRoomsPending).toBe(false);
+ expect(state.fetchPublicRoomsError).toEqual(expect.anything());
+ });
+
+ it('handles action type ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR correctly', () => {
+ const prevState = { fetchPublicRoomsError: new Error('some error') };
+ const state = reducer(
+ prevState,
+ { type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR }
+ );
+ expect(state).not.toBe(prevState); // should be immutable
+ expect(state.fetchPublicRoomsError).toBe(null);
+ });
+});
+
diff --git a/tests/features/room-admin/redux/reducer.test.js b/tests/features/room-admin/redux/reducer.test.js
new file mode 100644
index 0000000..9415324
--- /dev/null
+++ b/tests/features/room-admin/redux/reducer.test.js
@@ -0,0 +1,14 @@
+import reducer from '../../../../src/features/room-admin/redux/reducer';
+
+describe('room-admin/redux/reducer', () => {
+ it('does nothing if no matched action', () => {
+ const prevState = {};
+ const state = reducer(
+ prevState,
+ { type: '__UNKNOWN_ACTION_TYPE__' }
+ );
+ expect(state).toBe(prevState);
+ });
+
+ // TODO: add global reducer test if needed.
+});