Merged branch master into admin-login-upgrade

This commit is contained in:
Riley Davis
2017-02-28 16:31:49 -07:00
25 changed files with 557 additions and 194 deletions
+2 -1
View File
@@ -18,6 +18,7 @@
],
"rules": {
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error"
"react/jsx-uses-vars": "error",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ const routes = (
<Route path='/admin/login' component={LayoutContainer} />
<Route path='/admin/logon' component={LayoutContainer} />
<Route path='/admin' component={LayoutContainer}>
<IndexRoute component={ModerationContainer} />
<IndexRoute component={Dashboard} />
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
<Route path='streams' component={Streams} />
+1 -2
View File
@@ -75,8 +75,7 @@ export const submitUser = () => (dispatch, getState) => {
dispatch(installRequest());
coralApi('/setup', {method: 'POST', body: data})
.then(result => {
console.log(result);
dispatch(installSuccess());
dispatch(installSuccess(result));
dispatch(nextStep());
})
.catch(error => {
@@ -1,66 +0,0 @@
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}) => {
return (
<table className={styles.widgetTable}>
<thead className={styles.widgetHead}>
<tr>
<th></th>{/* empty on purpose */}
<th>{lang.t('streams.article')}</th>
<th>{lang.t('modqueue.flagged')}</th>
<th>{lang.t('modqueue.likes')}</th>
<th>{lang.t('dashboard.comment_count')}</th>
</tr>
</thead>
<tbody>
{
assets.length
? assets.map((asset, index) => {
const flagSummary = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary');
const likeSummary = asset.action_summaries.find(s => s.__typename === 'LikeAssetActionSummary');
return (
<tr key={asset.id}>
<td>{index + 1}.</td>
<td>
<Link to={`/admin/moderate/flagged/${asset.id}`}>{asset.title}</Link>
<p className={styles.lede}>{asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</td>
<td>{flagSummary ? flagSummary.actionCount : 0}</td>
<td>{likeSummary ? likeSummary.actionCount : 0}</td>
<td>{asset.commentCount}</td>
</tr>
);
})
: <tr><td colSpan="3">{lang.t('dashboard.no_flags')}</td></tr>
}
</tbody>
</table>
);
};
FlagWidget.propTypes = {
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;
@@ -1,19 +1,5 @@
.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 {
@@ -1,38 +1,46 @@
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';
import {compose} from 'react-apollo';
import {connect} from 'react-redux';
import {getMetrics} from 'coral-admin/src/graphql/queries';
import FlagWidget from './FlagWidget';
import LikeWidget from './LikeWidget';
import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation';
import {Spinner} from 'coral-ui';
class Dashboard extends React.Component {
render () {
const {data} = this.props;
const {metrics: assets} = data;
if (data.loading) {
if (this.props.data && this.props.data.loading) {
return <Spinner />;
}
if (data.error) {
return <code><pre>{data.error}</pre></code>;
}
const {data: {assetsByLike, assetsByFlag}} = this.props;
return (
<div className={styles.Dashboard}>
<div className={styles.widget}>
<h2 className={styles.heading}>Top Ten Articles with the most flagged comments</h2>
<FlagWidget assets={assets} />
</div>
<div className={styles.widget}>
<h2 className={styles.heading}>Top ten comments with the most likes</h2>
</div>
<FlagWidget assets={assetsByFlag} />
<LikeWidget assets={assetsByLike} />
</div>
);
}
}
const mapStateToProps = state => {
return {
settings: state.settings.toJS(),
moderation: state.moderation.toJS()
};
};
const mapDispatchToProps = dispatch => ({
showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
hideBanUserDialog: () => dispatch(hideBanUserDialog(false))
});
export default compose(
mostFlags
connect(mapStateToProps, mapDispatchToProps),
getMetrics
)(Dashboard);
@@ -0,0 +1,47 @@
import React from 'react';
import {Link} from 'react-router';
import styles from './Widget.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations';
const lang = new I18n(translations);
const FlagWidget = (props) => {
const {assets} = props;
return (
<div className={styles.widget}>
<h2 className={styles.heading}>Articles with the most flags</h2>
<table className={styles.widgetTable}>
<thead className={styles.widgetHead}>
<tr>
<th></th>{/* empty on purpose */}
<th>{lang.t('streams.article')}</th>
<th>{lang.t('modqueue.flagged')}</th>
</tr>
</thead>
<tbody>
{
assets.length
? assets.map((asset, index) => {
const flagSummary = asset.action_summaries.find(s => s.type === 'FlagAssetActionSummary');
return (
<tr key={asset.id}>
<td>{index + 1}.</td>
<td>
<Link to={`/admin/moderate/flagged/${asset.id}`}>{asset.title}</Link>
<p className={styles.lede}>{asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</td>
<td>{flagSummary ? flagSummary.actionCount : 0}</td>
</tr>
);
})
: <tr><td colSpan="3">{lang.t('dashboard.no_flags')}</td></tr>
}
</tbody>
</table>
</div>
);
};
export default FlagWidget;
@@ -0,0 +1,48 @@
import React from 'react';
import {Link} from 'react-router';
import styles from './Widget.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations';
const lang = new I18n(translations);
const LikeWidget = (props) => {
const {assets} = props;
return (
<div className={styles.widget}>
<h2 className={styles.heading}>Articles with the most likes</h2>
<table className={styles.widgetTable}>
<thead className={styles.widgetHead}>
<tr>
<th></th>{/* empty on purpose */}
<th>{lang.t('streams.article')}</th>
<th>{lang.t('modqueue.likes')}</th>
</tr>
</thead>
<tbody>
{
assets.length
? assets.map((asset, index) => {
const likeSummary = asset.action_summaries.find(s => s.type === 'LikeAssetActionSummary');
return (
<tr key={asset.id}>
<td>{index + 1}.</td>
<td>
<Link to={`/admin/moderate/flagged/${asset.id}`}>{asset.title}</Link>
<p className={styles.lede}>{asset.author} - Published: {new Date(asset.created_at).toLocaleDateString()}</p>
</td>
<td>{likeSummary ? likeSummary.actionCount : 0}</td>
</tr>
);
})
: <tr><td colSpan="3">{lang.t('dashboard.no_likes')}</td></tr>
}
</tbody>
</table>
</div>
);
};
export default LikeWidget;
@@ -0,0 +1,40 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations';
import ModerationQueue from 'coral-admin/src/containers/ModerationQueue/ModerationQueue';
import styles from './Widget.css';
import BanUserDialog from 'coral-admin/src/components/BanUserDialog';
const lang = new I18n(translations);
const MostLikedCommentsWidget = props => {
const {
comments,
moderation,
settings,
handleBanUser,
showBanUserDialog,
hideBanUserDialog,
acceptComment,
rejectComment
} = props;
return (
<div className={styles.widget}>
<h2 className={styles.heading}>{lang.t('most_liked_comments')}</h2>
<ModerationQueue
comments={comments}
suspectWords={settings.wordlist.suspect}
showBanUserDialog={showBanUserDialog}
acceptComment={acceptComment}
rejectComment={rejectComment} />
<BanUserDialog
open={moderation.banDialog}
user={moderation.user}
handleClose={hideBanUserDialog}
handleBanUser={handleBanUser} />
</div>
);
};
export default MostLikedCommentsWidget;
@@ -0,0 +1,42 @@
.widget {
box-sizing: border-box;
margin: 10px 5px 5px 5px;
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
padding: 15px;
flex: 1;
}
.heading {
margin: 0;
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;
}
.lede {
font-size: 0.9em;
color: grey;
}
@@ -65,6 +65,8 @@ class ModerationContainer extends Component {
}
}
const comments = data[activeTab];
return (
<div>
<ModerationHeader asset={asset} />
@@ -76,9 +78,8 @@ class ModerationContainer extends Component {
modQueueResort={modQueueResort}
/>
<ModerationQueue
data={data}
currentAsset={asset}
activeTab={activeTab}
comments={comments}
suspectWords={settings.wordlist.suspect}
showBanUserDialog={props.showBanUserDialog}
acceptComment={props.acceptComment}
@@ -7,15 +7,13 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from 'coral-admin/src/translations';
const lang = new I18n(translations);
const ModerationQueue = ({activeTab = 'premod', ...props}) => {
const areComments = props.data[activeTab].length;
const ModerationQueue = ({comments, ...props}) => {
return (
<div id="moderationList">
<ul style={{paddingLeft: 0}}>
{
areComments
? props.data[activeTab].map((comment, i) => {
comments.length
? comments.map((comment, i) => {
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
return <Comment
key={i}
@@ -37,12 +35,12 @@ const ModerationQueue = ({activeTab = 'premod', ...props}) => {
};
ModerationQueue.propTypes = {
data: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
showBanUserDialog: PropTypes.func.isRequired,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
currentAsset: PropTypes.object,
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
showBanUserDialog: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
comments: PropTypes.array.isRequired
};
export default ModerationQueue;
@@ -0,0 +1,12 @@
fragment metrics on Asset {
id
title
url
author
created_at
action_summaries {
type: __typename
actionCount
actionableItemCount
}
}
+13 -17
View File
@@ -1,23 +1,7 @@
import {graphql} from 'react-apollo';
import MOST_FLAGS from './mostFlags.graphql';
import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
export const mostFlags = graphql(MOST_FLAGS, {
options: () => {
// currently hard-coded per Greg's advice
const fiveMinutesAgo = new Date();
fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 305);
return {
variables: {
sort: 'FLAG',
from: fiveMinutesAgo.toISOString(),
to: new Date().toISOString()
}
};
}
});
import METRICS from './metricsQuery.graphql';
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
options: ({params: {id = null}}) => {
@@ -34,6 +18,18 @@ export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
})
});
export const getMetrics = graphql(METRICS, {
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
return {
variables: {
from: dashboardWindowStart,
to: dashboardWindowEnd
}
};
}
});
export const modQueueResort = (id, fetchMore) => (sort) => {
return fetchMore({
query: MOD_QUEUE_QUERY,
@@ -0,0 +1,10 @@
#import "../fragments/assetMetricsView.graphql"
query Metrics ($from: Date!, $to: Date!) {
assetsByFlag: assetMetrics(from: $from, to: $to, sort: FLAG) {
...metrics
}
assetsByLike: assetMetrics(from: $from, to: $to, sort: LIKE) {
...metrics
}
}
@@ -1,14 +0,0 @@
query Metrics ($from: Date!, $to: Date!, $sort: ACTION_TYPE!) {
metrics(from: $from, to: $to, sort: $sort) {
id
title
url
commentCount
author
created_at
action_summaries {
actionCount
actionableItemCount
}
}
}
@@ -1,11 +1,22 @@
import {Map, List} from 'immutable';
import * as actions from '../actions/settings';
// this is initialized here because
// currently you have to reload the dashboard to get new stats
// cleaner updates are planned in the future.
// TODO: if there are more than two fields for the dashboard being created here,
// please create a new reducer specifically for the Dashboard.
const DASHBOARD_WINDOW_MINUTES = 5;
let then = new Date();
then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
const initialState = Map({
wordlist: Map({
banned: List(),
suspect: List()
}),
dashboardWindowStart: then.toISOString(),
dashboardWindowEnd: new Date().toISOString(),
domains: Map({
whitelist: List()
}),
+4 -2
View File
@@ -114,7 +114,8 @@
},
"dashboard": {
"no_flags": "There have been no flags in the last 5 minutes! Hooray!",
"comment_count": "Comments"
"no_likes": "There have been no likes in the last 5 minutes. All quiet.",
"comment_count": "comments"
},
"streams": {
"empty_result": "No assets match this search. Maybe try widening your search?",
@@ -226,7 +227,8 @@
},
"dashbord": {
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
"comment_count": "Comentarios"
"no_likes": "A nadie le ha gustado algún comentario en los últimos 5 minutos. Todo tranquilo.",
"comment_count": "comentarios"
},
"streams": {
"empty_result": "No se encuentro articulo con esta busqueda. Tal vez extender la busqueda?",
@@ -137,12 +137,9 @@ class SignUpContent extends React.Component {
</div>
}
<div className={styles.footer}>
<span>
{lang.t('signIn.alreadyHaveAnAccount')}
<a id="coralSignInViewTrigger" onClick={() => changeView('SIGNIN')}>
{lang.t('signIn.signIn')}
</a>
</span>
{lang.t('signIn.alreadyHaveAnAccount')} <a id="coralSignInViewTrigger" onClick={() => changeView('SIGNIN')}>
{lang.t('signIn.signIn')}
</a>
</div>
</div>
);