feat: basic seeds table rendering

This commit is contained in:
zkldi
2022-09-22 02:46:42 +01:00
parent 947abe5867
commit 7449860198
6 changed files with 328 additions and 56 deletions
@@ -1,5 +1,7 @@
import { FormatTime } from "util/time";
import { APIFetchV1 } from "util/api";
import { LoadSeeds, MakeDataset } from "util/seeds";
import { StrSOV } from "util/sorts";
import useSetSubheader from "components/layout/header/useSetSubheader";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
@@ -13,6 +15,9 @@ import { TachiConfig } from "lib/config";
import ExternalLink from "components/util/ExternalLink";
import Card from "components/layout/page/Card";
import { GitCommit, Revision } from "types/git";
import { AllDatabaseSeeds } from "tachi-common";
import SelectButton from "components/util/SelectButton";
import BMSCourseLookupTable from "components/tables/seeds/BMSCourseLookupTable";
export default function SeedsViewer() {
useSetSubheader(["Developer Utils", "Database Seeds Management"]);
@@ -90,7 +95,9 @@ function InnerSeedsViewer({ hasLocalAPI }: { hasLocalAPI: boolean }) {
</Row>
{baseRev && (
<Row>
<Divider />
<Col xs={12}>
<Divider />
</Col>
<SeedsLoaderViewer baseRev={baseRev} headRev={headRev} />
</Row>
)}
@@ -99,8 +106,92 @@ function InnerSeedsViewer({ hasLocalAPI }: { hasLocalAPI: boolean }) {
}
function SeedsLoaderViewer({ baseRev, headRev }: { baseRev: Revision; headRev: Revision | null }) {
if (headRev !== null) {
return <>temp</>;
const [baseData, setBaseData] = useState<Partial<AllDatabaseSeeds> | null>(null);
const [headData, setHeadData] = useState<Partial<AllDatabaseSeeds> | null>(null);
useEffect(() => {
(async () => {
const data = await LoadSeeds(baseRev.repo, baseRev.c.sha);
setBaseData(data);
})();
}, [baseRev]);
useEffect(() => {
(async () => {
if (headRev === null) {
setHeadData(null);
return;
}
const data = await LoadSeeds(headRev.repo, headRev.c.sha);
setHeadData(data);
})();
}, [headRev]);
if (baseData === null) {
return (
<Col xs={12}>
<div className="d-flex justify-content-center">
<div>
<Loading />
<br />
Reading data...
</div>
</div>
</Col>
);
}
if (headData !== null) {
return <>{Object.keys(headData)}</>;
}
return <SeedsAbsoluteState seedsData={baseData} />;
}
function SeedsAbsoluteState({ seedsData }: { seedsData: Partial<AllDatabaseSeeds> }) {
const files = Object.keys(seedsData).sort(StrSOV((k) => k)) as Array<keyof AllDatabaseSeeds>;
const [file, setFile] = useState(files[0]);
return (
<>
<Col xs={12}>
<Card header="Collections">
<div className="d-flex flex-wrap">
{files.map((e) => (
<div key={e}>
<SelectButton id={e} setValue={setFile} value={file}>
{e} ({seedsData[e]?.length ?? 0})
</SelectButton>
</div>
))}
</div>
</Card>
</Col>
<Col xs={12}>
<Divider />
<h1 className="text-center">{file}</h1>
<Divider />
<SeedsTable data={seedsData} file={file} />
</Col>
</>
);
}
function SeedsTable({
data,
file,
}: {
data: Partial<AllDatabaseSeeds>;
file: keyof AllDatabaseSeeds;
}) {
const dataset = useMemo(() => MakeDataset(file, data), [file, data]);
if (file === "bms-course-lookup.json") {
return <BMSCourseLookupTable dataset={dataset as any} />;
}
return <></>;
@@ -1,15 +1,14 @@
import { NumericSOV, StrSOV } from "util/sorts";
import Muted from "components/util/Muted";
import React from "react";
import { BMSCourseDataset } from "types/tables";
import { BMSCourseWithRelated } from "types/seeds";
import { FormatChart } from "tachi-common";
import TachiTable, { Header } from "../components/TachiTable";
export default function BMSCourseLookupTable({ dataset }: { dataset: BMSCourseDataset }) {
const headers: Header<BMSCourseDataset[0]>[] = [
export default function BMSCourseLookupTable({ dataset }: { dataset: BMSCourseWithRelated[] }) {
const headers: Header<BMSCourseWithRelated>[] = [
["Title", "Title", StrSOV((x) => x.title)],
["Set", "Set", StrSOV((x) => x.set)],
["Playtype", "Playtype", StrSOV((x) => x.playtype)],
["Value", "Value", NumericSOV((x) => x.value)],
["Set (Index)", "Set (Idx)", StrSOV((x) => x.set)],
];
return (
@@ -22,57 +21,45 @@ export default function BMSCourseLookupTable({ dataset }: { dataset: BMSCourseDa
set: (x) => x.set,
md5: (x) => x.md5sums,
value: (x) => x.value,
// todo: figure out how to put song titles in here, maybe?
// don't ask
// it gets all the songtitles and joins them
song: (x) =>
x.__related.entries
.map((e) => {
if (typeof e === "string") {
return `UNKNOWN CHART (${e})`;
}
return FormatChart("bms", e.song, e.chart);
})
.join("\n"),
}}
rowFunction={(x) => <Row data={x} />}
/>
);
}
function Row({ data }: { data: BMSCourseDataset[0] }) {
// md5 hashes are 16 chars long; a bms course may be comprised of any amount of
// md5s. This splits the string every 16 characters into an array.
const md5s = data.md5sums.match(/.{16}/gu);
if (!md5s) {
return <div>TEMP ERRMSG</div>;
}
// sort songs by their md5 appearance.
const songData = [];
for (const md5 of md5s) {
const chart = data.__related.charts.find((e) => e.data.hashMD5 === md5);
if (!chart) {
throw new Error(`MD5 of ${md5} points to chart that doesn't exist.`);
}
const song = data.__related.songs.find((e) => e.id === chart.songID);
if (!song) {
throw new Error(`Song ${chart.songID} does not exist, but has a child chart?`);
}
songData.push({ title: song.title, md5 });
}
function Row({ data }: { data: BMSCourseWithRelated }) {
return (
<tr>
<td>
<span>{data.title}</span>
{songData.map((e) => (
<td className="text-left">
<div className="mb-4">
<strong>{data.title}</strong>
</div>
{data.__related.entries.map((e) => (
<>
<br />
<span>{e.title}</span> <Muted>({e.md5})</Muted>
{typeof e === "string" ? (
<span className="text-danger">UNKNOWN CHART ({e})</span>
) : (
<span>{FormatChart("bms", e.song, e.chart)}</span>
)}
</>
))}
</td>
<td>
<span>{data.set}</span>
{data.playtype} {data.set} ({data.value})
</td>
<td>{data.playtype}</td>
<td>{data.value}</td>
</tr>
);
}
+4 -1
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { CSSProperties } from "react";
import { Button } from "react-bootstrap";
import { ButtonVariant } from "react-bootstrap/esm/types";
import { JustChildren, SetState } from "types/react";
@@ -11,6 +11,7 @@ export default function SelectButton<T>({
onVariant = "primary",
offVariant = "outline-secondary",
disabled = false,
style,
}: {
id: T;
value: T;
@@ -18,12 +19,14 @@ export default function SelectButton<T>({
onVariant?: ButtonVariant;
offVariant?: ButtonVariant;
disabled?: boolean;
style?: CSSProperties;
} & JustChildren) {
return (
<Button
disabled={disabled}
variant={id === value ? onVariant : offVariant}
onClick={() => setValue(id)}
style={style}
>
{children}
</Button>
+106
View File
@@ -0,0 +1,106 @@
import {
BMSCourseDocument,
ChartDocument,
FolderDocument,
Game,
GameToIDStrings,
GoalDocument,
GoalDocumentAny,
GoalDocumentFolder,
GoalDocumentMulti,
GoalDocumentSingle,
IDStrings,
IDStringToGame,
MilestoneDocument,
MilestoneSetDocument,
SongDocument,
TableDocument,
} from "tachi-common";
// To render seeds with their tables properly, we need to conjoin our data with
// any other relevant info. These are used for rendering tables.
export type BMSCourseWithRelated = BMSCourseDocument & {
__related: {
/**
* Entry data is just a string in the case where the chart doesn't exist in
* the seeds. This string is just the MD5 of the chart that was expected.
*/
entries: Array<
| {
chart: ChartDocument<"bms:7K" | "bms:14K">;
song: SongDocument<"bms">;
}
| string
>;
};
};
export type TableWithRelated = TableDocument & {
__related: {
folders: Array<FolderDocument>;
};
};
export type GoalWithRelated =
| (GoalDocumentSingle & {
__related: {
chart: ChartDocument;
song: SongDocument;
};
})
| (GoalDocumentMulti & {
__related: {
charts: Array<ChartDocument>;
songs: Array<SongDocument>;
};
})
| (GoalDocumentFolder & {
__related: {
folder: FolderDocument;
};
})
| GoalDocumentAny;
export type MilestoneSetWithRelated = MilestoneSetDocument & {
__related: {
milestones: Array<MilestoneDocument>;
};
};
export type MilestoneWithRelated = MilestoneDocument & {
__related: {
goals: Array<GoalDocument>;
};
};
export type ChartWithRelated<T extends IDStrings> = ChartDocument<T> & {
__related: {
song: SongDocument<IDStringToGame[T]>;
};
};
type SongSeedsWithRelated = {
[G in Game as `songs-${G}.json`]: Array<SongDocument<G>>;
};
type ChartSeedsWithRelated = {
[G in Game as `charts-${G}.json`]: Array<ChartWithRelated<GameToIDStrings[G]>>;
};
/**
* A lookup type for turning database seeds into their corresponding datasets.
* This involves things like adding __related song and chart info, which is useful
* for rendering.
*/
export type DatabaseSeedsWithRelated = {
"bms-course-lookup.json": Array<BMSCourseWithRelated>;
// intentional: folders don't need to be joined with anything.
"folders.json": Array<FolderDocument>;
"tables.json": Array<TableWithRelated>;
"goals.json": Array<GoalWithRelated>;
"milestone-sets.json": Array<MilestoneSetWithRelated>;
"milestones.json": Array<MilestoneWithRelated>;
} & SongSeedsWithRelated &
ChartSeedsWithRelated;
-8
View File
@@ -1,5 +1,4 @@
import {
BMSCourseDocument,
ChartDocument,
IDStrings,
IDStringToGame,
@@ -50,10 +49,3 @@ export type UGSDataset<I extends IDStrings = IDStrings> = (UserGameStats<I> & {
index: integer;
};
})[];
export type BMSCourseDataset = (BMSCourseDocument & {
__related: {
charts: ChartDocument<"bms:7K"|"bms:14K">[];
songs: SongDocument<"bms">[];
};
})[];
+95 -2
View File
@@ -1,15 +1,25 @@
import { AllDatabaseSeeds, DatabaseSeedNames } from "tachi-common";
import {
AllDatabaseSeeds,
ChartDocument,
CreateSongMap,
DatabaseSeedNames,
Game,
} from "tachi-common";
import { BMSCourseWithRelated, DatabaseSeedsWithRelated } from "types/seeds";
import { APIFetchV1 } from "./api";
/**
* Given a repo and a reference return the status of database-seeds/collections
* as of that commit.
*
* If "WORKING_DIRECTORY" is passed as a ref, and the repository is local, this will
* be inferred as reading from the current state of collections on-disk.
*/
export async function LoadSeeds(repo: string, ref: string): Promise<Partial<AllDatabaseSeeds>> {
if (repo === "local") {
const params = new URLSearchParams();
if (ref) {
if (ref !== "WORKING_DIRECTORY") {
params.set("revision", ref);
}
@@ -54,3 +64,86 @@ export async function LoadSeeds(repo: string, ref: string): Promise<Partial<AllD
throw new Error(`Unknown repository type '${repo}'.`);
}
type NotSongsChartsSeeds = Exclude<
keyof AllDatabaseSeeds,
`songs-${Game}.json` | `charts-${Game}.json`
>;
export function MakeDataset<K extends keyof AllDatabaseSeeds>(
file: K,
data: Partial<AllDatabaseSeeds>
): DatabaseSeedsWithRelated[K] {
if (file.startsWith("songs-")) {
} else if (file.startsWith("charts-")) {
}
const f = file as NotSongsChartsSeeds;
switch (f) {
case "bms-course-lookup.json":
// temporarily hacking around this stuff with -as-any as i can't be bothered
// to figure out how to get the typesystem to accept this.
return RelateBMSCourses(data) as any;
case "folders.json":
case "goals.json":
case "milestone-sets.json":
case "milestones.json":
case "tables.json":
}
throw new Error("i've coooome undonne");
}
function RelateBMSCourses(data: Partial<AllDatabaseSeeds>): BMSCourseWithRelated[] {
const base = data["bms-course-lookup.json"];
if (!base) {
return [];
}
const bmsSongs = data["songs-bms.json"];
const bmsCharts = data["charts-bms.json"];
if (!bmsSongs || !bmsCharts) {
throw new Error(
`Couldn't find songs-bms/charts-bms, but tried to render bms-course-lookup. Not possible?`
);
}
const songMap = CreateSongMap(bmsSongs);
const chartMap = new Map<string, ChartDocument<"bms:7K" | "bms:14K">>();
for (const chart of bmsCharts) {
chartMap.set(chart.data.hashMD5, chart);
}
return base.map((course) => {
// md5 hashes are 32 chars long; a bms course may be comprised of any amount of
// md5s. This splits the string every 32 characters into an array.
const md5s = course.md5sums.match(/.{32}/gu);
if (!md5s) {
throw new Error(`Invalid MD5s in BMS Course? Got ${md5s}.`);
}
const dwr: BMSCourseWithRelated = {
...course,
__related: {
entries: md5s.map((e) => {
if (!chartMap.has(e)) {
return e;
}
return {
chart: chartMap.get(e)!,
song: songMap.get(chartMap.get(e)!.songID)!,
};
}),
},
};
return dwr;
});
}