From 74498601984ded7aa6587fbf72921e60378e4680 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Thu, 22 Sep 2022 02:46:42 +0100 Subject: [PATCH] feat: basic seeds table rendering --- .../app/pages/dashboard/utils/SeedsViewer.tsx | 97 +++++++++++++++- .../tables/seeds/BMSCourseLookupTable.tsx | 71 +++++------- client/src/components/util/SelectButton.tsx | 5 +- client/src/types/seeds.ts | 106 ++++++++++++++++++ client/src/types/tables.ts | 8 -- client/src/util/seeds.ts | 97 +++++++++++++++- 6 files changed, 328 insertions(+), 56 deletions(-) create mode 100644 client/src/types/seeds.ts diff --git a/client/src/app/pages/dashboard/utils/SeedsViewer.tsx b/client/src/app/pages/dashboard/utils/SeedsViewer.tsx index fbb27986b..23168e7bc 100644 --- a/client/src/app/pages/dashboard/utils/SeedsViewer.tsx +++ b/client/src/app/pages/dashboard/utils/SeedsViewer.tsx @@ -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 }) { {baseRev && ( - + + + )} @@ -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 | null>(null); + const [headData, setHeadData] = useState | 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 ( + +
+
+ +
+ Reading data... +
+
+ + ); + } + + if (headData !== null) { + return <>{Object.keys(headData)}; + } + + return ; +} + +function SeedsAbsoluteState({ seedsData }: { seedsData: Partial }) { + const files = Object.keys(seedsData).sort(StrSOV((k) => k)) as Array; + + const [file, setFile] = useState(files[0]); + + return ( + <> + + +
+ {files.map((e) => ( +
+ + {e} ({seedsData[e]?.length ?? 0}) + +
+ ))} +
+
+ + + +

{file}

+ + + + + ); +} + +function SeedsTable({ + data, + file, +}: { + data: Partial; + file: keyof AllDatabaseSeeds; +}) { + const dataset = useMemo(() => MakeDataset(file, data), [file, data]); + + if (file === "bms-course-lookup.json") { + return ; } return <>; diff --git a/client/src/components/tables/seeds/BMSCourseLookupTable.tsx b/client/src/components/tables/seeds/BMSCourseLookupTable.tsx index 8d25c85f1..24a68d62b 100644 --- a/client/src/components/tables/seeds/BMSCourseLookupTable.tsx +++ b/client/src/components/tables/seeds/BMSCourseLookupTable.tsx @@ -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[] = [ +export default function BMSCourseLookupTable({ dataset }: { dataset: BMSCourseWithRelated[] }) { + const headers: Header[] = [ ["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) => } /> ); } -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
TEMP ERRMSG
; - } - - // 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 ( - - {data.title} - {songData.map((e) => ( + +
+ {data.title} +
+ {data.__related.entries.map((e) => ( <>
- {e.title} ({e.md5}) + {typeof e === "string" ? ( + UNKNOWN CHART ({e}) + ) : ( + {FormatChart("bms", e.song, e.chart)} + )} ))} - {data.set} + {data.playtype} {data.set} ({data.value}) - {data.playtype} - {data.value} ); } diff --git a/client/src/components/util/SelectButton.tsx b/client/src/components/util/SelectButton.tsx index 715df6dcb..4010a9383 100644 --- a/client/src/components/util/SelectButton.tsx +++ b/client/src/components/util/SelectButton.tsx @@ -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({ onVariant = "primary", offVariant = "outline-secondary", disabled = false, + style, }: { id: T; value: T; @@ -18,12 +19,14 @@ export default function SelectButton({ onVariant?: ButtonVariant; offVariant?: ButtonVariant; disabled?: boolean; + style?: CSSProperties; } & JustChildren) { return ( diff --git a/client/src/types/seeds.ts b/client/src/types/seeds.ts new file mode 100644 index 000000000..e3455da7a --- /dev/null +++ b/client/src/types/seeds.ts @@ -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; + }; +}; + +export type GoalWithRelated = + | (GoalDocumentSingle & { + __related: { + chart: ChartDocument; + song: SongDocument; + }; + }) + | (GoalDocumentMulti & { + __related: { + charts: Array; + songs: Array; + }; + }) + | (GoalDocumentFolder & { + __related: { + folder: FolderDocument; + }; + }) + | GoalDocumentAny; + +export type MilestoneSetWithRelated = MilestoneSetDocument & { + __related: { + milestones: Array; + }; +}; + +export type MilestoneWithRelated = MilestoneDocument & { + __related: { + goals: Array; + }; +}; + +export type ChartWithRelated = ChartDocument & { + __related: { + song: SongDocument; + }; +}; + +type SongSeedsWithRelated = { + [G in Game as `songs-${G}.json`]: Array>; +}; + +type ChartSeedsWithRelated = { + [G in Game as `charts-${G}.json`]: Array>; +}; + +/** + * 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; + + // intentional: folders don't need to be joined with anything. + "folders.json": Array; + + "tables.json": Array; + "goals.json": Array; + "milestone-sets.json": Array; + "milestones.json": Array; +} & SongSeedsWithRelated & + ChartSeedsWithRelated; diff --git a/client/src/types/tables.ts b/client/src/types/tables.ts index 7f1c01da1..211ba4d7c 100644 --- a/client/src/types/tables.ts +++ b/client/src/types/tables.ts @@ -1,5 +1,4 @@ import { - BMSCourseDocument, ChartDocument, IDStrings, IDStringToGame, @@ -50,10 +49,3 @@ export type UGSDataset = (UserGameStats & { index: integer; }; })[]; - -export type BMSCourseDataset = (BMSCourseDocument & { - __related: { - charts: ChartDocument<"bms:7K"|"bms:14K">[]; - songs: SongDocument<"bms">[]; - }; -})[]; diff --git a/client/src/util/seeds.ts b/client/src/util/seeds.ts index 8da0ecf72..313b6f180 100644 --- a/client/src/util/seeds.ts +++ b/client/src/util/seeds.ts @@ -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> { 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; + +export function MakeDataset( + file: K, + data: Partial +): 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): 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>(); + + 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; + }); +}