import React, { useContext, useState } from "react";
import { Button, Form, Input, Modal, Popconfirm, Space, Table, Tag } from "antd";
import {
ControlContext, PageHeader, PageLayout, notifyErrorHandler, useAccount,
useColumnSearch, useTableQueryState,
} from "@clusterio/web_ui";
import {
ReportCreateRequest, ReportDeleteRequest, ReportRecord, ReportSetReadRequest,
WatchListAddRequest, WatchListRecord, WatchListRemoveRequest,
} from "../../messages.js";
import { useReports, useWatchList } from "..";
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
/** Modal which reports a player in the name of the web user. */
function CreateReportButton() {
const control = useContext(ControlContext);
const [open, setOpen] = useState(false);
const [form] = Form.useForm<{ playerName: string, reason: string }>();
async function createReport() {
const values = await form.validateFields();
await control.send(new ReportCreateRequest(values.playerName, values.reason));
form.resetFields();
setOpen(false);
}
return <>
{ createReport().catch(notifyErrorHandler("Error creating report")); }}
onCancel={() => setOpen(false)}
destroyOnHidden
>
>;
}
type WatchListValues = { playerName: string, reason: string };
/**
* Modal which adds a player to the watch list, filled in from a report or left empty.
* Mounted only while open, the form keeps its initial values from the first mount.
*/
function WatchListAddModal(props: { values: WatchListValues, onClose: () => void }) {
const control = useContext(ControlContext);
const [form] = Form.useForm();
async function addPlayer() {
const values = await form.validateFields();
await control.send(new WatchListAddRequest(values.playerName, values.reason));
props.onClose();
}
return { addPlayer().catch(notifyErrorHandler("Error adding to watch list")); }}
onCancel={props.onClose}
>
;
}
/**
* Table of every report with search on the player, reporter and reason, and
* filters on the instance and read state.
*/
function ReportsTable(props: { onWatch: (values: WatchListValues) => void }) {
const control = useContext(ControlContext);
const account = useAccount();
const [reports, synced] = useReports();
const [watchList] = useWatchList();
const tableState = useTableQueryState({
namespace: "report",
defaultSortKey: "time",
defaultSortOrder: "descend",
pagination: { defaultPageSize: 50 },
});
const playerSearch = useColumnSearch(tableState, "player", report => report.playerName, "Search players");
const bySearch = useColumnSearch(tableState, "by", report => report.byPlayerName, "Search reporters");
const reasonSearch = useColumnSearch(tableState, "reason", report => report.reason, "Search reasons");
const data = [...reports.values()];
const instanceNames = [...new Set(data.map(report => report.instanceName))].sort(strcmp);
const instanceFilters = instanceNames.map(name => ({ text: name || "Web UI", value: name }));
const canMarkRead = account.hasPermission("exp_reports.report.mark_read");
const canWatch = account.hasPermission("exp_reports.watch_list.add");
const canDelete = account.hasPermission("exp_reports.report.delete");
function setRead(report: ReportRecord, isRead: boolean) {
control.send(new ReportSetReadRequest(report.id, isRead)).catch(notifyErrorHandler("Error updating report"));
}
return report.playerName,
sorter: (a, b) => strcmp(a.playerName, b.playerName),
sortOrder: tableState.sortOrder("player"),
filteredValue: tableState.filteredValue("player"),
...playerSearch,
},
{
title: "Reported by",
key: "by",
render: (_, report) => report.byPlayerName,
sorter: (a, b) => strcmp(a.byPlayerName, b.byPlayerName),
sortOrder: tableState.sortOrder("by"),
filteredValue: tableState.filteredValue("by"),
...bySearch,
},
{
title: "Reason",
key: "reason",
render: (_, report) => report.reason,
filteredValue: tableState.filteredValue("reason"),
...reasonSearch,
},
{
title: "Instance",
key: "instance",
render: (_, report) => report.instanceName || "Web UI",
filters: instanceFilters,
filteredValue: tableState.filteredValue("instance"),
onFilter: (value, report) => report.instanceName === value,
sorter: (a, b) => strcmp(a.instanceName, b.instanceName),
sortOrder: tableState.sortOrder("instance"),
},
{
title: "Time",
key: "time",
render: (_, report) => new Date(report.createdAtMs).toLocaleString(),
sorter: (a, b) => a.createdAtMs - b.createdAtMs,
sortOrder: tableState.sortOrder("time"),
},
{
title: "Status",
key: "status",
render: (_, report) => (report.isRead ? Read : Unread),
filters: [{ text: "Read", value: "read" }, { text: "Unread", value: "unread" }],
filteredValue: tableState.filteredValue("status"),
onFilter: (value, report) => report.isRead === (value === "read"),
},
...canMarkRead || canWatch || canDelete ? [{
key: "actions",
render: (_: unknown, report: ReportRecord) =>
{canMarkRead && }
{canWatch && }
{canDelete && {
control.send(new ReportDeleteRequest(report.id))
.catch(notifyErrorHandler("Error deleting report"));
}}
>
}
,
}] : [],
]}
dataSource={data}
loading={!synced}
rowKey={report => report.id}
pagination={tableState.pagination}
onChange={tableState.onChange}
/>;
}
/** Table of the players whose joins are announced to admins. */
function WatchListTable() {
const control = useContext(ControlContext);
const account = useAccount();
const [watchList, synced] = useWatchList();
const tableState = useTableQueryState({
namespace: "watch",
defaultSortKey: "time",
defaultSortOrder: "descend",
pagination: { defaultPageSize: 50 },
});
const playerSearch = useColumnSearch(
tableState, "player", entry => entry.playerName, "Search players",
);
const reasonSearch = useColumnSearch(tableState, "reason", entry => entry.reason, "Search reasons");
const canRemove = account.hasPermission("exp_reports.watch_list.remove");
return entry.playerName,
sorter: (a, b) => strcmp(a.playerName, b.playerName),
sortOrder: tableState.sortOrder("player"),
filteredValue: tableState.filteredValue("player"),
...playerSearch,
},
{
title: "Reason",
key: "reason",
render: (_, entry) => entry.reason,
filteredValue: tableState.filteredValue("reason"),
...reasonSearch,
},
{
title: "Added by",
key: "by",
render: (_, entry) => entry.byPlayerName,
sorter: (a, b) => strcmp(a.byPlayerName, b.byPlayerName),
sortOrder: tableState.sortOrder("by"),
},
{
title: "Time",
key: "time",
render: (_, entry) => new Date(entry.createdAtMs).toLocaleString(),
sorter: (a, b) => a.createdAtMs - b.createdAtMs,
sortOrder: tableState.sortOrder("time"),
},
...canRemove ? [{
key: "actions",
render: (_: unknown, entry: WatchListRecord) => {
control.send(new WatchListRemoveRequest(entry.playerName))
.catch(notifyErrorHandler("Error removing from watch list"));
}}
>
,
}] : [],
]}
dataSource={[...watchList.values()]}
loading={!synced}
rowKey={entry => entry.playerName}
pagination={tableState.pagination}
onChange={tableState.onChange}
/>;
}
export default function ReportsPage() {
const account = useAccount();
const [watchValues, setWatchValues] = useState();
const canWatch = account.hasPermission("exp_reports.watch_list.add");
return
: undefined}
/>
{account.hasPermission("exp_reports.watch_list.subscribe") && <>
setWatchValues({ playerName: "", reason: "" })}>Add
: undefined}
/>
>}
{watchValues && setWatchValues(undefined)} />}
;
}