Merge branch 'master' into sory-139595043-qbox

This commit is contained in:
David Erwin
2017-02-16 11:26:22 -05:00
committed by GitHub
18 changed files with 473 additions and 14 deletions
+2
View File
@@ -9,6 +9,7 @@ import CommunityContainer from 'containers/Community/CommunityContainer';
import ModerationLayout from 'containers/ModerationQueue/ModerationLayout';
import ModerationContainer from 'containers/ModerationQueue/ModerationContainer';
import Dashboard from 'containers/Dashboard/Dashboard';
const routes = (
<div>
@@ -18,6 +19,7 @@ const routes = (
<Route path='community' component={CommunityContainer} />
<Route path='configure' component={Configure} />
<Route path='streams' component={Streams} />
<Route path='dashboard' component={Dashboard} />
{/* Moderation Routes */}
@@ -0,0 +1,34 @@
.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;
}
@@ -0,0 +1,66 @@
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 flagCount = asset.action_summaries.find(s => s.__typename === 'FlagAssetActionSummary').actionCount;
const likeCount = asset.action_summaries.find(s => s.__typename === 'LikeAssetActionSummary').actionCount;
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>{likeCount}</td>
<td>{flagCount}</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;
+24 -12
View File
@@ -13,21 +13,33 @@ export default ({handleLogout, restricted = false}) => (
!restricted ?
<div>
<Navigation className={styles.nav}>
<IndexLink className={styles.navLink} to="/admin/moderate"
activeClassName={styles.active}>
{lang.t('configure.moderate')}
</IndexLink>
<Link className={styles.navLink} to="/admin/streams"
activeClassName={styles.active}>
<IndexLink
className={styles.navLink}
to="/admin/moderate"
activeClassName={styles.active}>
{lang.t('configure.moderate')}
</IndexLink>
<Link className={styles.navLink}
to="/admin/streams"
activeClassName={styles.active}>
{lang.t('configure.streams')}
</Link>
<Link className={styles.navLink} to="/admin/community"
activeClassName={styles.active}>
{lang.t('configure.community')}
<Link className={styles.navLink}
to="/admin/community"
activeClassName={styles.active}>
{lang.t('configure.community')}
</Link>
<Link className={styles.navLink} to="/admin/configure"
activeClassName={styles.active}>
{lang.t('configure.configure')}
<Link
className={styles.navLink}
to="/admin/configure"
activeClassName={styles.active}>
{lang.t('configure.configure')}
</Link>
<Link
className={styles.navLink}
to="/admin/dashboard"
activeClassName={styles.active}>
{lang.t('configure.dashboard')}
</Link>
</Navigation>
<div className={styles.rightPanel}>
@@ -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;
}
@@ -0,0 +1,38 @@
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;
const {metrics: assets} = data;
if (data.loading) {
return <Spinner />;
}
if (data.error) {
return <code><pre>{data.error}</pre></code>;
}
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>
</div>
);
}
}
export default compose(
mostFlags
)(Dashboard);
@@ -1,6 +1,24 @@
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()
}
};
}
});
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
options: ({params: {id = ''}}) => {
return {
@@ -0,0 +1,14 @@
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
}
}
}
+12
View File
@@ -16,6 +16,7 @@
"loading": "Loading results"
},
"modqueue": {
"likes": "likes",
"all": "all",
"premod": "pre-mod",
"rejected": "rejected",
@@ -50,6 +51,7 @@
"configure": {
"custom-css-url": "Custom CSS URL",
"custom-css-url-desc": "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external.",
"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",
@@ -106,6 +108,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": "Comments"
},
"streams": {
"search": "Search",
"filter-streams": "Filter Streams",
@@ -140,6 +146,7 @@
"loading": "Cargando resultados"
},
"modqueue": {
"likes": "gustos",
"premod": "pre-mod",
"rejected": "rechazado",
"flagged": "marcado",
@@ -161,6 +168,7 @@
"configure": {
"custom-css-url": "URL CSS a medida",
"custom-css-url-desc": "URL de una hoja de estilo que va a sobrescribir los estilos por defecto de Embed Stream. Puede ser interna o externa.",
"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",
@@ -207,6 +215,10 @@
"cancel": "Cancelar",
"yes_ban_user": "Si, Suspendan el usuario"
},
"dashbord": {
"no_flags": "¡Nadie ha marcado nada en los últimos 5 minutos! ¡Bravo!",
"comment_count": "Comentarios"
},
"streams": {
"search": "",
"filter-streams": "",
+1 -1
View File
@@ -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',