From 369d5917b47a18460eb9c032f2c75850ca4df097 Mon Sep 17 00:00:00 2001 From: Stavros Maroulis Date: Thu, 1 Nov 2018 19:41:10 +0200 Subject: [PATCH] Add deck stats store and actions --- actions/stats/loadDeckStats.js | 25 ++++++ actions/stats/loadDeckStatsByTime.js | 23 +++++ actions/stats/loadDeckUserStats.js | 23 +++++ .../updateDeckActivityTimelineFilters.js | 23 +++++ actions/stats/updateDeckUserStatsFilters.js | 23 +++++ app.js | 5 +- components/Deck/DeckStats.js | 52 ++++++------ components/Deck/DeckStatsPage.js | 8 +- configs/routes.js | 16 +++- services/stats.js | 42 +++++++++- stores/DeckStatsStore.js | 84 +++++++++++++++++++ 11 files changed, 289 insertions(+), 35 deletions(-) create mode 100644 actions/stats/loadDeckStats.js create mode 100644 actions/stats/loadDeckStatsByTime.js create mode 100644 actions/stats/loadDeckUserStats.js create mode 100644 actions/stats/updateDeckActivityTimelineFilters.js create mode 100644 actions/stats/updateDeckUserStatsFilters.js create mode 100644 stores/DeckStatsStore.js diff --git a/actions/stats/loadDeckStats.js b/actions/stats/loadDeckStats.js new file mode 100644 index 000000000..0079ccfd9 --- /dev/null +++ b/actions/stats/loadDeckStats.js @@ -0,0 +1,25 @@ +import async from 'async'; +const log = require('../log/clog'); +import serviceUnavailable from '../error/serviceUnavailable'; +import loadDeckUserStats from './loadDeckUserStats'; +import loadDeckStatsByTime from './loadDeckStatsByTime'; + + +export default function loadDeckStats(context, payload, done) { + log.info(context); + async.parallel([ + (callback) => { + context.executeAction(loadDeckStatsByTime, payload, callback); + }, + (callback) => { + context.executeAction(loadDeckUserStats, payload, callback); + }, + ], (err, results) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + return; + } + done(); + }); +} diff --git a/actions/stats/loadDeckStatsByTime.js b/actions/stats/loadDeckStatsByTime.js new file mode 100644 index 000000000..95c545382 --- /dev/null +++ b/actions/stats/loadDeckStatsByTime.js @@ -0,0 +1,23 @@ +const log = require('../log/clog'); +import serviceUnavailable from '../error/serviceUnavailable'; +import DeckStatsStore from '../../stores/DeckStatsStore'; + + +export default function loadDeckStatsByTime(context, payload, done) { + let datePeriod = context.getStore(DeckStatsStore).timelineFilters.datePeriod; + let activityType = context.getStore(DeckStatsStore).timelineFilters.activityType; + + log.info(context); + + context.dispatch('SET_DECK_STATS_BY_TIME_LOADING'); + + context.service.read('stats.deckStatsByTime', {datePeriod, deckId: payload.deckId, activityType}, {timeout: 20 * 1000}, (err, res) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + } else { + context.dispatch('LOAD_DECK_STATS_BY_TIME', {statsByTime: res}); + } + done(); + }); +} diff --git a/actions/stats/loadDeckUserStats.js b/actions/stats/loadDeckUserStats.js new file mode 100644 index 000000000..1188d59af --- /dev/null +++ b/actions/stats/loadDeckUserStats.js @@ -0,0 +1,23 @@ +const log = require('../log/clog'); +import serviceUnavailable from '../error/serviceUnavailable'; +import DeckStatsStore from '../../stores/DeckStatsStore'; + + +export default function loadDeckUserStats(context, payload, done) { + let datePeriod = context.getStore(DeckStatsStore).membersStatsFilters.datePeriod; + let activityType = context.getStore(DeckStatsStore).membersStatsFilters.activityType; + + log.info(context); + + context.dispatch('SET_DECK_USER_STATS_LOADING'); + + context.service.read('stats.deckUserStats', {datePeriod, deckId: payload.deckId, activityType}, {timeout: 20 * 1000}, (err, res) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + } else { + context.dispatch('LOAD_DECK_USER_STATS', {deckUserStats: res}); + } + done(); + }); +} diff --git a/actions/stats/updateDeckActivityTimelineFilters.js b/actions/stats/updateDeckActivityTimelineFilters.js new file mode 100644 index 000000000..bc4aaaa61 --- /dev/null +++ b/actions/stats/updateDeckActivityTimelineFilters.js @@ -0,0 +1,23 @@ +import async from 'async'; +const log = require('../log/clog'); +import serviceUnavailable from '../error/serviceUnavailable'; +import loadDeckStatsByTime from '../stats/loadDeckStatsByTime'; + + +export default function updateDeckStatsActivityType(context, payload, done) { + log.info(context); + context.dispatch('UPDATE_DECK_ACTIVITY_TIMELINE_FILTERS', payload); + + async.parallel([ + (callback) => { + context.executeAction(loadDeckStatsByTime, payload, callback); + }, + ], (err, results) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + return; + } + done(); + }); +} diff --git a/actions/stats/updateDeckUserStatsFilters.js b/actions/stats/updateDeckUserStatsFilters.js new file mode 100644 index 000000000..9d9f51f88 --- /dev/null +++ b/actions/stats/updateDeckUserStatsFilters.js @@ -0,0 +1,23 @@ +import async from 'async'; +const log = require('../log/clog'); +import serviceUnavailable from '../error/serviceUnavailable'; +import loadDeckUserStats from '../stats/loadDeckUserStats'; + + +export default function updateDeckUserStatsFilters(context, payload, done) { + log.info(context); + context.dispatch('UPDATE_DECK_USER_STATS_FILTERS', payload); + + async.parallel([ + (callback) => { + context.executeAction(loadDeckUserStats, payload, callback); + }, + ], (err, results) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + return; + } + done(); + }); +} diff --git a/app.js b/app.js index 596bc1193..99cb387cf 100644 --- a/app.js +++ b/app.js @@ -55,6 +55,8 @@ import LoginModalStore from './stores/LoginModalStore'; import UserStatsStore from './stores/UserStatsStore'; import UserGroupsStore from './stores/UserGroupsStore'; import GroupStatsStore from './stores/GroupStatsStore'; +import DeckStatsStore from './stores/DeckStatsStore'; + // create new fluxible instance & register all stores const app = new Fluxible({ @@ -112,7 +114,8 @@ const app = new Fluxible({ LoginModalStore, UserStatsStore, UserGroupsStore, - GroupStatsStore + GroupStatsStore, + DeckStatsStore, ] }); diff --git a/components/Deck/DeckStats.js b/components/Deck/DeckStats.js index a306fb29e..2a72e1f51 100644 --- a/components/Deck/DeckStats.js +++ b/components/Deck/DeckStats.js @@ -1,8 +1,8 @@ import React from 'react'; import {Grid} from 'semantic-ui-react'; -// import updateDeckActivityTimelineFilters from '../../actions/stats/updateDeckActivityTimelineFilters'; -// import updateDeckUsersStatsFilters from '../../actions/stats/updateDeckUsersStatsFilters'; +import updateDeckActivityTimelineFilters from '../../actions/stats/updateDeckActivityTimelineFilters'; +import updateDeckUserStatsFilters from '../../actions/stats/updateDeckUserStatsFilters'; import {defineMessages} from 'react-intl'; import ActivityTimeline from '../../components/Stats/ActivityTimeline'; @@ -20,43 +20,39 @@ class DeckStats extends React.Component { getIntlMessages() { return defineMessages({ - membersStatsTitle: { - id: 'Stats.deckUsersStatsTitle', + deckUserStatsTitle: { + id: 'Stats.deckUserStatsTitle', defaultMessage: 'User Activity' }, }); } handleTimelinePeriodChange(event, {value}) { - // TODO - // this.context.executeAction(updateDeckActivityTimelineFilters, { - // datePeriod: value, - // deckid: this.props.deckid, - // }); + this.context.executeAction(updateDeckActivityTimelineFilters, { + datePeriod: value, + deckId: this.props.deckId, + }); } handleTimelineActivityChange(event, {value}) { - // TODO - // this.context.executeAction(updateDeckActivityTimelineFilters, { - // activityType: value, - // deckid: this.props.deckid, - // }); + this.context.executeAction(updateDeckActivityTimelineFilters, { + activityType: value, + deckId: this.props.deckId, + }); } handleMembersStatsPeriodChange(event, {value}) { - // TODO - // this.context.executeAction(updateDeckUsersStatsFilters, { - // datePeriod: value, - // deckid: this.props.deckid, - // }); + this.context.executeAction(updateDeckUserStatsFilters, { + datePeriod: value, + deckId: this.props.deckId, + }); } handleMembersStatsActivityChange(event, {value}) { - // TODO - // this.context.executeAction(updateDeckUsersStatsFilters, { - // activityType: value, - // deckid: this.props.deckid, - // }); + this.context.executeAction(updateDeckUserStatsFilters, { + activityType: value, + deckId: this.props.deckId, + }); } render() { @@ -74,10 +70,10 @@ class DeckStats extends React.Component { - diff --git a/components/Deck/DeckStatsPage.js b/components/Deck/DeckStatsPage.js index 44f5c08fe..a2c9b761e 100644 --- a/components/Deck/DeckStatsPage.js +++ b/components/Deck/DeckStatsPage.js @@ -5,7 +5,7 @@ import { Grid, Divider, Button, Header, Image, Icon, Item, Label, Menu, Segment, import { connectToStores } from 'fluxible-addons-react'; import DeckPageStore from '../../stores/DeckPageStore'; import DeckViewStore from '../../stores/DeckViewStore'; -import GroupStatsStore from '../../stores/GroupStatsStore'; // TODO remove this +import DeckStatsStore from '../../stores/DeckStatsStore'; import { Microservices } from '../../configs/microservices'; import CustomDate from './util/CustomDate'; @@ -87,7 +87,7 @@ class DeckStatsPage extends React.Component { ); - let statsElement = ; + let statsElement = ; return ( @@ -110,11 +110,11 @@ class DeckStatsPage extends React.Component { } } -DeckStatsPage = connectToStores(DeckStatsPage, [DeckPageStore, DeckViewStore, GroupStatsStore], (context, props) => { +DeckStatsPage = connectToStores(DeckStatsPage, [DeckPageStore, DeckViewStore, DeckStatsStore], (context, props) => { return { DeckPageStore: context.getStore(DeckPageStore).getState(), DeckViewStore: context.getStore(DeckViewStore).getState(), - GroupStatsStore: context.getStore(GroupStatsStore).getState(), + DeckStatsStore: context.getStore(DeckStatsStore).getState(), }; }); diff --git a/configs/routes.js b/configs/routes.js index 7a188f66d..f07b91186 100644 --- a/configs/routes.js +++ b/configs/routes.js @@ -36,6 +36,8 @@ import checkReviewableUser from '../actions/userReview/checkReviewableUser'; import loadCollection from '../actions/collections/loadCollection'; import prepareSSO from '../actions/user/prepareSSO'; import {navigateAction} from 'fluxible-router'; +import loadDeckStats from '../actions/stats/loadDeckStats'; + export default { //-----------------------------------HomePage routes------------------------------ @@ -381,7 +383,19 @@ export default { handler: require('../components/Deck/DeckStatsPage'), page: 'deckstatspage', action: (context, payload, done) => { - context.executeAction(loadDeck, payload, done); + async.series([ + (callback) => { + context.executeAction(loadDeck, payload, callback); + }, + (callback) => { + context.executeAction(loadDeckStats, {deckId: payload.params.id}, callback); + }, + (err, result) => { + if(err) console.log(err); + done(); + } + ]); + } }, diff --git a/services/stats.js b/services/stats.js index c68aff345..b3c075fab 100644 --- a/services/stats.js +++ b/services/stats.js @@ -51,7 +51,7 @@ export default { name: 'stats', read: (req, resource, params, config, callback) => { let args = params.params ? params.params : params; - let {username, activityType, datePeriod, groupid} = args; + let {username, activityType, datePeriod, groupid, deckId} = args; if (resource === 'stats.userStatsByTime') { let fromDate = periodToDate(datePeriod); @@ -92,6 +92,46 @@ export default { json: true }).then((response) => callback(null, fillMissingDates(fromDate, response))) .catch((err) => callback(err)); + } else if (resource === 'stats.deckStatsByTime') { + let fromDate = periodToDate(datePeriod); + let pipeline = [{ + '$match': { + 'timestamp': {'$gte': {'$dte': fromDate.toISOString()}}, + 'statement.verb.id': activityTypeToVerb(activityType), + 'statement.context.contextActivities.parent.id': {'$regex': new RegExp(`/deck/${deckId.split('-')[0]}$`)} + } + }, { + '$project': { + 'date': { + '$dateToString': { + 'format': '%Y-%m-%d', + 'date': '$timestamp' + } + } + } + }, { + '$group': { + '_id': '$date', + 'count': { + '$sum': 1 + } + } + }, { + '$sort': { + '_id': 1 + } + }]; + console.log(JSON.stringify(pipeline)); + rp({ + method: 'GET', + uri: Microservices.lrs.uri + '/statements/aggregate', + qs: { + pipeline: JSON.stringify(pipeline), + }, + headers: {'Authorization': 'Basic ' + Microservices.lrs.basicAuth}, + json: true + }).then((response) => callback(null, fillMissingDates(fromDate, response))) + .catch((err) => callback(err)); } else if (resource === 'stats.groupStatsByTime') { let fromDate = periodToDate(datePeriod); rp.post({ diff --git a/stores/DeckStatsStore.js b/stores/DeckStatsStore.js new file mode 100644 index 000000000..3f4755d4b --- /dev/null +++ b/stores/DeckStatsStore.js @@ -0,0 +1,84 @@ +import {BaseStore} from 'fluxible/addons'; + +class DeckStatsStore extends BaseStore { + constructor(dispatcher) { + super(dispatcher); + this.timelineFilters = {datePeriod: 'LAST_7_DAYS', activityType: 'view'}; + this.deckUserStatsFilters = {datePeriod: 'LAST_7_DAYS', activityType: 'view'}; + this.deckUserStats = []; + this.deckUserStatsLoading = true; + this.statsByTime = []; + this.statsByTimeLoading = true; + } + + updateStatsByTime(payload) { + this.statsByTime = payload.statsByTime; + this.statsByTimeLoading = false; + this.emitChange(); + } + + updateDeckUserStats(payload) { + this.deckUserStats = payload.deckUserStats; + this.deckUserStatsLoading = false; + this.emitChange(); + } + updateTimelineFilters(payload) { + this.timelineFilters.datePeriod = payload.datePeriod || this.timelineFilters.datePeriod; + this.timelineFilters.activityType = payload.activityType || this.timelineFilters.activityType; + this.emitChange(); + } + + updateDeckUserStatsFilters(payload) { + this.deckUserStatsFilters.datePeriod = payload.datePeriod || this.deckUserStatsFilters.datePeriod; + this.deckUserStatsFilters.activityType = payload.activityType || this.deckUserStatsFilters.activityType; + this.emitChange(); + } + + setStatsByTimeLoading() { + this.statsByTimeLoading = true; + this.emitChange(); + } + + setDeckUserStatsLoading() { + this.deckUserStatsLoading = true; + this.emitChange(); + } + + + getState() { + return { + timelineFilters: this.timelineFilters, + statsByTime: this.statsByTime, + statsByTimeLoading: this.statsByTimeLoading, + deckUserStatsLoading: this.deckUserStatsLoading, + deckUserStats: this.deckUserStats, + deckUserStatsFilters: this.deckUserStatsFilters + }; + } + + dehydrate() { + return this.getState(); + } + + rehydrate(state) { + this.timelineFilters = state.timelineFilters; + this.statsByTime = state.statsByTime; + this.statsByTimeLoading = state.statsByTimeLoading; + this.deckUserStatsLoading = state.deckUserStatsLoading; + this.deckUserStats = state.deckUserStats; + this.deckUserStatsFilters = state.deckUserStatsFilters; + } +} + +DeckStatsStore.storeName = 'DeckStatsStore'; +DeckStatsStore.handlers = { + 'UPDATE_DECK_ACTIVITY_TIMELINE_FILTERS': 'updateTimelineFilters', + 'LOAD_DECK_STATS_BY_TIME': 'updateStatsByTime', + 'SET_DECK_STATS_BY_TIME_LOADING': 'setStatsByTimeLoading', + 'LOAD_DECK_USER_STATS': 'updateDeckUserStats', + 'UPDATE_DECK_USER_STATS_FILTERS': 'updateDeckUserStatsFilters', + 'SET_DECK_USER_STATS_LOADING': 'setDeckUserStatsLoading', + +}; + +export default DeckStatsStore;