mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-03 04:18:16 +03:00
feat: user imports page
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user