diff --git a/src/components/CMVotingGroupGraph/CMVotingGraphs.tsx b/src/components/CMVotingGroupGraph/CMVotingGraphs.tsx
new file mode 100644
index 0000000000..a2cc4d4efa
--- /dev/null
+++ b/src/components/CMVotingGroupGraph/CMVotingGraphs.tsx
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) Online-Go.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { format, subDays, startOfWeek as startOfWeekDateFns } from "date-fns";
+
+import { dropCurrentPeriod } from "@/lib/misc";
+import * as data from "@/lib/data";
+
+import { ResponsiveLine } from "@nivo/line";
+import React from "react";
+
+function startOfWeek(the_date: Date): Date {
+ return startOfWeekDateFns(the_date, { weekStartsOn: 1 }); // 1 = Monday
+}
+
+export interface ReportCount {
+ date: string;
+ escalated: number;
+ consensus: number;
+ non_consensus: number;
+}
+
+export interface ReportAlignmentCount {
+ date: string;
+ escalated: number;
+ unanimous: number;
+ non_unanimous: number;
+}
+
+// Hardcoding the vertical axis of all "report count" graphs as the total number helps convey
+// the relative number of types of reports.
+
+// TBD: it might be nice if this "max" number was dynamically provided by the server, but
+// we are already possibly hitting it hard for these rollups
+
+const EXPECTED_MAX_WEEKLY_CM_REPORTS = 200;
+const Y_STEP_SIZE = 40; // must divide nicely into EXPECTED_MAX_WEEKLY_CM_REPORTS
+
+interface CMVoteCountGraphProps {
+ vote_data: ReportCount[];
+ period: number;
+}
+
+export const CMVoteCountGraph = ({ vote_data, period }: CMVoteCountGraphProps): JSX.Element => {
+ if (!vote_data) {
+ vote_data = [];
+ }
+
+ const aggregateDataByWeek = React.useMemo(() => {
+ const aggregated: {
+ [key: string]: {
+ escalated: number;
+ consensus: number;
+ non_consensus: number;
+ total: number;
+ };
+ } = {};
+
+ vote_data.forEach(({ date, escalated, consensus, non_consensus }) => {
+ const weekStart = startOfWeek(new Date(date)).toISOString().slice(0, 10); // Get week start and convert to ISO string for key
+
+ if (!aggregated[weekStart]) {
+ aggregated[weekStart] = { escalated: 0, consensus: 0, non_consensus: 0, total: 0 };
+ }
+ aggregated[weekStart].escalated += escalated;
+ aggregated[weekStart].consensus += consensus;
+ aggregated[weekStart].non_consensus += non_consensus;
+ aggregated[weekStart].total += escalated + consensus + non_consensus;
+ });
+
+ return Object.entries(aggregated).map(([date, counts]) => ({
+ date,
+ ...counts,
+ }));
+ }, [vote_data]);
+
+ const totals_data = React.useMemo(() => {
+ return [
+ {
+ id: "consensus",
+ data: dropCurrentPeriod(
+ aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.consensus,
+ })),
+ ),
+ },
+ {
+ id: "escalated",
+ data: dropCurrentPeriod(
+ aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.escalated,
+ })),
+ ),
+ },
+ {
+ id: "non-consensus",
+ data: dropCurrentPeriod(
+ aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.non_consensus,
+ })),
+ ),
+ },
+ ];
+ }, [aggregateDataByWeek]);
+
+ const percent_data = React.useMemo(
+ () => [
+ {
+ id: "consensus",
+ data: aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.consensus / week.total,
+ })),
+ },
+ {
+ id: "escalated",
+ data: aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.escalated / week.total,
+ })),
+ },
+ {
+ id: "non-consensus",
+ data: aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.non_consensus / week.total,
+ })),
+ },
+ ],
+ [aggregateDataByWeek],
+ );
+
+ const chart_theme =
+ data.get("theme") === "light" // (Accessible theme TBD - this assumes accessible is dark for now)
+ ? {
+ /* nivo defaults work well with our light theme */
+ }
+ : {
+ text: { fill: "#FFFFFF" },
+ tooltip: { container: { color: "#111111" } },
+ grid: { line: { stroke: "#444444" } },
+ };
+
+ const line_colors = {
+ consensus: "rgba(0, 128, 0, 1)", // green
+ escalated: "rgba(255, 165, 0, 1)", // orange
+ "non-consensus": "rgba(255, 0, 0, 1)", // red
+ };
+
+ const percent_line_colours = {
+ consensus: "rgba(0, 128, 0, 0.4)",
+ escalated: "rgba(255, 165, 0, 0.4)",
+ "non-consensus": "rgba(255, 0, 0, 0.4)",
+ };
+
+ if (!totals_data[0].data.length) {
+ return
No activity yet
;
+ }
+
+ return (
+
+
+ line_colors[id as keyof typeof line_colors]}
+ animate
+ curve="monotoneX"
+ enablePoints={false}
+ enableSlices="x"
+ axisBottom={{
+ format: "%d %b %g",
+ tickValues: "every week",
+ }}
+ xFormat="time:%Y-%m-%d"
+ xScale={{
+ type: "time",
+ min: format(
+ startOfWeekDateFns(subDays(new Date(), period), { weekStartsOn: 1 }),
+ "yyyy-MM-dd",
+ ),
+ format: "%Y-%m-%d",
+ useUTC: false,
+ precision: "day",
+ }}
+ axisLeft={{
+ tickValues: Array.from(
+ { length: EXPECTED_MAX_WEEKLY_CM_REPORTS / Y_STEP_SIZE + 1 },
+ (_, i) => i * Y_STEP_SIZE,
+ ),
+ }}
+ yScale={{
+ stacked: false,
+ type: "linear",
+ min: 0,
+ max: EXPECTED_MAX_WEEKLY_CM_REPORTS,
+ }}
+ margin={{
+ bottom: 40,
+ left: 60,
+ right: 40,
+ top: 5,
+ }}
+ theme={chart_theme}
+ />
+
+
+
+ percent_line_colours[id as keyof typeof percent_line_colours]
+ }
+ animate
+ curve="monotoneX"
+ enablePoints={false}
+ enableSlices="x"
+ axisBottom={{
+ format: "%d %b %g",
+ tickValues: "every week",
+ }}
+ xFormat="time:%Y-%m-%d"
+ xScale={{
+ type: "time",
+ min: format(
+ startOfWeekDateFns(subDays(new Date(), period), { weekStartsOn: 1 }),
+ "yyyy-MM-dd",
+ ),
+ format: "%Y-%m-%d",
+ useUTC: false,
+ precision: "day",
+ }}
+ axisLeft={{
+ format: (d) => `${Math.round(d * 100)}%`, // Format ticks as percentages
+ tickValues: 6,
+ }}
+ yFormat=" >-.0p"
+ yScale={{
+ stacked: true,
+ type: "linear",
+ }}
+ margin={{
+ bottom: 40,
+ left: 60,
+ right: 40,
+ top: 5,
+ }}
+ theme={chart_theme}
+ />
+
+
+ );
+};
+
+interface CMVotingGroupGraphProps {
+ vote_data: ReportAlignmentCount[];
+ period: number;
+}
+
+export const CMVotingGroupGraph = ({ vote_data, period }: CMVotingGroupGraphProps): JSX.Element => {
+ if (!vote_data) {
+ vote_data = [];
+ }
+
+ const aggregateDataByWeek = React.useMemo(() => {
+ const aggregated: {
+ [key: string]: {
+ escalated: number;
+ unanimous: number;
+ non_unanimous: number;
+ total: number;
+ };
+ } = {};
+
+ vote_data.forEach(({ date, escalated, unanimous, non_unanimous }) => {
+ const weekStart = startOfWeek(new Date(date)).toISOString().slice(0, 10); // Get week start and convert to ISO string for key
+
+ if (!aggregated[weekStart]) {
+ aggregated[weekStart] = { escalated: 0, unanimous: 0, non_unanimous: 0, total: 0 };
+ }
+ aggregated[weekStart].escalated += escalated;
+ aggregated[weekStart].unanimous += unanimous;
+ aggregated[weekStart].non_unanimous += non_unanimous;
+ aggregated[weekStart].total += unanimous + non_unanimous;
+ });
+
+ return Object.entries(aggregated).map(([date, counts]) => ({
+ date,
+ ...counts,
+ }));
+ }, [vote_data]);
+
+ const totals_data = React.useMemo(() => {
+ return [
+ {
+ id: "unanimous",
+ data: dropCurrentPeriod(
+ aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.unanimous,
+ })),
+ ),
+ },
+ {
+ id: "escalated",
+ data: dropCurrentPeriod(
+ aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.escalated,
+ })),
+ ),
+ },
+ {
+ id: "non-unanimous",
+ data: dropCurrentPeriod(
+ aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.non_unanimous,
+ })),
+ ),
+ },
+ ];
+ }, [aggregateDataByWeek]);
+
+ const percent_data = React.useMemo(
+ () => [
+ {
+ id: "unanimous",
+ data: aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.unanimous / week.total,
+ })),
+ },
+ {
+ id: "escalated",
+ data: aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.escalated / week.total,
+ })),
+ },
+ {
+ id: "non-unanimous",
+ data: aggregateDataByWeek.map((week) => ({
+ x: week.date,
+ y: week.non_unanimous / week.total,
+ })),
+ },
+ ],
+ [aggregateDataByWeek],
+ );
+
+ const chart_theme =
+ data.get("theme") === "light" // (Accessible theme TBD - this assumes accessible is dark for now)
+ ? {
+ /* nivo defaults work well with our light theme */
+ }
+ : {
+ text: { fill: "#FFFFFF" },
+ tooltip: { container: { color: "#111111" } },
+ grid: { line: { stroke: "#444444" } },
+ };
+
+ const line_colors = {
+ unanimous: "rgba(0, 128, 0, 1)", // green
+ escalated: "rgba(255, 165, 0, 1)", // orange
+ "non-unanimous": "rgba(255, 0, 0, 1)", // red
+ };
+
+ if (!totals_data[0].data.length) {
+ return No activity yet
;
+ }
+
+ return (
+
+
+ line_colors[id as keyof typeof line_colors]}
+ animate
+ curve="monotoneX"
+ enablePoints={false}
+ enableSlices="x"
+ axisBottom={{
+ format: "%d %b %g",
+ tickValues: "every week",
+ }}
+ xFormat="time:%Y-%m-%d"
+ xScale={{
+ type: "time",
+ min: format(
+ startOfWeekDateFns(subDays(new Date(), period), { weekStartsOn: 1 }),
+ "yyyy-MM-dd",
+ ),
+ format: "%Y-%m-%d",
+ useUTC: false,
+ precision: "day",
+ }}
+ axisLeft={{
+ tickValues: Array.from(
+ { length: EXPECTED_MAX_WEEKLY_CM_REPORTS / Y_STEP_SIZE + 1 },
+ (_, i) => i * Y_STEP_SIZE,
+ ),
+ }}
+ yScale={{
+ stacked: false,
+ type: "linear",
+ min: 0,
+ max: EXPECTED_MAX_WEEKLY_CM_REPORTS,
+ }}
+ margin={{
+ bottom: 40,
+ left: 60,
+ right: 40,
+ top: 5,
+ }}
+ theme={chart_theme}
+ />
+
+
+ line_colors[id as keyof typeof line_colors]}
+ animate
+ curve="monotoneX"
+ enablePoints={false}
+ enableSlices="x"
+ axisBottom={{
+ format: "%d %b %g",
+ tickValues: "every week",
+ }}
+ xFormat="time:%Y-%m-%d"
+ xScale={{
+ type: "time",
+ min: format(
+ startOfWeekDateFns(subDays(new Date(), period), { weekStartsOn: 1 }),
+ "yyyy-MM-dd",
+ ),
+ format: "%Y-%m-%d",
+ useUTC: false,
+ precision: "day",
+ }}
+ axisLeft={{
+ format: (d) => `${Math.round(d * 100)}%`, // Format ticks as percentages
+ tickValues: 6,
+ }}
+ yFormat=" >-.0p"
+ yScale={{
+ stacked: false,
+ type: "linear",
+ max: 1,
+ }}
+ margin={{
+ bottom: 40,
+ left: 60,
+ right: 40,
+ top: 5,
+ }}
+ theme={chart_theme}
+ />
+
+
+ );
+};
diff --git a/src/components/CMVotingGroupGraph/index.ts b/src/components/CMVotingGroupGraph/index.ts
new file mode 100644
index 0000000000..87a7c9ec9f
--- /dev/null
+++ b/src/components/CMVotingGroupGraph/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) Online-Go.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export * from "./CMVotingGraphs";
diff --git a/src/components/NavBar/NavBar.tsx b/src/components/NavBar/NavBar.tsx
index f072e23021..4fac688bf0 100644
--- a/src/components/NavBar/NavBar.tsx
+++ b/src/components/NavBar/NavBar.tsx
@@ -290,12 +290,11 @@ export function NavBar(): JSX.Element {
{_("Rating Calculator")}
- {(user.is_moderator || !!user.moderator_powers) && (
-
-
- {_("Reports Center")}
-
- )}
+
+
+ {_("Reports Center")}
+
+
{user.is_moderator && (
diff --git a/src/views/ReportsCenter/ReportsCenter.tsx b/src/views/ReportsCenter/ReportsCenter.tsx
index ef6b7cfe8c..077962f6ce 100644
--- a/src/views/ReportsCenter/ReportsCenter.tsx
+++ b/src/views/ReportsCenter/ReportsCenter.tsx
@@ -36,6 +36,7 @@ interface OtherView {
special: string;
title: string;
show_cm: boolean;
+ show_all: boolean;
}
const categories: (ReportDescription | OtherView)[] = [
@@ -54,11 +55,11 @@ const categories: (ReportDescription | OtherView)[] = [
},
])
.concat([
- { special: "hr", title: "", show_cm: true },
- { special: "history", title: "History", show_cm: true },
- { special: "cm", title: "Community Moderation", show_cm: true },
- { special: "my_reports", title: "My Own Reports", show_cm: true },
- { special: "settings", title: "Settings", show_cm: false },
+ { special: "hr", title: "", show_cm: true, show_all: false },
+ { special: "history", title: "History", show_cm: true, show_all: false },
+ { special: "cm", title: "Community Moderation", show_cm: true, show_all: true },
+ { special: "my_reports", title: "My Own Reports", show_cm: true, show_all: true },
+ { special: "settings", title: "Settings", show_cm: false, show_all: false },
]);
const category_priorities: { [type: string]: number } = {};
@@ -131,10 +132,6 @@ export function ReportsCenter(): JSX.Element | null {
navigateTo(`/reports-center/${category}`);
}, []);
- if (!user.is_moderator && !user.moderator_powers) {
- return null;
- }
-
const selectReport = (report_id: number) => {
if (report_id) {
navigateTo(`/reports-center/${category}/${report_id}`);
@@ -145,13 +142,15 @@ export function ReportsCenter(): JSX.Element | null {
const visible_categories = user.is_moderator
? categories
- : // community moderators supported report types
- categories.filter(
- (category) =>
- ("special" in category && category.show_cm) ||
- (!("special" in category) &&
- community_mod_has_power(user.moderator_powers, category.type)),
- );
+ : user.moderator_powers
+ ? // community moderators supported report types
+ categories.filter(
+ (category) =>
+ ("special" in category && category.show_cm) ||
+ (!("special" in category) &&
+ community_mod_has_power(user.moderator_powers, category.type)),
+ )
+ : categories.filter((category) => "special" in category && category.show_all);
const my_reports = report_manager
.getEligibleReports()
diff --git a/src/views/ReportsCenter/ReportsCenterCMDashboard.tsx b/src/views/ReportsCenter/ReportsCenterCMDashboard.tsx
index 104f09f796..c48c64e46e 100644
--- a/src/views/ReportsCenter/ReportsCenterCMDashboard.tsx
+++ b/src/views/ReportsCenter/ReportsCenterCMDashboard.tsx
@@ -16,34 +16,26 @@
*/
import React, { useEffect } from "react";
-import { format, subDays, startOfWeek as startOfWeekDateFns } from "date-fns";
import { get } from "@/lib/requests";
-import * as data from "@/lib/data";
-
-import { ResponsiveLine } from "@nivo/line";
import { useUser } from "@/lib/hooks";
import { PaginatedTable } from "@/components/PaginatedTable";
import { Player } from "@/components/Player";
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
-import { llm_pgettext, pgettext } from "@/lib/translate";
+import { llm_pgettext, pgettext, _ } from "@/lib/translate";
import { UserVoteActivityGraph } from "@/views/User";
-import { dropCurrentPeriod } from "@/lib/misc";
-import { CMPieCharts } from "@/views/User";
-interface ReportCount {
- date: string;
- escalated: number;
- consensus: number;
- non_consensus: number;
-}
-
-interface ReportAlignmentCount {
- date: string;
- escalated: number;
- unanimous: number;
- non_unanimous: number;
+import { CMPieCharts } from "@/views/User";
+import {
+ CMVoteCountGraph,
+ CMVotingGroupGraph,
+ ReportAlignmentCount,
+ ReportCount,
+} from "@/components/CMVotingGroupGraph";
+
+interface SystemPerformanceData {
+ [reportType: string]: string;
}
interface CMVotingOutcomeData {
@@ -58,456 +50,13 @@ interface IndividualCMVotingOutcomeData {
user_id: number;
vote_data: CMVotingOutcomeData;
}
-
-function startOfWeek(the_date: Date): Date {
- return startOfWeekDateFns(the_date, { weekStartsOn: 1 }); // 1 = Monday
-}
-
-// Hardcoding the vertical axis of all "report count" graphs as the total number helps convey
-// the relative number of types of reports.
-
-// TBD: it might be nice if this number was dynamically provided by the server, but
-// we are already possibly hitting it hard for these rollups
-
-const EXPECTED_MAX_WEEKLY_CM_REPORTS = 200;
-const Y_STEP_SIZE = 40; // must divide nicely into EXPECTED_MAX_WEEKLY_CM_REPORTS
-
-interface CMVoteCountGraphProps {
- vote_data: ReportCount[];
- period: number;
-}
-
-const CMVoteCountGraph = ({ vote_data, period }: CMVoteCountGraphProps): JSX.Element => {
- if (!vote_data) {
- vote_data = [];
- }
-
- const aggregateDataByWeek = React.useMemo(() => {
- const aggregated: {
- [key: string]: {
- escalated: number;
- consensus: number;
- non_consensus: number;
- total: number;
- };
- } = {};
-
- vote_data.forEach(({ date, escalated, consensus, non_consensus }) => {
- const weekStart = startOfWeek(new Date(date)).toISOString().slice(0, 10); // Get week start and convert to ISO string for key
-
- if (!aggregated[weekStart]) {
- aggregated[weekStart] = { escalated: 0, consensus: 0, non_consensus: 0, total: 0 };
- }
- aggregated[weekStart].escalated += escalated;
- aggregated[weekStart].consensus += consensus;
- aggregated[weekStart].non_consensus += non_consensus;
- aggregated[weekStart].total += escalated + consensus + non_consensus;
- });
-
- return Object.entries(aggregated).map(([date, counts]) => ({
- date,
- ...counts,
- }));
- }, [vote_data]);
-
- const totals_data = React.useMemo(() => {
- return [
- {
- id: "consensus",
- data: dropCurrentPeriod(
- aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.consensus,
- })),
- ),
- },
- {
- id: "escalated",
- data: dropCurrentPeriod(
- aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.escalated,
- })),
- ),
- },
- {
- id: "non-consensus",
- data: dropCurrentPeriod(
- aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.non_consensus,
- })),
- ),
- },
- ];
- }, [aggregateDataByWeek]);
-
- const percent_data = React.useMemo(
- () => [
- {
- id: "consensus",
- data: aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.consensus / week.total,
- })),
- },
- {
- id: "escalated",
- data: aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.escalated / week.total,
- })),
- },
- {
- id: "non-consensus",
- data: aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.non_consensus / week.total,
- })),
- },
- ],
- [aggregateDataByWeek],
- );
-
- const chart_theme =
- data.get("theme") === "light" // (Accessible theme TBD - this assumes accessible is dark for now)
- ? {
- /* nivo defaults work well with our light theme */
- }
- : {
- text: { fill: "#FFFFFF" },
- tooltip: { container: { color: "#111111" } },
- grid: { line: { stroke: "#444444" } },
- };
-
- const line_colors = {
- consensus: "rgba(0, 128, 0, 1)", // green
- escalated: "rgba(255, 165, 0, 1)", // orange
- "non-consensus": "rgba(255, 0, 0, 1)", // red
- };
-
- const percent_line_colours = {
- consensus: "rgba(0, 128, 0, 0.4)",
- escalated: "rgba(255, 165, 0, 0.4)",
- "non-consensus": "rgba(255, 0, 0, 0.4)",
- };
-
- if (!totals_data[0].data.length) {
- return No activity yet
;
- }
-
- return (
-
-
- line_colors[id as keyof typeof line_colors]}
- animate
- curve="monotoneX"
- enablePoints={false}
- enableSlices="x"
- axisBottom={{
- format: "%d %b %g",
- tickValues: "every week",
- }}
- xFormat="time:%Y-%m-%d"
- xScale={{
- type: "time",
- min: format(
- startOfWeekDateFns(subDays(new Date(), period), { weekStartsOn: 1 }),
- "yyyy-MM-dd",
- ),
- format: "%Y-%m-%d",
- useUTC: false,
- precision: "day",
- }}
- axisLeft={{
- tickValues: Array.from(
- { length: EXPECTED_MAX_WEEKLY_CM_REPORTS / Y_STEP_SIZE + 1 },
- (_, i) => i * Y_STEP_SIZE,
- ),
- }}
- yScale={{
- stacked: false,
- type: "linear",
- min: 0,
- max: EXPECTED_MAX_WEEKLY_CM_REPORTS,
- }}
- margin={{
- bottom: 40,
- left: 60,
- right: 40,
- top: 5,
- }}
- theme={chart_theme}
- />
-
-
-
- percent_line_colours[id as keyof typeof percent_line_colours]
- }
- animate
- curve="monotoneX"
- enablePoints={false}
- enableSlices="x"
- axisBottom={{
- format: "%d %b %g",
- tickValues: "every week",
- }}
- xFormat="time:%Y-%m-%d"
- xScale={{
- type: "time",
- min: format(
- startOfWeekDateFns(subDays(new Date(), period), { weekStartsOn: 1 }),
- "yyyy-MM-dd",
- ),
- format: "%Y-%m-%d",
- useUTC: false,
- precision: "day",
- }}
- axisLeft={{
- format: (d) => `${Math.round(d * 100)}%`, // Format ticks as percentages
- tickValues: 6,
- }}
- yFormat=" >-.0p"
- yScale={{
- stacked: true,
- type: "linear",
- }}
- margin={{
- bottom: 40,
- left: 60,
- right: 40,
- top: 5,
- }}
- theme={chart_theme}
- />
-
-
- );
-};
-
-interface CMVotingGroupGraphProps {
- vote_data: ReportAlignmentCount[];
- period: number;
-}
-
-const CMVotingGroupGraph = ({ vote_data, period }: CMVotingGroupGraphProps): JSX.Element => {
- if (!vote_data) {
- vote_data = [];
- }
-
- const aggregateDataByWeek = React.useMemo(() => {
- const aggregated: {
- [key: string]: {
- escalated: number;
- unanimous: number;
- non_unanimous: number;
- total: number;
- };
- } = {};
-
- vote_data.forEach(({ date, escalated, unanimous, non_unanimous }) => {
- const weekStart = startOfWeek(new Date(date)).toISOString().slice(0, 10); // Get week start and convert to ISO string for key
-
- if (!aggregated[weekStart]) {
- aggregated[weekStart] = { escalated: 0, unanimous: 0, non_unanimous: 0, total: 0 };
- }
- aggregated[weekStart].escalated += escalated;
- aggregated[weekStart].unanimous += unanimous;
- aggregated[weekStart].non_unanimous += non_unanimous;
- aggregated[weekStart].total += unanimous + non_unanimous;
- });
-
- return Object.entries(aggregated).map(([date, counts]) => ({
- date,
- ...counts,
- }));
- }, [vote_data]);
-
- const totals_data = React.useMemo(() => {
- return [
- {
- id: "unanimous",
- data: dropCurrentPeriod(
- aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.unanimous,
- })),
- ),
- },
- {
- id: "escalated",
- data: dropCurrentPeriod(
- aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.escalated,
- })),
- ),
- },
- {
- id: "non-unanimous",
- data: dropCurrentPeriod(
- aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.non_unanimous,
- })),
- ),
- },
- ];
- }, [aggregateDataByWeek]);
-
- const percent_data = React.useMemo(
- () => [
- {
- id: "unanimous",
- data: aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.unanimous / week.total,
- })),
- },
- {
- id: "escalated",
- data: aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.escalated / week.total,
- })),
- },
- {
- id: "non-unanimous",
- data: aggregateDataByWeek.map((week) => ({
- x: week.date,
- y: week.non_unanimous / week.total,
- })),
- },
- ],
- [aggregateDataByWeek],
- );
-
- const chart_theme =
- data.get("theme") === "light" // (Accessible theme TBD - this assumes accessible is dark for now)
- ? {
- /* nivo defaults work well with our light theme */
- }
- : {
- text: { fill: "#FFFFFF" },
- tooltip: { container: { color: "#111111" } },
- grid: { line: { stroke: "#444444" } },
- };
-
- const line_colors = {
- unanimous: "rgba(0, 128, 0, 1)", // green
- escalated: "rgba(255, 165, 0, 1)", // orange
- "non-unanimous": "rgba(255, 0, 0, 1)", // red
- };
-
- if (!totals_data[0].data.length) {
- return No activity yet
;
- }
-
- return (
-
-
- line_colors[id as keyof typeof line_colors]}
- animate
- curve="monotoneX"
- enablePoints={false}
- enableSlices="x"
- axisBottom={{
- format: "%d %b %g",
- tickValues: "every week",
- }}
- xFormat="time:%Y-%m-%d"
- xScale={{
- type: "time",
- min: format(
- startOfWeekDateFns(subDays(new Date(), period), { weekStartsOn: 1 }),
- "yyyy-MM-dd",
- ),
- format: "%Y-%m-%d",
- useUTC: false,
- precision: "day",
- }}
- axisLeft={{
- tickValues: Array.from(
- { length: EXPECTED_MAX_WEEKLY_CM_REPORTS / Y_STEP_SIZE + 1 },
- (_, i) => i * Y_STEP_SIZE,
- ),
- }}
- yScale={{
- stacked: false,
- type: "linear",
- min: 0,
- max: EXPECTED_MAX_WEEKLY_CM_REPORTS,
- }}
- margin={{
- bottom: 40,
- left: 60,
- right: 40,
- top: 5,
- }}
- theme={chart_theme}
- />
-
-
- line_colors[id as keyof typeof line_colors]}
- animate
- curve="monotoneX"
- enablePoints={false}
- enableSlices="x"
- axisBottom={{
- format: "%d %b %g",
- tickValues: "every week",
- }}
- xFormat="time:%Y-%m-%d"
- xScale={{
- type: "time",
- min: format(
- startOfWeekDateFns(subDays(new Date(), period), { weekStartsOn: 1 }),
- "yyyy-MM-dd",
- ),
- format: "%Y-%m-%d",
- useUTC: false,
- precision: "day",
- }}
- axisLeft={{
- format: (d) => `${Math.round(d * 100)}%`, // Format ticks as percentages
- tickValues: 6,
- }}
- yFormat=" >-.0p"
- yScale={{
- stacked: false,
- type: "linear",
- max: 1,
- }}
- margin={{
- bottom: 40,
- left: 60,
- right: 40,
- top: 5,
- }}
- theme={chart_theme}
- />
-
-
- );
-};
-
export function ReportsCenterCMDashboard(): JSX.Element {
const user = useUser();
const [selectedTabIndex, setSelectedTabIndex] = React.useState(user.moderator_powers ? 0 : 1);
const [vote_data, setVoteData] = React.useState(null);
const [users_data, setUsersData] = React.useState(null);
+ const [system_data, setSystemData] = React.useState(null);
- // `Tabs` isn't expecting the possibility that the initial tab is not zero.
useEffect(() => {
handleTabSelect(selectedTabIndex);
}, []);
@@ -520,6 +69,8 @@ export function ReportsCenterCMDashboard(): JSX.Element {
fetchVoteData();
} else if (index === 3 && !users_data) {
fetchUsersData();
+ } else if (index === 4 && !system_data) {
+ fetchSystemData();
}
};
@@ -545,6 +96,28 @@ export function ReportsCenterCMDashboard(): JSX.Element {
});
};
+ const fetchSystemData = () => {
+ get(`moderation/system_performance`)
+ .then((response) => {
+ const fetchedData: SystemPerformanceData = response;
+ const currentDate = new Date();
+
+ const performanceAsAge: SystemPerformanceData = Object.fromEntries(
+ Object.entries(fetchedData).map(([reportType, dateString]) => {
+ const date = new Date(dateString);
+ const age = Math.floor(
+ (currentDate.getTime() - date.getTime()) / (1000 * 60 * 60),
+ ); // age in hours
+ return [reportType, age.toString()];
+ }),
+ );
+ setSystemData(performanceAsAge);
+ })
+ .catch((err) => {
+ console.error(err);
+ });
+ };
+
return (
- My Summary
- Group Outcomes
- Individual Votes
- My Votes
+
+ {pgettext("This is a title of a page showing summary graphs", "My Summary")}
+
+
+ {pgettext(
+ "This is a title of a page showing graphs of Community Moderation outcomes",
+ "Group Outcomes",
+ )}
+
+
+ {pgettext(
+ "This is a title of a page showing graphs of individual Community Moderators' votes",
+ "Individual Votes",
+ )}
+
+
+ {pgettext(
+ "This is a title of a page showing a Community Moderator how their votes turned out",
+ "My Votes",
+ )}
+
+
+ {pgettext(
+ "This is a title of a page showing graphs how the report system is performing",
+ "System Performance",
+ )}
+
{/* My Summary: A CM's Summary Pie Charts */}
@@ -581,21 +177,23 @@ export function ReportsCenterCMDashboard(): JSX.Element {
{/* Group Outcomes: The overall CM voting outcomes */}
- {vote_data
- ? ["overall", "escaping", "stalling", "score_cheating"].map((report_type) => (
-
-
{report_type}
- {vote_data[report_type] ? (
-
- ) : (
- "no data"
- )}
-
- ))
- : "loading..."}
+ {vote_data ? (
+ ["overall", "escaping", "stalling", "score_cheating"].map((report_type) => (
+
+
{report_type}
+ {vote_data[report_type] ? (
+
+ ) : (
+ "no data"
+ )}
+
+ ))
+ ) : (
+ {_("loading...")}
+ )}
{/* Individual Votes: Moderator view of each CM's vote categories */}
@@ -625,21 +223,54 @@ export function ReportsCenterCMDashboard(): JSX.Element {
{/* My Votes: A CM's individual vote categories */}
- {users_data
- ? ["overall", "escaping", "stalling", "score_cheating"].map((report_type) => (
-
-
{report_type}
- {users_data[report_type] ? (
-
- ) : (
- "no data"
- )}
-
- ))
- : "loading..."}
+ {users_data ? (
+ ["overall", "escaping", "stalling", "score_cheating"].map((report_type) => (
+
+
{report_type}
+ {users_data[report_type] ? (
+
+ ) : (
+ "no data"
+ )}
+
+ ))
+ ) : (
+ {_("loading...")}
+ )}
+
+
+ {/* System Performance: Info about how the report system is performing */}
+
+ {_("System Performance - oldest open reports")}
+ {system_data ? (
+
+ {Object.entries(system_data).map(([reportType, age]) => {
+ // Cursor came up with this, there's probably a library or something we already have that can do this
+ const ageInHours = parseInt(age, 10);
+ let displayAge;
+ if (ageInHours < 24) {
+ displayAge = `${ageInHours} hour${ageInHours !== 1 ? "s" : ""}`;
+ } else {
+ const days = Math.floor(ageInHours / 24);
+ const hours = ageInHours % 24;
+ displayAge = `${days} day${days !== 1 ? "s" : ""}`;
+ if (hours > 0) {
+ displayAge += ` and ${hours} hour${hours !== 1 ? "s" : ""}`;
+ }
+ }
+ return (
+ -
+ {reportType}: {displayAge}
+
+ );
+ })}
+
+ ) : (
+ {_("loading...")}
+ )}
);