Add secret HTML5 conversion page at /conversion

A lil page for us to keep track of Neopets's HTML5 conversion progress!
This commit is contained in:
Emi Matchu 2021-01-20 10:36:46 -08:00
parent 6da2ddb453
commit 275d1d62ab
5 changed files with 202 additions and 2 deletions

View file

@ -30,6 +30,7 @@ const tryLoadable = (load, options) =>
options
);
const ConversionPage = tryLoadable(() => import("./ConversionPage"));
const HomePage = tryLoadable(() => import("./HomePage"));
const ItemSearchPage = tryLoadable(() => import("./ItemSearchPage"));
const ItemPage = tryLoadable(() => import("./ItemPage"));
@ -162,6 +163,11 @@ function App() {
<PrivacyPolicyPage />
</PageLayout>
</Route>
<Route path="/conversion">
<PageLayout>
<ConversionPage />
</PageLayout>
</Route>
<Route path="/">
<PageLayout hideHomeLink>
<HomePage />

107
src/app/ConversionPage.js Normal file
View file

@ -0,0 +1,107 @@
import React from "react";
import {
Box,
CircularProgress,
CircularProgressLabel,
Flex,
Stack,
} from "@chakra-ui/react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
import { ErrorMessage, Heading1 } from "./util";
function ConversionPage() {
const { loading, error, data } = useQuery(
gql`
query ConversionPage_NoAuthRequired {
numAppearanceLayersConverted
numAppearanceLayersTotal
numPetLayersConverted: numAppearanceLayersConverted(type: PET_LAYER)
numPetLayersTotal: numAppearanceLayersTotal(type: PET_LAYER)
numItemLayersConverted: numAppearanceLayersConverted(type: ITEM_LAYER)
numItemLayersTotal: numAppearanceLayersTotal(type: ITEM_LAYER)
}
`,
{ onError: (e) => console.error(e) }
);
return (
<Box>
<Heading1>HTML5 Conversion Hub</Heading1>
<Box height="6" />
<Stack direction="row" spacing="12" align="center">
<ConversionProgress
label="All layers"
color="green.500"
size="150px"
numConverted={data?.numAppearanceLayersConverted}
numTotal={data?.numAppearanceLayersTotal}
isLoading={loading}
/>
<ConversionProgress
label="Pet layers"
color="blue.500"
size="125px"
numConverted={data?.numPetLayersConverted}
numTotal={data?.numPetLayersTotal}
isLoading={loading}
/>
<ConversionProgress
label="Item layers"
color="blue.500"
size="125px"
numConverted={data?.numItemLayersConverted}
numTotal={data?.numItemLayersTotal}
isLoading={loading}
/>
</Stack>
{error && (
<ErrorMessage marginTop="2">
Oops, we couldn't load the latest data: {error.message}
</ErrorMessage>
)}
</Box>
);
}
function ConversionProgress({
label,
color,
size,
numConverted,
numTotal,
isLoading,
}) {
const convertedPercent = (numConverted / numTotal) * 100;
return (
<Flex direction="column" align="center">
<CircularProgress
color={color}
size={size}
value={convertedPercent || 0}
isIndeterminate={isLoading}
>
{numConverted != null && numTotal != null && (
<CircularProgressLabel>
{Math.floor(convertedPercent)}%
</CircularProgressLabel>
)}
</CircularProgress>
<Box height="1" />
<Box textAlign="center">
<Box fontSize="xl">{label}</Box>
{numConverted != null && numTotal != null && (
<Box fontSize="xs">
{numConverted.toLocaleString()} of {numTotal.toLocaleString()}
</Box>
)}
</Box>
</Flex>
);
}
export default ConversionPage;

View file

@ -84,8 +84,12 @@ export function Heading3({ children, ...props }) {
/**
* ErrorMessage is a simple error message for simple errors!
*/
export function ErrorMessage({ children }) {
return <Box color="red.400">{children}</Box>;
export function ErrorMessage({ children, ...props }) {
return (
<Box color="red.400" {...props}>
{children}
</Box>
);
}
export function useCommonStyles() {

View file

@ -709,6 +709,40 @@ const buildSwfAssetLoader = (db) =>
);
});
const buildSwfAssetCountLoader = (db) =>
new DataLoader(
async (requests) => {
const [rows, _] = await db.execute(
`
SELECT count(*) AS count, type,
(manifest IS NOT NULL AND manifest != "") AS is_converted
FROM swf_assets
GROUP BY type, is_converted;
`
);
const entities = rows.map(normalizeRow);
return requests.map(({ type, isConverted }) => {
// Find the returned rows that match this count request.
let matchingEntities = entities;
if (type != null) {
matchingEntities = matchingEntities.filter((e) => e.type === type);
}
if (isConverted != null) {
matchingEntities = matchingEntities.filter(
(e) => Boolean(e.isConverted) === isConverted
);
}
// Add their counts together, and return the total.
return matchingEntities.map((e) => e.count).reduce((a, b) => a + b, 0);
});
},
{
cacheKeyFn: ({ type, isConverted }) => `${type},${isConverted}`,
}
);
const buildSwfAssetByRemoteIdLoader = (db) =>
new DataLoader(
async (typeAndRemoteIdPairs) => {
@ -1190,6 +1224,7 @@ function buildLoaders(db) {
loaders
);
loaders.swfAssetLoader = buildSwfAssetLoader(db);
loaders.swfAssetCountLoader = buildSwfAssetCountLoader(db);
loaders.swfAssetByRemoteIdLoader = buildSwfAssetByRemoteIdLoader(db);
loaders.itemSwfAssetLoader = buildItemSwfAssetLoader(db, loaders);
loaders.petSwfAssetLoader = buildPetSwfAssetLoader(db, loaders);

View file

@ -8,6 +8,11 @@ const typeDefs = gql`
SIZE_150
}
enum LayerType {
PET_LAYER
ITEM_LAYER
}
# Cache for 1 week (unlikely to change)
type AppearanceLayer @cacheControl(maxAge: 604800) {
# The DTI ID. Guaranteed unique across all layers of all types.
@ -67,6 +72,18 @@ const typeDefs = gql`
"""
restrictedZones: [Zone!]!
}
extend type Query {
# Return the number of layers that have been converted to HTML5, optionally
# filtered by type. Cache for 30 minutes (we re-sync with Neopets every
# hour).
numAppearanceLayersConverted(type: LayerType): Int!
@cacheControl(maxAge: 1800)
# Return the total number of layers, optionally filtered by type. Cache for
# 30 minutes (we re-sync with Neopets every hour).
numAppearanceLayersTotal(type: LayerType): Int! @cacheControl(maxAge: 1800)
}
`;
const resolvers = {
@ -200,8 +217,39 @@ const resolvers = {
return { id: String(rows[0].parent_id) };
},
},
Query: {
numAppearanceLayersConverted: async (
_,
{ type },
{ swfAssetCountLoader }
) => {
const count = await swfAssetCountLoader.load({
type: convertLayerTypeToSwfAssetType(type),
isConverted: true,
});
return count;
},
numAppearanceLayersTotal: async (_, { type }, { swfAssetCountLoader }) => {
const count = await swfAssetCountLoader.load({
type: convertLayerTypeToSwfAssetType(type),
});
return count;
},
},
};
function convertLayerTypeToSwfAssetType(layerType) {
switch (layerType) {
case "PET_LAYER":
return "biology";
case "ITEM_LAYER":
return "object";
default:
return null;
}
}
async function loadAndCacheAssetManifest(db, layer) {
let manifest;
try {