From d4e68de34959883d8ba10cd68e1a2a735be1d27d Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Fri, 10 Feb 2017 15:32:30 -0700 Subject: [PATCH 01/13] setting up dashboard :) --- client/coral-admin/src/AppRouter.js | 2 + .../coral-admin/src/components/ui/Header.js | 40 +++++++++++++------ .../src/containers/Dashboard/Dashboard.js | 15 +++++++ .../coral-admin/src/graphql/queries/index.js | 4 ++ .../src/graphql/queries/mostFlags.graphql | 3 ++ client/coral-admin/src/translations.json | 2 + graph/loaders/assets.js | 10 +++++ graph/resolvers/root_query.js | 10 ++++- graph/typeDefs.graphql | 40 +++++++++++++++++++ 9 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 client/coral-admin/src/containers/Dashboard/Dashboard.js create mode 100644 client/coral-admin/src/graphql/queries/index.js create mode 100644 client/coral-admin/src/graphql/queries/mostFlags.graphql diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 9721123fa..e76690417 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -8,6 +8,7 @@ import CommentStream from 'containers/CommentStream/CommentStream'; import InstallContainer from 'containers/Install/InstallContainer'; import CommunityContainer from 'containers/Community/CommunityContainer'; import ModerationContainer from 'containers/ModerationQueue/ModerationContainer'; +import Dashboard from 'containers/Dashboard/Dashboard'; const routes = (
@@ -18,6 +19,7 @@ const routes = ( +
); diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 3e6598d6e..7263c7ca4 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -13,21 +13,35 @@ export default ({handleLogout, restricted = false}) => ( !restricted ?
- - {lang.t('configure.moderate')} - - - {lang.t('configure.community')} + + {lang.t('configure.moderate')} + + + {lang.t('configure.community')} - - {lang.t('configure.configure')} + + {lang.t('configure.configure')} - - {lang.t('configure.streams')} + + {lang.t('configure.streams')} + + + {lang.t('configure.dashboard')}
diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js new file mode 100644 index 000000000..c287d5f2c --- /dev/null +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -0,0 +1,15 @@ +import React from 'react'; +import {compose} from 'react-apollo'; +import {mostFlags} from 'coral-admin/src/graphql/queries'; + +class Dashboard extends React.Component { + render () { + return ( +
Dashboard
+ ); + } +} + +export default compose( + mostFlags +)(Dashboard); diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js new file mode 100644 index 000000000..30550e48a --- /dev/null +++ b/client/coral-admin/src/graphql/queries/index.js @@ -0,0 +1,4 @@ +import {graphql} from 'react-apollo'; +import MOST_FLAGS from './mostFlags.graphql'; + +export const mostFlags = graphql(MOST_FLAGS, {}); diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql new file mode 100644 index 000000000..b84e21d08 --- /dev/null +++ b/client/coral-admin/src/graphql/queries/mostFlags.graphql @@ -0,0 +1,3 @@ +query mostFlags { + metric +} diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 0eb1ab98d..68658d4a0 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -48,6 +48,7 @@ "copy": "Copy to Clipboard" }, "configure": { + "dashboard": "Dashboard", "enable-pre-moderation": "Enable pre-moderation", "enable-pre-moderation-text": "Moderators must approve any comment before it is published.", "require-email-verification": "Require Email Verification", @@ -157,6 +158,7 @@ "username_flags": "" }, "configure": { + "dashboard": "Panel", "enable-pre-moderation": "Habilitar pre-moderación", "enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.", "require-email-verification": "Necesita confirmación de correo", diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js index a07a18a3f..ba8fb3751 100644 --- a/graph/loaders/assets.js +++ b/graph/loaders/assets.js @@ -46,6 +46,15 @@ const findOrCreateAssetByURL = (context, asset_url) => { }); }; +const getAssetsForMetrics = ({loaders: {Actions, Comments}}) => { + return Actions.getByTypes({action_type: 'FLAG', item_type: 'COMMENT'}) + .then((actions) => { // ALL ACTIONS :O + const ids = actions.map(({item_id}) => item_id); + + return Comments.getByQuery({ids}); + }); +}; + /** * Creates a set of loaders based on a GraphQL context. * @param {Object} context the context of the GraphQL request @@ -59,6 +68,7 @@ module.exports = (context) => ({ getByURL: (url) => findOrCreateAssetByURL(context, url), getByID: new DataLoader((ids) => genAssetsByID(context, ids)), + getForMetrics: () => getAssetsForMetrics(context), getAll: new util.SingletonResolver(() => AssetModel.find({})) } }); diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 8aa14d5c6..72d57ed14 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -34,7 +34,7 @@ const RootQuery = { // Map the actions from the items referenced byt this query. The actions // returned by this query are explicitly going to be distinct by their // `item_id`'s. - let ids = actions.map((action) => action.item_id); + let ids = actions.map(({item_id}) => item_id); // Perform the query using the available resolver. return Comments.getByQuery({ids, statuses, asset_id, parent_id, limit, cursor, sort}); @@ -44,6 +44,14 @@ const RootQuery = { return Comments.getByQuery(query); }, + metric(_, args, {user, loaders: {Assets}}) { + if (user == null || !user.hasRoles('ADMIN')) { + return null; + } + + return Assets.getForMetrics(); + }, + // This returns the current user, ensure that if we aren't logged in, we // return null. me(_, args, {user}) { diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index e231bc0c3..7cbb6bd47 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -167,6 +167,36 @@ interface ActionSummary { current_user: Action } +# A summary of actions for a specific action type on an Asset. +interface AssetActionSummary { + + # Number of actions associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the actions. + actionableItemCount: Int +} + +# A summary of counts related to all the Likes on an Asset. +type LikeAssetActionSummary implements AssetActionSummary { + + # Number of actions associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the actions. + actionableItemCount: Int +} + +# A summary of counts related to all the Flags on an Asset. +type FlagAssetActionSummary implements AssetActionSummary { + + # Number of actions associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the actions. + actionableItemCount: Int +} + # LikeAction is used by users who "like" a specific entity. type LikeAction implements Action { @@ -294,6 +324,13 @@ type Asset { # The date that the asset was created. created_at: Date + + # Summary of all Actions against all entities associated with the Asset. + # (likes, flags, etc.) + action_summaries: [AssetActionSummary] + + # Unique users that have commented on this Asset. + authorCount: Int } ################################################################################ @@ -358,6 +395,9 @@ type RootQuery { # The currently logged in user based on the request. me: User + + # metrics + metric: [Asset] } ################################################################################ From 0bef413a3a7efa0c8195bbecdaab0eb7f9f62355 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 14 Feb 2017 15:20:28 -0700 Subject: [PATCH 02/13] saving my place --- client/coral-admin/src/AppRouter.js | 1 + .../src/containers/Dashboard/Dashboard.css | 23 +++++++++++++++++++ .../src/containers/Dashboard/Dashboard.js | 15 +++++++++++- .../src/graphql/queries/mostFlags.graphql | 4 +++- 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 client/coral-admin/src/containers/Dashboard/Dashboard.css diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 33baa4d7b..37616a207 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -22,6 +22,7 @@ const routes = ( +
); diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.css b/client/coral-admin/src/containers/Dashboard/Dashboard.css new file mode 100644 index 000000000..be2cd9f7a --- /dev/null +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.css @@ -0,0 +1,23 @@ +.Dashboard { + display: flex; + padding: 5px; +} + +.widget { + margin-top: 10px; + flex: 1; + box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2); + margin-right: 10px; + padding: 15px; + +} + +.widget:last-child { + margin-right: 0; +} + +.heading { + margin: 0; + font-size: 1.5rem; + font-weight: bold; +} diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js index c287d5f2c..e01eec319 100644 --- a/client/coral-admin/src/containers/Dashboard/Dashboard.js +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -1,11 +1,24 @@ import React from 'react'; import {compose} from 'react-apollo'; import {mostFlags} from 'coral-admin/src/graphql/queries'; +import styles from './Dashboard.css'; class Dashboard extends React.Component { render () { return ( -
Dashboard
+
+
+

Top Ten Articles with the most flagged comments

+ + + + +
ArticleFlags
+
+
+

Top ten comments with the most likes

+
+
); } } diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql index b84e21d08..229d64541 100644 --- a/client/coral-admin/src/graphql/queries/mostFlags.graphql +++ b/client/coral-admin/src/graphql/queries/mostFlags.graphql @@ -1,3 +1,5 @@ query mostFlags { - metric + metric { + id + } } From 50e3e733c3e06b8aaeb907574c06dff6d1c0bb91 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 14 Feb 2017 16:12:38 -0700 Subject: [PATCH 03/13] create FlagWidget --- .../coral-admin/src/components/FlagWidget.css | 5 ++++ .../coral-admin/src/components/FlagWidget.js | 26 +++++++++++++++++++ .../src/containers/Dashboard/Dashboard.js | 23 +++++++++++----- 3 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 client/coral-admin/src/components/FlagWidget.css create mode 100644 client/coral-admin/src/components/FlagWidget.js diff --git a/client/coral-admin/src/components/FlagWidget.css b/client/coral-admin/src/components/FlagWidget.css new file mode 100644 index 000000000..590aa51d1 --- /dev/null +++ b/client/coral-admin/src/components/FlagWidget.css @@ -0,0 +1,5 @@ +.heading { + margin: 0; + font-size: 1.5rem; + font-weight: bold; +} diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js new file mode 100644 index 000000000..be167de8a --- /dev/null +++ b/client/coral-admin/src/components/FlagWidget.js @@ -0,0 +1,26 @@ +import React, {PropTypes} from 'react'; +import styles from './FlagWidget.css'; + +const FlagWidget = props => { + return ( + + + + + + {/* display asset list as rows here */} + +
ArticleFlags
+ ); +}; + +FlagWidget.propTypes = { + assets: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + title: PropTypes.string, + url: PropTypes.string, + count: PropTypes.number + })).isRequired +}; + +export default FlagWidget; diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js index e01eec319..4d7f268f8 100644 --- a/client/coral-admin/src/containers/Dashboard/Dashboard.js +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -1,21 +1,30 @@ import React from 'react'; import {compose} from 'react-apollo'; import {mostFlags} from 'coral-admin/src/graphql/queries'; +import {Spinner} from 'coral-ui'; import styles from './Dashboard.css'; +import FlagWidget from '../../components/FlagWidget'; class Dashboard extends React.Component { render () { + + const {data} = this.props; + + if (data.loading) { + return ; + } + + if (data.error) { + return
{data.error}
; + } + return (
-
+

Top Ten Articles with the most flagged comments

- - - - -
ArticleFlags
+
-
+

Top ten comments with the most likes

From e640bc2cc0581321ea7f68c5f0ea1f3aaf124c7d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 14 Feb 2017 17:02:50 -0700 Subject: [PATCH 04/13] Added edge for metrics, fixed client linting errors --- .../coral-admin/src/components/FlagWidget.js | 3 +- graph/loaders/index.js | 2 + graph/loaders/metrics.js | 137 ++++++++++++++++++ graph/resolvers/asset_action_summary.js | 10 ++ graph/resolvers/index.js | 2 + graph/resolvers/root_query.js | 4 +- graph/typeDefs.graphql | 23 +-- 7 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 graph/loaders/metrics.js create mode 100644 graph/resolvers/asset_action_summary.js diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js index be167de8a..324b76c10 100644 --- a/client/coral-admin/src/components/FlagWidget.js +++ b/client/coral-admin/src/components/FlagWidget.js @@ -1,7 +1,6 @@ import React, {PropTypes} from 'react'; -import styles from './FlagWidget.css'; -const FlagWidget = props => { +const FlagWidget = () => { return ( diff --git a/graph/loaders/index.js b/graph/loaders/index.js index 536e40fa9..5b1894b65 100644 --- a/graph/loaders/index.js +++ b/graph/loaders/index.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const Actions = require('./actions'); const Assets = require('./assets'); const Comments = require('./comments'); +const Metrics = require('./metrics'); const Settings = require('./settings'); const Users = require('./users'); @@ -18,6 +19,7 @@ module.exports = (context) => { Actions, Assets, Comments, + Metrics, Settings, Users ].map((loaders) => { diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js new file mode 100644 index 000000000..a827ddeea --- /dev/null +++ b/graph/loaders/metrics.js @@ -0,0 +1,137 @@ +const _ = require('lodash'); +const CommentModel = require('../../models/comment'); +const AssetModel = require('../../models/asset'); +const ActionModel = require('../../models/action'); + +const getMetrics = (context, {from, to}) => { + + let commentMetrics = {}; + let assetMetrics = []; + + return ActionModel.aggregate([ + + // Find all actions that were created in the time range. + {$match: { + action_type: 'FLAG', + item_type: 'COMMENTS', + created_at: { + $gt: from, + $lt: to + } + }}, + + // Count all those items. + {$group: { + _id: '$item_id', + count: { + $sum: 1 + } + }}, + + // Project the count to a better field. + {$project: { + item_id: '$_id', + count: '$count' + }} + ]).then((actionSummaries) => { + + // Collect all the action summaries into a dictionary. + actionSummaries.forEach((actionSummary) => { + commentMetrics[actionSummary.item_id] = actionSummary.count; + }); + + // Collect just the comment id's. + let commentIDs = actionSummaries.map((as) => as.item_id); + + // Find those comments. + return CommentModel.aggregate([ + + // Get only those comments. + {$match: { + id: { + $in: commentIDs + } + }}, + + // Group by their asset id and push in the comment id. + {$group: { + _id: { + asset_id: '$asset_id' + }, + ids: { + $addToSet: '$id' + } + }}, + + // Project that data only as better fields. + {$project: { + asset_id: '$_id.asset_id', + ids: '$ids' + }} + ]); + }) + .then((commentResults) => { + + // Compute all the action summaries for the assets based on the time slice + // that you requested. + commentResults.forEach((result) => { + let actionCount = 0; + + result.ids.forEach((id) => { + actionCount += commentMetrics[id]; + }); + + assetMetrics.push({ + id: result.asset_id, + actionCount, + actionableItemCount: result.ids.length + }); + }); + + // Sort the assets by flag count. + assetMetrics.sort((a, b) => { + return b.flags - a.flags; + }); + + // Only keep the top 10. + assetMetrics = assetMetrics.slice(0, 10); + + // Determine the assets that we need to return. + return AssetModel.find({ + id: { + $in: assetMetrics.map((asset) => asset.id) + } + }); + }) + .then((assets) => { + + // Join up the assets that are returned by their id. + let groupedAssets = _.groupBy(assets, 'id'); + + // Return from the sorted asset metrics and return their assetes. + return assetMetrics.map(({id, actionCount, actionableItemCount}) => { + if (id in groupedAssets) { + let asset = groupedAssets[id][0]; + + let flagAssetActionSummary = { + action_type: 'FLAG', + actionCount, + actionableItemCount + }; + + // Add the action summaries to the asset. + asset.action_summaries = [flagAssetActionSummary]; + + return asset; + } + + return null; + }).filter((asset) => asset != null); + }); +}; + +module.exports = (context) => ({ + Metrics: { + get: ({from, to}) => getMetrics(context, {from, to}) + } +}); diff --git a/graph/resolvers/asset_action_summary.js b/graph/resolvers/asset_action_summary.js new file mode 100644 index 000000000..99483f9bb --- /dev/null +++ b/graph/resolvers/asset_action_summary.js @@ -0,0 +1,10 @@ +const AssetActionSummary = { + __resolveType({action_type}) { + switch (action_type) { + case 'FLAG': + return 'FlagAssetActionSummary'; + } + } +}; + +module.exports = AssetActionSummary; diff --git a/graph/resolvers/index.js b/graph/resolvers/index.js index 84dd10fdc..65461dc76 100644 --- a/graph/resolvers/index.js +++ b/graph/resolvers/index.js @@ -1,5 +1,6 @@ const ActionSummary = require('./action_summary'); const Action = require('./action'); +const AssetActionSummary = require('./asset_action_summary'); const Asset = require('./asset'); const Comment = require('./comment'); const Date = require('./date'); @@ -17,6 +18,7 @@ const ValidationUserError = require('./validation_user_error'); module.exports = { ActionSummary, Action, + AssetActionSummary, Asset, Comment, Date, diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index ff81e4c0f..432ad126f 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -39,12 +39,12 @@ const RootQuery = { return Comments.getByQuery(query); }, - metric(_, args, {user, loaders: {Assets}}) { + metrics(_, {from, to}, {user, loaders: {Metrics}}) { if (user == null || !user.hasRoles('ADMIN')) { return null; } - return Assets.getForMetrics(); + return Metrics.get({from, to}); }, // This returns the current user, ensure that if we aren't logged in, we diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 0ba307046..5d9262612 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -198,16 +198,6 @@ interface AssetActionSummary { actionableItemCount: Int } -# A summary of counts related to all the Likes on an Asset. -type LikeAssetActionSummary implements AssetActionSummary { - - # Number of actions associated with actionable types on this this Asset. - actionCount: Int - - # Number of unique actionable types that are referenced by the actions. - actionableItemCount: Int -} - # A summary of counts related to all the Flags on an Asset. type FlagAssetActionSummary implements AssetActionSummary { @@ -343,15 +333,12 @@ type Asset { # The date that the asset was closed at. closedAt: Date - # The date that the asset was created. - created_at: Date - # Summary of all Actions against all entities associated with the Asset. # (likes, flags, etc.) action_summaries: [AssetActionSummary] - # Unique users that have commented on this Asset. - authorCount: Int + # The date that the asset was created. + created_at: Date } ################################################################################ @@ -386,7 +373,7 @@ type ValidationUserError implements UserError { } ################################################################################ -## Queries +## Queries; ################################################################################ # Establishes the ordering of the content by their created_at time stamp. @@ -424,8 +411,8 @@ type RootQuery { # The currently logged in user based on the request. me: User - # metrics - metric: [Asset] + # Metrics related to user actions are saturated into the assets returned. + metrics(from: Date!, to: Date!): [Asset] } ################################################################################ From 6eda4956639254c9ba93ed317b27dbde0e463875 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 15 Feb 2017 12:57:51 -0700 Subject: [PATCH 05/13] display most flagged assets in a table --- .../coral-admin/src/components/FlagWidget.css | 24 ++++++++++ .../coral-admin/src/components/FlagWidget.js | 46 ++++++++++++++----- .../src/containers/Dashboard/Dashboard.js | 3 +- .../coral-admin/src/graphql/queries/index.js | 15 +++++- .../src/graphql/queries/mostFlags.graphql | 11 ++++- client/coral-docs/src/services/fetcher.js | 2 +- 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/client/coral-admin/src/components/FlagWidget.css b/client/coral-admin/src/components/FlagWidget.css index 590aa51d1..03be150d4 100644 --- a/client/coral-admin/src/components/FlagWidget.css +++ b/client/coral-admin/src/components/FlagWidget.css @@ -3,3 +3,27 @@ font-size: 1.5rem; font-weight: bold; } + +.widgetTable { + width: 100%; + border-collapse: collapse; + box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2); +} + +.widgetTable thead th { + border-bottom: 1px solid #f47e6b; + padding: 10px; + text-align: left; +} + +.widgetTable tbody tr { + border-bottom: 1px solid lightgrey; +} + +.widgetTable tbody tr:last-child { + border-bottom: none; +} + +.widgetTable tbody td { + padding: 10px; +} diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js index 324b76c10..f01008e28 100644 --- a/client/coral-admin/src/components/FlagWidget.js +++ b/client/coral-admin/src/components/FlagWidget.js @@ -1,25 +1,49 @@ import React, {PropTypes} from 'react'; +import styles from './FlagWidget.css'; + +const FlagWidget = ({assets}) => { -const FlagWidget = () => { return ( -
- - +
ArticleFlags
+ + + + + + - {/* display asset list as rows here */} + {assets.map(asset => { + const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount; + return ( + + + + + + ); + })}
ArticleFlagsComment Count
{asset.title}{flagCount}{asset.commentCount}
); }; FlagWidget.propTypes = { - assets: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - title: PropTypes.string, - url: PropTypes.string, - count: PropTypes.number - })).isRequired + assets: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + title: PropTypes.string, + url: PropTypes.string, + commentCount: PropTypes.number, + action_summaries: PropTypes.arrayOf( + PropTypes.shape({ + __typename: PropTypes.string.isRequired, + actionCount: PropTypes.number.isRequired, + actionableItemCount: PropTypes.number.isRequired + }) + ) + }) + ).isRequired }; export default FlagWidget; diff --git a/client/coral-admin/src/containers/Dashboard/Dashboard.js b/client/coral-admin/src/containers/Dashboard/Dashboard.js index 4d7f268f8..6baa16af1 100644 --- a/client/coral-admin/src/containers/Dashboard/Dashboard.js +++ b/client/coral-admin/src/containers/Dashboard/Dashboard.js @@ -9,6 +9,7 @@ class Dashboard extends React.Component { render () { const {data} = this.props; + const {metrics: assets} = data; if (data.loading) { return ; @@ -22,7 +23,7 @@ class Dashboard extends React.Component {

Top Ten Articles with the most flagged comments

- +

Top ten comments with the most likes

diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js index a0cceb0c7..9b8a51216 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -3,7 +3,20 @@ import {graphql} from 'react-apollo'; import MOST_FLAGS from './mostFlags.graphql'; import MOD_QUEUE_QUERY from './modQueueQuery.graphql'; -export const mostFlags = graphql(MOST_FLAGS, {}); +export const mostFlags = graphql(MOST_FLAGS, { + options: () => { + + // currently hard-coded per Greg's advice + const fiveMinutesAgo = new Date(); + fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5); + return { + variables: { + from: fiveMinutesAgo.toISOString(), + to: new Date().toISOString() + } + }; + } +}); export const modQueueQuery = graphql(MOD_QUEUE_QUERY, { options: ({params: {id = ''}}) => { diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql index 229d64541..1bcec7a1e 100644 --- a/client/coral-admin/src/graphql/queries/mostFlags.graphql +++ b/client/coral-admin/src/graphql/queries/mostFlags.graphql @@ -1,5 +1,12 @@ -query mostFlags { - metric { +query Metrics ($from: Date!, $to: Date!) { + metrics(from: $from, to: $to) { id + title + url + commentCount + action_summaries { + actionCount + actionableItemCount + } } } diff --git a/client/coral-docs/src/services/fetcher.js b/client/coral-docs/src/services/fetcher.js index e4d6f091d..aec448762 100644 --- a/client/coral-docs/src/services/fetcher.js +++ b/client/coral-docs/src/services/fetcher.js @@ -1,5 +1,5 @@ export default function fetcher(query) { - return fetch(`${window.location.host}/api/v1/graph/ql`, { + return fetch(`${window.location.origin}/api/v1/graph/ql`, { method: 'POST', headers: { Accept: 'application/json', From b26c170a52fe23327fec462dde0a24622c199d3c Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 15 Feb 2017 14:11:44 -0700 Subject: [PATCH 06/13] add some translations and link to mod queue --- .../coral-admin/src/components/FlagWidget.js | 37 ++++++++++++------- client/coral-admin/src/translations.json | 8 ++++ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js index f01008e28..422087aaf 100644 --- a/client/coral-admin/src/components/FlagWidget.js +++ b/client/coral-admin/src/components/FlagWidget.js @@ -1,5 +1,10 @@ import React, {PropTypes} from 'react'; +import {Link} from 'react-router'; import styles from './FlagWidget.css'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from 'coral-admin/src/translations'; + +const lang = new I18n(translations); const FlagWidget = ({assets}) => { @@ -7,22 +12,28 @@ const FlagWidget = ({assets}) => { - - - + {/* empty on purpose */} + + + - {assets.map(asset => { - const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount; - return ( - - - - - - ); - })} + { + assets.length + ? assets.map((asset, index) => { + const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount; + return ( + + + + + + + ); + }) + : + }
ArticleFlagsComment Count{lang.t('streams.article')}{lang.t('modqueue.flagged')}{lang.t('dashboard.comment_count')}
{asset.title}{flagCount}{asset.commentCount}
{index + 1}{lang.t('configure.moderate')} » {asset.title}{flagCount}{asset.commentCount}
{lang.t('dashboard.no_flags')}
); diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 9c9f92868..2c55ead26 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -107,6 +107,10 @@ "email": "Another member of the community recently flagged your {0} for review. Because of its content your {0} was rejected. This means you can no longer comment, like, or flag content until you rewrite your {0}. Please e-mail moderator@newsorg.com if you have any questions or concerns.", "write_message": "Write a message" }, + "dashboard": { + "no_flags": "There have been no flags in the last 5 minutes! Hooray!", + "comment_count": "Comment Count" + }, "streams": { "search": "Search", "filter-streams": "Filter Streams", @@ -209,6 +213,10 @@ "cancel": "Cancelar", "yes_ban_user": "Si, Suspendan el usuario" }, + "dashbord": { + "no_flags": "", + "comment_count": "" + }, "streams": { "search": "", "filter-streams": "", From b3b36a245aa61ee65ed1a48d7ed0f658a53ce5b7 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 15 Feb 2017 14:23:54 -0700 Subject: [PATCH 07/13] =?UTF-8?q?even=20more=20translations=20en=20Espa?= =?UTF-8?q?=C3=B1ol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/coral-admin/src/translations.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 2c55ead26..416353903 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -214,8 +214,8 @@ "yes_ban_user": "Si, Suspendan el usuario" }, "dashbord": { - "no_flags": "", - "comment_count": "" + "no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!", + "comment_count": "Cantidad de comentarios" }, "streams": { "search": "", From f91607cff9033456c259118d42e0e8bb59a9b85a Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 15 Feb 2017 15:36:46 -0700 Subject: [PATCH 08/13] show the author and publication date --- client/coral-admin/src/components/FlagWidget.css | 5 +++++ client/coral-admin/src/components/FlagWidget.js | 7 +++++-- client/coral-admin/src/graphql/queries/mostFlags.graphql | 2 ++ graph/typeDefs.graphql | 3 +++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/components/FlagWidget.css b/client/coral-admin/src/components/FlagWidget.css index 03be150d4..f40da403d 100644 --- a/client/coral-admin/src/components/FlagWidget.css +++ b/client/coral-admin/src/components/FlagWidget.css @@ -27,3 +27,8 @@ .widgetTable tbody td { padding: 10px; } + +.lede { + font-size: 0.9em; + color: grey; +} diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js index 422087aaf..aa8ceed30 100644 --- a/client/coral-admin/src/components/FlagWidget.js +++ b/client/coral-admin/src/components/FlagWidget.js @@ -25,8 +25,11 @@ const FlagWidget = ({assets}) => { const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount; return ( - {index + 1} - {lang.t('configure.moderate')} » {asset.title} + {index + 1}. + + {asset.title} +

{asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}

+ {flagCount} {asset.commentCount} diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql index 1bcec7a1e..819f9126f 100644 --- a/client/coral-admin/src/graphql/queries/mostFlags.graphql +++ b/client/coral-admin/src/graphql/queries/mostFlags.graphql @@ -4,6 +4,8 @@ query Metrics ($from: Date!, $to: Date!) { title url commentCount + author + created_at action_summaries { actionCount actionableItemCount diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 5d9262612..a71985a36 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -339,6 +339,9 @@ type Asset { # The date that the asset was created. created_at: Date + + # The author(s) of the asset. + author: String } ################################################################################ From 93636aabac614262e976f425d5b93b2af0193f15 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 16:14:54 -0700 Subject: [PATCH 09/13] Added like summaries --- graph/loaders/metrics.js | 82 +++++++++++++++---------- graph/resolvers/asset_action_summary.js | 2 + graph/resolvers/root_query.js | 4 +- graph/typeDefs.graphql | 19 ++++-- 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index a827ddeea..8b3dee5f5 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -3,7 +3,7 @@ const CommentModel = require('../../models/comment'); const AssetModel = require('../../models/asset'); const ActionModel = require('../../models/action'); -const getMetrics = (context, {from, to}) => { +const getMetrics = (context, {from, to, sort, limit}) => { let commentMetrics = {}; let assetMetrics = []; @@ -12,7 +12,6 @@ const getMetrics = (context, {from, to}) => { // Find all actions that were created in the time range. {$match: { - action_type: 'FLAG', item_type: 'COMMENTS', created_at: { $gt: from, @@ -22,7 +21,10 @@ const getMetrics = (context, {from, to}) => { // Count all those items. {$group: { - _id: '$item_id', + _id: { + item_id: '$item_id', + action_type: '$action_type' + }, count: { $sum: 1 } @@ -30,18 +32,23 @@ const getMetrics = (context, {from, to}) => { // Project the count to a better field. {$project: { - item_id: '$_id', + item_id: '$_id.item_id', + action_type: '$_id.action_type', count: '$count' }} ]).then((actionSummaries) => { // Collect all the action summaries into a dictionary. - actionSummaries.forEach((actionSummary) => { - commentMetrics[actionSummary.item_id] = actionSummary.count; + actionSummaries.forEach(({item_id, action_type, count}) => { + if (!(item_id in commentMetrics)) { + commentMetrics[item_id] = []; + } + + commentMetrics[item_id].push({action_type, count}); }); // Collect just the comment id's. - let commentIDs = actionSummaries.map((as) => as.item_id); + let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id)); // Find those comments. return CommentModel.aggregate([ @@ -72,29 +79,46 @@ const getMetrics = (context, {from, to}) => { }) .then((commentResults) => { - // Compute all the action summaries for the assets based on the time slice - // that you requested. - commentResults.forEach((result) => { - let actionCount = 0; + assetMetrics = commentResults.map(({ids, asset_id}) => { + let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type'); - result.ids.forEach((id) => { - actionCount += commentMetrics[id]; - }); + let action_summaries = Object.keys(summaries).map((action_type) => ({ + action_type, + actionCount: summaries[action_type].reduce((acc, {count}) => acc + count, 0), + actionableItemCount: summaries[action_type].length + })); - assetMetrics.push({ - id: result.asset_id, - actionCount, - actionableItemCount: result.ids.length - }); + return {action_summaries, id: asset_id}; }); - // Sort the assets by flag count. + // Sort these metrics by the predefined sort order. This will ensure that + // if the action summary does not exist on the object, that it is less + // prefered over the one that does have it. assetMetrics.sort((a, b) => { - return b.flags - a.flags; + let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort)); + let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort)); + + // If either a or b don't have this action type, then one of them will + // automatically win. + if (aActionSummary == null || bActionSummary == null) { + if (bActionSummary != null) { + return 1; + } + + if (aActionSummary != null) { + return -1; + } + + return 0; + } + + // Both of them had an actionCount, hence we can determine that we could + // compare the actual values directly. + return bActionSummary.actionCount - aActionSummary.actionCount; }); - // Only keep the top 10. - assetMetrics = assetMetrics.slice(0, 10); + // Only keep the top `limit`. + assetMetrics = assetMetrics.slice(0, limit); // Determine the assets that we need to return. return AssetModel.find({ @@ -109,18 +133,12 @@ const getMetrics = (context, {from, to}) => { let groupedAssets = _.groupBy(assets, 'id'); // Return from the sorted asset metrics and return their assetes. - return assetMetrics.map(({id, actionCount, actionableItemCount}) => { + return assetMetrics.map(({id, action_summaries}) => { if (id in groupedAssets) { let asset = groupedAssets[id][0]; - let flagAssetActionSummary = { - action_type: 'FLAG', - actionCount, - actionableItemCount - }; - // Add the action summaries to the asset. - asset.action_summaries = [flagAssetActionSummary]; + asset.action_summaries = action_summaries; return asset; } @@ -132,6 +150,6 @@ const getMetrics = (context, {from, to}) => { module.exports = (context) => ({ Metrics: { - get: ({from, to}) => getMetrics(context, {from, to}) + get: ({from, to, sort, limit}) => getMetrics(context, {from, to, sort, limit}) } }); diff --git a/graph/resolvers/asset_action_summary.js b/graph/resolvers/asset_action_summary.js index 99483f9bb..c4a3cef01 100644 --- a/graph/resolvers/asset_action_summary.js +++ b/graph/resolvers/asset_action_summary.js @@ -3,6 +3,8 @@ const AssetActionSummary = { switch (action_type) { case 'FLAG': return 'FlagAssetActionSummary'; + case 'LIKE': + return 'LikeAssetActionSummary'; } } }; diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js index 432ad126f..eb66274dd 100644 --- a/graph/resolvers/root_query.js +++ b/graph/resolvers/root_query.js @@ -39,12 +39,12 @@ const RootQuery = { return Comments.getByQuery(query); }, - metrics(_, {from, to}, {user, loaders: {Metrics}}) { + metrics(_, {from, to, sort, limit = 10}, {user, loaders: {Metrics}}) { if (user == null || !user.hasRoles('ADMIN')) { return null; } - return Metrics.get({from, to}); + return Metrics.get({from, to, sort, limit}); }, // This returns the current user, ensure that if we aren't logged in, we diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index a71985a36..e14b7676f 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -201,10 +201,20 @@ interface AssetActionSummary { # A summary of counts related to all the Flags on an Asset. type FlagAssetActionSummary implements AssetActionSummary { - # Number of actions associated with actionable types on this this Asset. + # Number of flags associated with actionable types on this this Asset. actionCount: Int - # Number of unique actionable types that are referenced by the actions. + # Number of unique actionable types that are referenced by the flags. + actionableItemCount: Int +} + +# A summary of counts related to all the Likes on an Asset. +type LikeAssetActionSummary implements AssetActionSummary { + + # Number of likes associated with actionable types on this this Asset. + actionCount: Int + + # Number of unique actionable types that are referenced by the likes. actionableItemCount: Int } @@ -414,8 +424,9 @@ type RootQuery { # The currently logged in user based on the request. me: User - # Metrics related to user actions are saturated into the assets returned. - metrics(from: Date!, to: Date!): [Asset] + # Metrics related to user actions are saturated into the assets returned. The + # sort will affect if it will allow + metrics(from: Date!, to: Date!, sort: ACTION_TYPE!, limit: Int = 10): [Asset] } ################################################################################ From 6c141cbce06b629b6b01c0c12fbe50774d962b3a Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 15 Feb 2017 16:52:06 -0700 Subject: [PATCH 10/13] now with likes --- client/coral-admin/src/components/FlagWidget.js | 3 +++ client/coral-admin/src/graphql/queries/index.js | 3 ++- client/coral-admin/src/graphql/queries/mostFlags.graphql | 4 ++-- client/coral-admin/src/translations.json | 6 ++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/client/coral-admin/src/components/FlagWidget.js b/client/coral-admin/src/components/FlagWidget.js index aa8ceed30..8116d4ff9 100644 --- a/client/coral-admin/src/components/FlagWidget.js +++ b/client/coral-admin/src/components/FlagWidget.js @@ -15,6 +15,7 @@ const FlagWidget = ({assets}) => { {/* empty on purpose */} {lang.t('streams.article')} {lang.t('modqueue.flagged')} + {lang.t('modqueue.likes')} {lang.t('dashboard.comment_count')} @@ -23,6 +24,7 @@ const FlagWidget = ({assets}) => { assets.length ? assets.map((asset, index) => { const flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount; + const likeCount = asset.action_summaries.find(s => s.__typename === 'LikeAssetActionSummary').actionCount; return ( {index + 1}. @@ -30,6 +32,7 @@ const FlagWidget = ({assets}) => { {asset.title}

{asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}

+ {likeCount} {flagCount} {asset.commentCount} diff --git a/client/coral-admin/src/graphql/queries/index.js b/client/coral-admin/src/graphql/queries/index.js index 9b8a51216..5a429756e 100644 --- a/client/coral-admin/src/graphql/queries/index.js +++ b/client/coral-admin/src/graphql/queries/index.js @@ -8,9 +8,10 @@ export const mostFlags = graphql(MOST_FLAGS, { // currently hard-coded per Greg's advice const fiveMinutesAgo = new Date(); - fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5); + fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 305); return { variables: { + sort: 'FLAG', from: fiveMinutesAgo.toISOString(), to: new Date().toISOString() } diff --git a/client/coral-admin/src/graphql/queries/mostFlags.graphql b/client/coral-admin/src/graphql/queries/mostFlags.graphql index 819f9126f..182ea0c8b 100644 --- a/client/coral-admin/src/graphql/queries/mostFlags.graphql +++ b/client/coral-admin/src/graphql/queries/mostFlags.graphql @@ -1,5 +1,5 @@ -query Metrics ($from: Date!, $to: Date!) { - metrics(from: $from, to: $to) { +query Metrics ($from: Date!, $to: Date!, $sort: ACTION_TYPE!) { + metrics(from: $from, to: $to, sort: $sort) { id title url diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 416353903..5b15564d7 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -16,6 +16,7 @@ "loading": "Loading results" }, "modqueue": { + "likes": "likes", "all": "all", "premod": "pre-mod", "rejected": "rejected", @@ -109,7 +110,7 @@ }, "dashboard": { "no_flags": "There have been no flags in the last 5 minutes! Hooray!", - "comment_count": "Comment Count" + "comment_count": "Comments" }, "streams": { "search": "Search", @@ -145,6 +146,7 @@ "loading": "Cargando resultados" }, "modqueue": { + "likes": "gustos", "premod": "pre-mod", "rejected": "rechazado", "flagged": "marcado", @@ -215,7 +217,7 @@ }, "dashbord": { "no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!", - "comment_count": "Cantidad de comentarios" + "comment_count": "Comentarios" }, "streams": { "search": "", From 9eefc0299f104812adae9c443eb8e55a830a6989 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 17:11:50 -0700 Subject: [PATCH 11/13] Optimization pass --- graph/loaders/metrics.js | 228 +++++++++++++++++++++------------------ graph/loaders/util.js | 10 ++ 2 files changed, 133 insertions(+), 105 deletions(-) diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index 8b3dee5f5..ea3fe4d31 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -1,13 +1,104 @@ const _ = require('lodash'); +const DataLoader = require('dataloader'); +const util = require('./util'); +const objectCacheKeyFn = util.objectCacheKeyFn; +const arrayCacheKeyFn = util.arrayCacheKeyFn; + const CommentModel = require('../../models/comment'); -const AssetModel = require('../../models/asset'); const ActionModel = require('../../models/action'); -const getMetrics = (context, {from, to, sort, limit}) => { +const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => { let commentMetrics = {}; let assetMetrics = []; + return Metrics.getRecentActions.load({from, to}) + .then((actionSummaries) => { + + commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => { + if (!(item_id in acc)) { + acc[item_id] = []; + } + + acc[item_id].push({action_type, count}); + + return acc; + }, {}); + + // Collect just the comment id's. + let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id)); + + // Find those comments. + return Metrics.getSpecificComments.load(commentIDs); + }) + .then((commentResults) => { + + assetMetrics = commentResults.map(({ids, asset_id}) => { + let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type'); + + let action_summaries = Object.keys(summaries).map((action_type) => ({ + action_type, + actionCount: summaries[action_type].reduce((acc, {count}) => acc + count, 0), + actionableItemCount: summaries[action_type].length + })); + + return {action_summaries, id: asset_id}; + }); + + // Sort these metrics by the predefined sort order. This will ensure that + // if the action summary does not exist on the object, that it is less + // prefered over the one that does have it. + assetMetrics.sort((a, b) => { + let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort)); + let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort)); + + // If either a or b don't have this action type, then one of them will + // automatically win. + if (aActionSummary == null || bActionSummary == null) { + if (bActionSummary != null) { + return 1; + } + + if (aActionSummary != null) { + return -1; + } + + return 0; + } + + // Both of them had an actionCount, hence we can determine that we could + // compare the actual values directly. + return bActionSummary.actionCount - aActionSummary.actionCount; + }); + + // Only keep the top `limit`. + assetMetrics = assetMetrics.slice(0, limit); + + // Determine the assets that we need to return. + return Assets.getByID.loadMany(assetMetrics.map((asset) => asset.id)); + }) + .then((assets) => { + + // Join up the assets that are returned by their id. + let groupedAssets = _.groupBy(assets, 'id'); + + // Return from the sorted asset metrics and return their assetes. + return assetMetrics.map(({id, action_summaries}) => { + if (id in groupedAssets) { + let asset = groupedAssets[id][0]; + + // Add the action summaries to the asset. + asset.action_summaries = action_summaries; + + return asset; + } + + return null; + }).filter((asset) => asset != null); + }); +}; + +const getRecentActions = (context, {from, to}) => { return ActionModel.aggregate([ // Find all actions that were created in the time range. @@ -36,120 +127,47 @@ const getMetrics = (context, {from, to, sort, limit}) => { action_type: '$_id.action_type', count: '$count' }} - ]).then((actionSummaries) => { + ]); +}; - // Collect all the action summaries into a dictionary. - actionSummaries.forEach(({item_id, action_type, count}) => { - if (!(item_id in commentMetrics)) { - commentMetrics[item_id] = []; - } +const getSpecificComments = (context, ids) => { + return CommentModel.aggregate([ - commentMetrics[item_id].push({action_type, count}); - }); - - // Collect just the comment id's. - let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id)); - - // Find those comments. - return CommentModel.aggregate([ - - // Get only those comments. - {$match: { - id: { - $in: commentIDs - } - }}, - - // Group by their asset id and push in the comment id. - {$group: { - _id: { - asset_id: '$asset_id' - }, - ids: { - $addToSet: '$id' - } - }}, - - // Project that data only as better fields. - {$project: { - asset_id: '$_id.asset_id', - ids: '$ids' - }} - ]); - }) - .then((commentResults) => { - - assetMetrics = commentResults.map(({ids, asset_id}) => { - let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type'); - - let action_summaries = Object.keys(summaries).map((action_type) => ({ - action_type, - actionCount: summaries[action_type].reduce((acc, {count}) => acc + count, 0), - actionableItemCount: summaries[action_type].length - })); - - return {action_summaries, id: asset_id}; - }); - - // Sort these metrics by the predefined sort order. This will ensure that - // if the action summary does not exist on the object, that it is less - // prefered over the one that does have it. - assetMetrics.sort((a, b) => { - let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort)); - let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort)); - - // If either a or b don't have this action type, then one of them will - // automatically win. - if (aActionSummary == null || bActionSummary == null) { - if (bActionSummary != null) { - return 1; - } - - if (aActionSummary != null) { - return -1; - } - - return 0; - } - - // Both of them had an actionCount, hence we can determine that we could - // compare the actual values directly. - return bActionSummary.actionCount - aActionSummary.actionCount; - }); - - // Only keep the top `limit`. - assetMetrics = assetMetrics.slice(0, limit); - - // Determine the assets that we need to return. - return AssetModel.find({ + // Get only those comments. + {$match: { id: { - $in: assetMetrics.map((asset) => asset.id) + $in: ids } - }); - }) - .then((assets) => { + }}, - // Join up the assets that are returned by their id. - let groupedAssets = _.groupBy(assets, 'id'); - - // Return from the sorted asset metrics and return their assetes. - return assetMetrics.map(({id, action_summaries}) => { - if (id in groupedAssets) { - let asset = groupedAssets[id][0]; - - // Add the action summaries to the asset. - asset.action_summaries = action_summaries; - - return asset; + // Group by their asset id and push in the comment id. + {$group: { + _id: { + asset_id: '$asset_id' + }, + ids: { + $addToSet: '$id' } + }}, - return null; - }).filter((asset) => asset != null); - }); + // Project that data only as better fields. + {$project: { + asset_id: '$_id.asset_id', + ids: '$ids' + }} + ]); }; module.exports = (context) => ({ Metrics: { + getSpecificComments: new DataLoader(([ids]) => getSpecificComments(context, ids).then((c) => [c]), { + batch: false, + cacheKeyFn: arrayCacheKeyFn + }), + getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), { + batch: false, + cacheKeyFn: objectCacheKeyFn('from', 'to') + }), get: ({from, to, sort, limit}) => getMetrics(context, {from, to, sort, limit}) } }); diff --git a/graph/loaders/util.js b/graph/loaders/util.js index 4640d8245..ab6f8a1f3 100644 --- a/graph/loaders/util.js +++ b/graph/loaders/util.js @@ -130,10 +130,20 @@ const objectCacheKeyFn = (...paths) => (obj) => { return paths.map((path) => obj[path]).join(':'); }; +/** + * Maps an object's paths to a string that can be used as a cache key. + * @param {Array} paths paths on the object to be used to generate the cache + * key + */ +const arrayCacheKeyFn = (arr) => { + return arr.sort().join(':'); +}; + module.exports = { singleJoinBy, arrayJoinBy, objectCacheKeyFn, + arrayCacheKeyFn, SingletonResolver, SharedCacheDataLoader }; From b75aed8bd8bacd6faa886423a7d7612c9f7420d3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 17:28:47 -0700 Subject: [PATCH 12/13] Another optimization pass --- graph/loaders/metrics.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index ea3fe4d31..a6170f912 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -1,8 +1,6 @@ const _ = require('lodash'); const DataLoader = require('dataloader'); -const util = require('./util'); -const objectCacheKeyFn = util.objectCacheKeyFn; -const arrayCacheKeyFn = util.arrayCacheKeyFn; +const {objectCacheKeyFn} = require('./util'); const CommentModel = require('../../models/comment'); const ActionModel = require('../../models/action'); @@ -29,7 +27,7 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => { let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id)); // Find those comments. - return Metrics.getSpecificComments.load(commentIDs); + return Metrics.getSpecificComments.loadMany(commentIDs); }) .then((commentResults) => { @@ -160,10 +158,7 @@ const getSpecificComments = (context, ids) => { module.exports = (context) => ({ Metrics: { - getSpecificComments: new DataLoader(([ids]) => getSpecificComments(context, ids).then((c) => [c]), { - batch: false, - cacheKeyFn: arrayCacheKeyFn - }), + getSpecificComments: new DataLoader((ids) => getSpecificComments(context, ids)), getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), { batch: false, cacheKeyFn: objectCacheKeyFn('from', 'to') From 27819d0d6a55d50d05ddabb7eb532cacbd162c8d Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 15 Feb 2017 17:42:54 -0700 Subject: [PATCH 13/13] Changed aggregate -> find --- graph/loaders/metrics.js | 41 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/graph/loaders/metrics.js b/graph/loaders/metrics.js index a6170f912..a842165c0 100644 --- a/graph/loaders/metrics.js +++ b/graph/loaders/metrics.js @@ -29,9 +29,12 @@ const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => { // Find those comments. return Metrics.getSpecificComments.loadMany(commentIDs); }) - .then((commentResults) => { + .then((comments) => { - assetMetrics = commentResults.map(({ids, asset_id}) => { + let commentResults = _.groupBy(comments, 'asset_id'); + + assetMetrics = Object.keys(commentResults).map((asset_id) => { + let ids = commentResults[asset_id].map((comment) => comment.id); let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type'); let action_summaries = Object.keys(summaries).map((action_type) => ({ @@ -129,31 +132,15 @@ const getRecentActions = (context, {from, to}) => { }; const getSpecificComments = (context, ids) => { - return CommentModel.aggregate([ - - // Get only those comments. - {$match: { - id: { - $in: ids - } - }}, - - // Group by their asset id and push in the comment id. - {$group: { - _id: { - asset_id: '$asset_id' - }, - ids: { - $addToSet: '$id' - } - }}, - - // Project that data only as better fields. - {$project: { - asset_id: '$_id.asset_id', - ids: '$ids' - }} - ]); + return CommentModel.find({ + id: { + $in: ids + } + }) + .select({ + id: 1, + asset_id: 1 + }); }; module.exports = (context) => ({