Merge pull request #72 from storytold/bflat-moderation-controls

most needed moderations pages
This commit is contained in:
Brandon Thomas authored and GitHub committed 2022-07-29 22:16:42 -04:00
commit 49cef534e6
3 files changed
+406 -333

No files matched your search

@@ -1,32 +1,32 @@
import React, { useEffect, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { ApiConfig } from '@storyteller/components';
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
import { formatDistance } from 'date-fns';
import { BackLink } from '../../_common/BackLink';
import { FrontendUrlConfig } from '../../../../common/FrontendUrlConfig';
import React, { useEffect, useState } from "react";
import { Link, useHistory } from "react-router-dom";
import { ApiConfig } from "@storyteller/components";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { formatDistance } from "date-fns";
import { BackLink } from "../../_common/BackLink";
import { FrontendUrlConfig } from "../../../../common/FrontendUrlConfig";
interface Props {
sessionWrapper: SessionWrapper,
sessionWrapper: SessionWrapper;
}
interface IpBanListResponse {
success: boolean,
ip_address_bans: Array<IpBanListItem>,
success: boolean;
ip_address_bans: Array<IpBanListItem>;
}
interface IpBanListItem {
ip_address: string,
maybe_target_user_token: string,
maybe_target_username: string,
ip_address: string;
maybe_target_user_token: string;
maybe_target_username: string;
mod_user_token: string,
mod_username: string,
mod_display_name: string,
mod_notes: string,
mod_user_token: string;
mod_username: string;
mod_display_name: string;
mod_notes: string;
created_at: string,
updated_at: string,
created_at: string;
updated_at: string;
}
function ModerationIpBanListFc(props: Props) {
@@ -43,35 +43,35 @@ function ModerationIpBanListFc(props: Props) {
const endpointUrl = api.getModerationIpBanList();
fetch(endpointUrl, {
method: 'GET',
method: "GET",
headers: {
'Accept': 'application/json',
Accept: "application/json",
},
credentials: 'include',
credentials: "include",
})
.then(res => res.json())
.then(res => {
const response : IpBanListResponse = res;
if (!response.success) {
return;
}
.then((res) => res.json())
.then((res) => {
const response: IpBanListResponse = res;
if (!response.success) {
return;
}
setIpBanList(response.ip_address_bans)
})
.catch(e => {
//this.props.onSpeakErrorCallback();
});
setIpBanList(response.ip_address_bans);
})
.catch((e) => {
//this.props.onSpeakErrorCallback();
});
}, []); // NB: Empty array dependency sets to run ONLY on mount
const handleNewIpAddressChange = (ev: React.FormEvent<HTMLInputElement>) => {
setNewIpAddress((ev.target as HTMLInputElement).value)
setNewIpAddress((ev.target as HTMLInputElement).value);
};
const handleModNotesChange = (ev: React.FormEvent<HTMLInputElement>) => {
setModNotes((ev.target as HTMLInputElement).value)
setModNotes((ev.target as HTMLInputElement).value);
};
const handleFormSubmit = (ev: React.FormEvent<HTMLFormElement>) : boolean => {
const handleFormSubmit = (ev: React.FormEvent<HTMLFormElement>): boolean => {
ev.preventDefault();
const api = new ApiConfig();
@@ -80,41 +80,43 @@ function ModerationIpBanListFc(props: Props) {
const request = {
ip_address: newIpAddress,
mod_notes: modNotes,
}
};
fetch(endpointUrl, {
method: 'POST',
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
Accept: "application/json",
"Content-Type": "application/json",
},
credentials: 'include',
credentials: "include",
body: JSON.stringify(request),
})
.then(res => res.json())
.then(res => {
if (res.success) {
history.go(0); // NB: Force reload
}
})
.catch(e => {});
.then((res) => res.json())
.then((res) => {
if (res.success) {
history.go(0); // NB: Force reload
}
})
.catch((e) => {});
return false;
}
};
if (!props.sessionWrapper.canBanUsers()) {
return <h1>Unauthorized</h1>;
}
const now = new Date();
let rows : Array<JSX.Element> = [];
let rows: Array<JSX.Element> = [];
ipBanList.forEach(ban => {
ipBanList.forEach((ban) => {
const modUserLink = `/profile/${ban.mod_username}`;
const viewBanLink = `/moderation/ip_bans/${ban.ip_address}`;
const createTime = new Date(ban.created_at);
const relativeCreateTime = formatDistance(createTime, now, { addSuffix: true });
const relativeCreateTime = formatDistance(createTime, now, {
addSuffix: true,
});
rows.push(
<tr key={ban.ip_address}>
@@ -125,94 +127,104 @@ function ModerationIpBanListFc(props: Props) {
<td>{ban.mod_notes}</td>
<td>{relativeCreateTime}</td>
<td>
<Link to={viewBanLink}>view / edit</Link>
<Link to={viewBanLink} className="btn btn-primary">
View/Edit
</Link>
</td>
</tr>
)
);
});
return (
<div>
<h1 className="title is-1"> Moderation Ip Ban List </h1>
<p>
<BackLink link={FrontendUrlConfig.moderationMain()} text="Back to moderation" />
</p>
<br />
<p>IP Address bans will prevent bad actors from using and abusing the website.</p>
<br />
<h3 className="title is-3"> Create Ban </h3>
<div className="container py-5">
<h1 className="display-5 fw-bold">Moderation IP Ban List</h1>
<p>
IP Address bans will prevent bad actors from using and abusing the
website.
</p>
<div className="pt-4">
<BackLink
link={FrontendUrlConfig.moderationMain()}
text="Back to moderation"
/>
</div>
</div>
<form onSubmit={handleFormSubmit}>
<div className="field">
<label className="label">Ip Address</label>
<div className="control has-icons-left has-icons-right">
<input
onChange={handleNewIpAddressChange}
className="input"
type="text"
placeholder="IP Address, eg. 255.255.255.255"
value={newIpAddress}
/>
<span className="icon is-small is-left">
<i className="fas fa-envelope"></i>
</span>
<span className="icon is-small is-right">
<i className="fas fa-exclamation-triangle"></i>
</span>
<div className="container-panel pt-3 pb-4">
<div className="panel p-3 p-lg-4">
<h2 className="panel-title fw-bold">Create Ban</h2>
<div className="py-6">
<div className="d-flex flex-column gap-4">
<div>
<label className="sub-title">IP Address</label>
<div className="form-group">
<input
onChange={handleNewIpAddressChange}
className="form-control"
type="text"
placeholder="IP Address, eg. 255.255.255.255"
value={newIpAddress}
/>
<span className="icon is-small is-left">
<i className="fas fa-envelope"></i>
</span>
<span className="icon is-small is-right">
<i className="fas fa-exclamation-triangle"></i>
</span>
</div>
{/*<p className="help">{invalidReason}</p>*/}
</div>
<div>
<label className="sub-title">Moderator Notes</label>
<div className="form-group">
<input
onChange={handleModNotesChange}
className="form-control"
type="text"
placeholder="Notes / reason for ban"
value={modNotes}
/>
<span className="icon is-small is-left">
<i className="fas fa-envelope"></i>
</span>
<span className="icon is-small is-right">
<i className="fas fa-exclamation-triangle"></i>
</span>
</div>
{/*<p className="help">{invalidReason}</p>*/}
</div>
</div>
</div>
</div>
{/*<p className="help">{invalidReason}</p>*/}
</div>
<div className="field">
<label className="label">Moderator Notes</label>
<div className="control has-icons-left has-icons-right">
<input
onChange={handleModNotesChange}
className="input"
type="text"
placeholder="Notes / reason for ban"
value={modNotes}
/>
<span className="icon is-small is-left">
<i className="fas fa-envelope"></i>
</span>
<span className="icon is-small is-right">
<i className="fas fa-exclamation-triangle"></i>
</span>
</div>
{/*<p className="help">{invalidReason}</p>*/}
<div className="container pt-3 pb-5">
<button className="btn btn-primary w-100">Create Ban</button>
</div>
<button className="button is-danger is-large is-fullwidth">Create Ban</button>
</form>
<br />
<br />
<h3 className="title is-3"> Existing Bans </h3>
<table className="table">
<thead>
<tr>
<th>IP Address</th>
<th>Moderator</th>
<th>Moderator Notes</th>
<th>Created At</th>
<th>View / Edit</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<div className="container-panel py-5">
<div className="panel p-3 p-lg-4">
<h2 className="panel-title fw-bold">Existing Bans</h2>
<div className="py-6 table-responsive">
<table className="table">
<thead>
<tr>
<th>IP Address</th>
<th>Moderator</th>
<th>Moderator Notes</th>
<th>Created At</th>
<th>View / Edit</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
</div>
</div>
</div>
</div>
)
);
}
export { ModerationIpBanListFc };
@@ -1,25 +1,39 @@
import React, { useCallback, useEffect, useState } from 'react';
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
import { FrontendUrlConfig } from '../../../../common/FrontendUrlConfig';
import { BackLink } from '../../_common/BackLink';
import { GetTtsInferenceStats, GetTtsInferenceStatsIsOk } from '../../../api/moderation/GetTtsInferenceStats';
import { GetW2lInferenceStats, GetW2lInferenceStatsIsOk } from '../../../api/moderation/GetW2lInferenceStats';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBomb, faRedo } from '@fortawesome/free-solid-svg-icons';
import { KillAction, KillTtsInferenceJobsIsSuccess, KillTtsInferenceJobs } from '@storyteller/components/src/api/moderation/tts/KillTtsInferenceJobs';
import React, { useCallback, useEffect, useState } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { FrontendUrlConfig } from "../../../../common/FrontendUrlConfig";
import { BackLink } from "../../_common/BackLink";
import {
GetTtsInferenceStats,
GetTtsInferenceStatsIsOk,
} from "../../../api/moderation/GetTtsInferenceStats";
import {
GetW2lInferenceStats,
GetW2lInferenceStatsIsOk,
} from "../../../api/moderation/GetW2lInferenceStats";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBomb, faRedo } from "@fortawesome/free-solid-svg-icons";
import {
KillAction,
KillTtsInferenceJobsIsSuccess,
KillTtsInferenceJobs,
} from "@storyteller/components/src/api/moderation/tts/KillTtsInferenceJobs";
interface Props {
sessionWrapper: SessionWrapper,
sessionWrapper: SessionWrapper;
}
function ModerationJobStatsFc(props: Props) {
// NB: We have more TTS stats than W2L stats now.
const [ttsSecondsSinceFirst, setTtsSecondsSinceFirst] = useState<number>(-1);
const [ttsPendingJobCount, setTtsPendingJobCount] = useState<number>(-1);
const [ttsPendingPriorityNonzeroJobCount, setTtsPendingPriorityNonzeroJobCount] = useState<number>(-1);
const [ttsPendingPriorityGtOneJobCount, setTtsPendingPriorityGtOneJobCount] = useState<number>(-1);
const [ttsAttemptFailedJobCount, setTtsAttemptFailedJobCount] = useState<number>(-1);
const [
ttsPendingPriorityNonzeroJobCount,
setTtsPendingPriorityNonzeroJobCount,
] = useState<number>(-1);
const [ttsPendingPriorityGtOneJobCount, setTtsPendingPriorityGtOneJobCount] =
useState<number>(-1);
const [ttsAttemptFailedJobCount, setTtsAttemptFailedJobCount] =
useState<number>(-1);
const [w2lPendingJobCount, setW2lPendingJobCount] = useState<number>(-1);
const [w2lSecondsSinceFirst, setW2lSecondsSinceFirst] = useState<number>(-1);
@@ -29,12 +43,16 @@ function ModerationJobStatsFc(props: Props) {
if (GetTtsInferenceStatsIsOk(response)) {
setTtsSecondsSinceFirst(response.seconds_since_first);
setTtsPendingJobCount(response.pending_count);
setTtsPendingPriorityNonzeroJobCount(response.pending_priority_nonzero_count);
setTtsPendingPriorityGtOneJobCount(response.pending_priority_gt_one_count);
setTtsPendingPriorityNonzeroJobCount(
response.pending_priority_nonzero_count
);
setTtsPendingPriorityGtOneJobCount(
response.pending_priority_gt_one_count
);
setTtsAttemptFailedJobCount(response.attempt_failed_count);
}
}, []);
const getW2lStats = useCallback(async () => {
const response = await GetW2lInferenceStats();
if (GetW2lInferenceStatsIsOk(response)) {
@@ -54,26 +72,26 @@ function ModerationJobStatsFc(props: Props) {
const killPending = async () => {
await doKill(KillAction.AllPending);
}
};
const killPendingAndFailed = async () => {
await doKill(KillAction.AllPendingAndFailed);
}
};
const killZeroPriorityPending = async () => {
await doKill(KillAction.ZeroPriorityPending);
}
};
const doKill = async(killAction: KillAction) => {
const doKill = async (killAction: KillAction) => {
const response = await KillTtsInferenceJobs(killAction);
if (KillTtsInferenceJobsIsSuccess(response)) {
reloadStats();
}
}
};
if (ttsPendingJobCount === -1 && w2lPendingJobCount === -1) {
return <div />
return <div />;
}
if (!props.sessionWrapper.canBanUsers()) {
@@ -85,148 +103,161 @@ function ModerationJobStatsFc(props: Props) {
return (
<div>
<h1 className="title is-1"> Job Stats </h1>
<div className="container py-5">
<h1 className="display-5 fw-bold">Job Stats</h1>
<div className="pt-3">
<BackLink
link={FrontendUrlConfig.moderationMain()}
text="Back to moderation"
/>
</div>
</div>
<BackLink link={FrontendUrlConfig.moderationMain()} text="Back to moderation" />
<div className="container-panel pt-3 pb-5">
<div className="panel p-3 p-lg-4">
<h2 className="panel-title fw-bold">TTS Stats</h2>
<div className="py-6">
<table className="table is-fullwidth is-bordered is-striped">
<thead>
<tr>
<th colSpan={2} className="is-info">
<u>Misc Stats</u>
</th>
</tr>
</thead>
<tbody>
<tr>
<th>TTS wait time</th>
<td>{ttsWait}</td>
</tr>
</tbody>
<thead>
<tr>
<th colSpan={2} className="is-info">
<u>Failed Jobs by Type</u>
</th>
</tr>
</thead>
<tbody>
<tr>
<th>Pending Jobs (All)</th>
<td>{ttsPendingJobCount} pending </td>
</tr>
<tr>
<th>Pending Jobs (Priority &gt; 0)</th>
<td>{ttsPendingPriorityNonzeroJobCount} pending </td>
</tr>
<tr>
<th>Pending Jobs (Priority &gt; 1)</th>
<td>{ttsPendingPriorityGtOneJobCount} pending </td>
</tr>
<tr>
<th>Failed Jobs w/ Retry</th>
<td>{ttsAttemptFailedJobCount} pending </td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br />
<br />
<h2 className="title is-3">TTS Stats</h2>
<div className="container-panel pt-3 pb-5">
<div className="panel p-3 p-lg-4">
<h2 className="panel-title fw-bold">W2L Stats</h2>
<div className="py-6">
<table className="table is-fullwidth is-bordered is-striped">
<thead>
<tr>
<th colSpan={2} className="is-info">
<u>Misc Stats</u>
</th>
</tr>
</thead>
<tbody>
<tr>
<th>W2L wait time</th>
<td>{w2lWait}</td>
</tr>
</tbody>
<thead>
<tr>
<th colSpan={2} className="is-info">
<u>Failed Jobs by Type</u>
</th>
</tr>
</thead>
<tbody>
<tr>
<th>Pending Jobs (All)</th>
<td>{w2lPendingJobCount} pending </td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<table className="table is-fullwidth is-bordered is-striped">
<thead>
<tr>
<th colSpan={2} className="is-info"><u>Misc Stats</u></th>
</tr>
</thead>
<tbody>
<tr>
<th>
TTS wait time
</th>
<td>{ttsWait}</td>
</tr>
</tbody>
</table>
<div className="container-panel pt-3 pb-5">
<div className="panel p-3 p-lg-4">
<h2 className="panel-title fw-bold">Reload Job Stats</h2>
<div className="py-6">
<button
className="btn btn-primary w-100"
onClick={() => reloadStats()}
>
<FontAwesomeIcon icon={faRedo} className="me-2" />
Reload
</button>
</div>
</div>
</div>
<table className="table is-fullwidth is-bordered is-striped">
<thead>
<tr>
<th colSpan={2} className="is-info"><u>Failed Jobs by Type</u></th>
</tr>
</thead>
<tbody>
<tr>
<th>
Pending Jobs (All)
</th>
<td>{ttsPendingJobCount} pending </td>
</tr>
<tr>
<th>
Pending Jobs (Priority &gt; 0)
</th>
<td>{ttsPendingPriorityNonzeroJobCount} pending </td>
</tr>
<tr>
<th>
Pending Jobs (Priority &gt; 1)
</th>
<td>{ttsPendingPriorityGtOneJobCount} pending </td>
</tr>
<tr>
<th>
Failed Jobs w/ Retry
</th>
<td>{ttsAttemptFailedJobCount} pending </td>
</tr>
</tbody>
</table>
<div className="container-panel pt-3 pb-5">
<div className="panel p-3 p-lg-4">
<h2 className="panel-title fw-bold">Kill Stuck Jobs (Danger Zone)</h2>
<div className="py-6">
<h5 className="fw-semibold mb-4">(Please don't do this.)</h5>
<div className="d-flex flex-column gap-3">
<button
className="btn btn-destructive w-100"
onClick={() => killZeroPriorityPending()}
>
Kill Zero-Priority Pending TTS
</button>
<br />
<button
className="btn btn-destructive w-100"
onClick={() => killPending()}
>
Kill ALL Pending TTS&nbsp;
<FontAwesomeIcon icon={faBomb} />
</button>
<h2 className="title is-3">W2L Stats</h2>
<button
className="btn btn-destructive w-100"
onClick={() => killPendingAndFailed()}
>
Kill ALL Pending and Failed TTS&nbsp;
<FontAwesomeIcon icon={faBomb} />
</button>
</div>
</div>
</div>
</div>
<table className="table is-fullwidth is-bordered is-striped">
<thead>
<tr>
<th colSpan={2} className="is-info"><u>Misc Stats</u></th>
</tr>
</thead>
<tbody>
<tr>
<th>
W2L wait time
</th>
<td>{w2lWait}</td>
</tr>
</tbody>
</table>
<table className="table is-fullwidth is-bordered is-striped">
<thead>
<tr>
<th colSpan={2} className="is-info"><u>Failed Jobs by Type</u></th>
</tr>
</thead>
<tbody>
<tr>
<th>
Pending Jobs (All)
</th>
<td>{w2lPendingJobCount} pending </td>
</tr>
</tbody>
</table>
<br />
<h2 className="title is-4">Reload Job Stats</h2>
<button
className="button is-large is-primary is-fullwidth"
onClick={() => reloadStats()}
>Reload&nbsp;<FontAwesomeIcon icon={faRedo} /></button>
<br />
<br />
<h2 className="title is-4">Kill Stuck Jobs (Danger Zone)</h2>
<h2 className="subtitle is-6">(Please don't do this.)</h2>
<button
className="button is-large is-warning is-fullwidth"
onClick={() => killZeroPriorityPending()}
>Kill Zero-Priority Pending TTS</button>
<br />
<button
className="button is-large is-danger is-fullwidth"
onClick={() => killPending()}
>Kill ALL Pending TTS&nbsp;<FontAwesomeIcon icon={faBomb} /></button>
<br />
<button
className="button is-large is-danger is-fullwidth"
onClick={() => killPendingAndFailed()}
>Kill ALL Pending and Failed TTS&nbsp;<FontAwesomeIcon icon={faBomb} /></button>
<br />
<br />
<BackLink link={FrontendUrlConfig.moderationMain()} text="Back to moderation" />
<div className="container pb-5">
<BackLink
link={FrontendUrlConfig.moderationMain()}
text="Back to moderation"
/>
</div>
</div>
)
);
}
function humanWaitTime(seconds: number) : string {
function humanWaitTime(seconds: number): string {
if (seconds === -1) {
return 'error';
return "error";
} else if (seconds < 60) {
return `${seconds} seconds`;
} else if (seconds < 60 * 60) {
@@ -1,74 +1,104 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { FrontendUrlConfig } from '../../../../common/FrontendUrlConfig';
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
import React from "react";
import { Link } from "react-router-dom";
import { FrontendUrlConfig } from "../../../../common/FrontendUrlConfig";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faBan,
faClipboardCheck,
faListCheck,
faMicrophone,
faTags,
faUsers,
} from "@fortawesome/free-solid-svg-icons";
interface Props {
sessionWrapper: SessionWrapper,
sessionWrapper: SessionWrapper;
}
function ModerationFc(props: Props) {
if (!props.sessionWrapper.canBanUsers()) {
return <h1>Unauthorized</h1>;
}
return (
<div>
<h1 className="title is-1"> Moderation Controls </h1>
<div className="container py-5">
<h1 className="display-5 fw-bold text-center text-lg-start">
Moderation Controls
</h1>
</div>
<h3 className="title is-4"> Emergency </h3>
<Link
to="/moderation/ip_bans"
className="button is-link is-large is-fullwidth is-light"
>IP Bans</Link>
<br />
<div className="container-panel pt-3 pb-5">
<div className="panel p-3 p-lg-4">
<h2 className="panel-title fw-bold">Emergency</h2>
<div className="py-6">
<div className="d-flex flex-column gap-3">
<Link to="/moderation/ip_bans" className="btn btn-primary w-100">
<FontAwesomeIcon icon={faBan} className="me-2" />
IP Bans
</Link>
<Link
to="/moderation/job_stats"
className="btn btn-primary w-100"
>
<FontAwesomeIcon icon={faListCheck} className="me-2" />
Job Stats
</Link>
</div>
</div>
</div>
</div>
<Link
to="/moderation/job_stats"
className="button is-link is-large is-fullwidth is-light"
>Job Stats</Link>
<br />
<div className="container-panel pt-3 pb-5">
<div className="panel p-3 p-lg-4">
<h2 className="panel-title fw-bold">Lookup, Stats, and Editing</h2>
<div className="py-6">
<div className="d-flex flex-column gap-3">
<Link
to="/moderation/user/list"
className="btn btn-secondary w-100"
>
<FontAwesomeIcon icon={faUsers} className="me-2" />
User List
</Link>
<h3 className="title is-4"> Lookup, Stats, and Editing </h3>
<Link
to={FrontendUrlConfig.moderationTtsCategoryList()}
className="btn btn-secondary w-100"
>
<FontAwesomeIcon icon={faTags} className="me-2" />
Manage TTS Categories
</Link>
<Link
to="/moderation/user/list"
className="button is-link is-large is-fullwidth is-light"
>User List</Link>
<Link
to="/moderation/approve/w2l_templates"
className="btn btn-secondary w-100"
>
<FontAwesomeIcon icon={faClipboardCheck} className="me-2" />
Unapproved W2L Templates
</Link>
<br />
<Link
to={FrontendUrlConfig.moderationTtsCategoryList()}
className="button is-link is-large is-fullwidth is-light"
>Manage TTS Categories</Link>
<br />
<Link
to="/moderation/approve/w2l_templates"
className="button is-link is-large is-fullwidth is-light"
>Unapproved W2L Templates</Link>
<br />
<Link
to="/moderation/voice_stats"
className="button is-link is-large is-fullwidth is-light"
>Voice Stats</Link>
<br />
<p>
More mod controls will be added in the future: user roles, activity tracking,
timed bans, account bans, etc.
</p>
<Link
to="/moderation/voice_stats"
className="btn btn-secondary w-100"
>
<FontAwesomeIcon icon={faMicrophone} className="me-2" />
Voice Stats
</Link>
</div>
</div>
</div>
</div>
<div className="container pb-5">
<p>
More mod controls will be added in the future: user roles, activity
tracking, timed bans, account bans, etc.
</p>
</div>
</div>
)
);
}
export { ModerationFc };