feat: user imports page

This commit is contained in:
zkldi
2022-11-06 00:55:56 +00:00
parent d86e3190e6
commit f609426e1e
6 changed files with 218 additions and 107 deletions
@@ -0,0 +1,124 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import FailedImportsTable from "components/tables/imports/FailedImportsTable";
import ImportsTable from "components/tables/imports/ImportsTable";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import ImportViewerOptions from "components/util/import/ImportViewerOptions";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import SelectLinkButton from "components/util/SelectLinkButton";
import React, { useMemo, useState } from "react";
import { Route, Switch } from "react-router-dom";
import { ImportDocument, ImportTrackerFailed, PublicUserDocument } from "tachi-common";
import { FailedImportDataset, ImportDataset } from "types/tables";
export default function UserImportsPage({ reqUser }: { reqUser: PublicUserDocument }) {
useSetSubheader(
["Users", reqUser.username, "Imports"],
[reqUser],
`${reqUser.username}'s Imports`
);
// honestly yanked straight from ImportAnalysers.tsx.
const [userIntent, setUserIntent] = useState<string | null>(null);
const [importType, setImportType] = useState<string | null>(null);
const params = useMemo(() => {
const p = new URLSearchParams();
if (userIntent) {
p.set("userIntent", userIntent);
}
if (importType) {
p.set("importType", importType);
}
return p;
}, [userIntent, importType]);
return (
<>
<div className="d-flex justify-content-center btn-group">
<SelectLinkButton to={`/dashboard/users/${reqUser.username}/imports`}>
Recent Imports
</SelectLinkButton>
<SelectLinkButton to={`/dashboard/users/${reqUser.username}/imports/failed`}>
Recent Failed Imports
</SelectLinkButton>
</div>
<Divider />
<ImportViewerOptions {...{ userIntent, setUserIntent, importType, setImportType }} />
<Divider />
<Switch>
<Route exact path={`/dashboard/users/${reqUser.username}/imports`}>
<ViewRecentImports params={params} reqUser={reqUser} />
</Route>
<Route path={`/dashboard/users/${reqUser.username}/imports/failed`}>
<div>hi</div>
<ViewRecentFailedImports params={params} reqUser={reqUser} />
</Route>
</Switch>
</>
);
}
function ViewRecentImports({
params,
reqUser,
}: {
params: URLSearchParams;
reqUser: PublicUserDocument;
}) {
const { data, error } = useApiQuery<Array<ImportDocument>>(
`/users/${reqUser.id}/imports?${params.toString()}`
);
if (error) {
return <ApiError error={error} />;
}
if (!data) {
return <Loading />;
}
const dataset: ImportDataset = data.map((e) => ({
__related: {
user: reqUser,
},
...e,
}));
return <ImportsTable dataset={dataset} />;
}
function ViewRecentFailedImports({
params,
reqUser,
}: {
params: URLSearchParams;
reqUser: PublicUserDocument;
}) {
const { data, error } = useApiQuery<Array<ImportTrackerFailed>>(
`/users/${reqUser.id}/imports/failed?${params.toString()}`
);
if (error) {
return <ApiError error={error} />;
}
if (!data) {
return <Loading />;
}
const dataset: FailedImportDataset = data.map((e) => ({
__related: {
user: reqUser,
},
...e,
}));
return <FailedImportsTable dataset={dataset} />;
}
@@ -1,20 +1,18 @@
import { CreateUserMap } from "util/data";
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
import FailedImportsTable from "components/tables/imports/FailedImportsTable";
import ImportsTable from "components/tables/imports/ImportsTable";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import ImportViewerOptions from "components/util/import/ImportViewerOptions";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import Select from "components/util/Select";
import SelectLinkButton from "components/util/SelectLinkButton";
import { TachiConfig } from "lib/config";
import React, { useMemo, useState } from "react";
import { Route, Switch } from "react-router-dom";
import { FailedImportsReturn, ImportsReturn } from "types/api-returns";
import { SetState } from "types/react";
import { FailedImportDataset, ImportDataset } from "types/tables";
import FailedImportsTable from "components/tables/imports/FailedImportsTable";
export default function ImportAnalysers() {
useSetSubheader(["Developer Utils", "Import Analyser"]);
@@ -117,46 +115,3 @@ function ViewRecentFailedImports({ params }: { params: URLSearchParams }) {
return <FailedImportsTable dataset={dataset} />;
}
function ImportViewerOptions({
userIntent,
setUserIntent,
importType,
setImportType,
}: {
userIntent: string | null;
setUserIntent: SetState<string | null>;
importType: string | null;
setImportType: SetState<string | null>;
}) {
return (
<>
<Card header="Options">
<Select
name="User Intent?"
allowNull
unselectedName="Either"
setValue={setUserIntent}
value={userIntent}
>
<option value="true">Yes</option>
<option value="false">No</option>
</Select>
<br />
<Select
name="Import Type?"
allowNull
unselectedName="Any"
setValue={setImportType}
value={importType}
>
{TachiConfig.importTypes.map((e) => (
<option key={e} value={e}>
{e}
</option>
))}
</Select>
</Card>
</>
);
}
+6
View File
@@ -29,6 +29,7 @@ import { useQuery } from "react-query";
import { Redirect, Route, Switch, useHistory, useParams } from "react-router-dom";
import { FormatGame, Game, GetGameConfig, PublicUserDocument, UserGameStats } from "tachi-common";
import { UGPTStatsReturn } from "types/api-returns";
import UserImportsPage from "app/pages/dashboard/users/UserImportsPage";
import ScoresPage from "../pages/dashboard/users/games/_game/_playtype/ScoresPage";
import UserPage from "../pages/dashboard/users/UserPage";
@@ -130,6 +131,11 @@ function UserProfileRoutes({ reqUser }: { reqUser: PublicUserDocument }) {
<UserIntegrationsPage reqUser={reqUser} />
</RequireAuthAsUserParam>
</Route>
<Route path="/dashboard/users/:userID/imports">
<RequireAuthAsUserParam>
<UserImportsPage reqUser={reqUser} />
</RequireAuthAsUserParam>
</Route>
<Route exact path="/dashboard/users/:userID/invites">
<RequireAuthAsUserParam>
<UserInvitesPage reqUser={reqUser} />
@@ -113,6 +113,11 @@ export function UserBottomNav({
Service Integrations
</NavItem>
);
navItems.push(
<NavItem key="imports" to={`${baseUrl}/imports`}>
Imports
</NavItem>
);
// If mandates login, assume that we also use invite codes.
// I'm sure we could set up an elaborate way of doing this by
@@ -0,0 +1,48 @@
import Card from "components/layout/page/Card";
import { TachiConfig } from "lib/config";
import React from "react";
import { SetState } from "types/react";
import Select from "../Select";
export default function ImportViewerOptions({
userIntent,
setUserIntent,
importType,
setImportType,
}: {
userIntent: string | null;
setUserIntent: SetState<string | null>;
importType: string | null;
setImportType: SetState<string | null>;
}) {
return (
<>
<Card header="Options">
<Select
name="Made with intent (i.e. not by an automated hook or IR)"
allowNull
unselectedName="Either"
setValue={setUserIntent}
value={userIntent}
>
<option value="true">Yes</option>
<option value="false">No</option>
</Select>
<br />
<Select
name="Import Type?"
allowNull
unselectedName="Any"
setValue={setImportType}
value={importType}
>
{TachiConfig.importTypes.map((e) => (
<option key={e} value={e}>
{e}
</option>
))}
</Select>
</Card>
</>
);
}
@@ -11,75 +11,48 @@ import type { ImportTrackerDocument, ImportTypes } from "tachi-common";
const router: Router = Router({ mergeParams: true });
/**
* Return minimal information about up to 500 of this user's most recent imports.
* To control where that 500 starts from, pass the timeFinished param.
* Query this user's imports. Returns the 500 most recently-finished imports.
*
* This endpoint is intended to be used by developers to triage certain bugs.
*
* @param timeFinished - Where to start counting this users 500 imports from, this
* should be a unix timestamp in milliseconds.
* @param importType - Optionally, limit the returns to only this import type.
* @param userIntent - Optionally, limit returns to only those with or without userIntent.
*
* @name GET /api/v1/users/:userID/imports
*/
router.get("/", prValidate({ timeFinished: "*string" }), async (req, res) => {
const userID = GetTachiData(req, "requestedUser").id;
router.get(
"/",
prValidate({
importType: p.optional(p.isIn(TachiConfig.IMPORT_TYPES)),
userIntent: p.optional(p.isIn("true", "false")),
}),
async (req, res) => {
const userID = GetTachiData(req, "requestedUser").id;
const importType = req.query.importType as ImportTypes | undefined;
const query = req.query as { timeFinished: string | undefined };
// all query input ends up as strings, so we need convert it into an optional
// boolean
const userIntent =
req.query.userIntent === undefined ? undefined : req.query.userIntent === "true";
let timeFinished = Infinity;
if (IsNonEmptyString(query.timeFinished)) {
timeFinished = Number(query.timeFinished);
if (Number.isNaN(timeFinished)) {
return res.status(400).json({
success: false,
description: `Couldn't read timeFinished as unix milliseconds.`,
});
}
}
const imports = await db.imports.find(
{
const query = {
userIntent,
userID,
timeFinished: { $lte: timeFinished },
},
{
limit: 500,
importType,
};
DeleteUndefinedProps(query);
const imports = await db.imports.find(query, {
sort: { timeFinished: -1 },
}
);
limit: 500,
});
return res.status(200).json({
success: true,
description: `Found ${imports.length} imports.`,
body: imports,
});
});
/**
* Return all of this user's imports that were made with user intent.
*
* Note that we can safely do this without rate limiting, because an import with
* userIntent implies that the user uploaded a file or something similar. Intent
* Imports are not vulnerable to being brutalised by every fervidex upload or similar.
*
* @name GET /api/v1/users/:userID/imports/with-user-intent
*/
router.get("/with-user-intent", async (req, res) => {
const userID = GetTachiData(req, "requestedUser").id;
const importsWithIntent = await db.imports.find({
userID,
userIntent: true,
});
return res.status(200).json({
success: true,
description: `Found ${importsWithIntent.length} imports that were made with user-intent.`,
body: importsWithIntent,
});
});
return res.status(200).json({
success: true,
description: `Found ${imports.length} imports.`,
body: imports,
});
}
);
/**
* Return this users 500 most recent failed imports.