Add modeling info to homepage

To make this fast, I had to tweak the GraphQL resolver a bit to run a filtered version of the query for `newestItems` instead of scanning the full database! But yeah, looking good!

I think I'm gonna want to swap out "Fully modeled" for some insight about who it fits
This commit is contained in:
Emi Matchu 2021-07-11 18:09:29 -07:00
parent a19c2facbf
commit f2259d6487
4 changed files with 171 additions and 82 deletions

View file

@ -379,6 +379,10 @@ function NewItemsSectionContent() {
thumbnailUrl thumbnailUrl
isNc isNc
isPb isPb
speciesThatNeedModels {
id
name
}
} }
} }
` `
@ -402,29 +406,30 @@ function NewItemsSectionContent() {
); );
if (loading) { if (loading) {
const footer = <Box fontSize="xs" height="1em" />;
return ( return (
<Delay> <Delay>
<ItemCardHStack> <ItemCardHStack>
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton minHeightNumLines={3} /> <SquareItemCardSkeleton footer={footer} minHeightNumLines={3} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
<SquareItemCardSkeleton /> <SquareItemCardSkeleton footer={footer} />
</ItemCardHStack> </ItemCardHStack>
</Delay> </Delay>
); );
@ -448,7 +453,26 @@ function NewItemsSectionContent() {
return ( return (
<ItemCardHStack> <ItemCardHStack>
{newestItems.map((item) => ( {newestItems.map((item) => (
<SquareItemCard key={item.id} item={item} /> <SquareItemCard
key={item.id}
item={item}
footer={
item.speciesThatNeedModels.length > 0 ? (
<Box
fontSize="xs"
fontStyle="italic"
fontWeight="600"
opacity="0.8"
>
Need {item.speciesThatNeedModels.length}1 models
</Box>
) : (
<Box fontSize="xs" fontStyle="italic" opacity="0.8">
Fully modeled!
</Box>
)
}
/>
))} ))}
</ItemCardHStack> </ItemCardHStack>
); );

View file

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { import {
Box,
Skeleton, Skeleton,
useColorModeValue, useColorModeValue,
useTheme, useTheme,
@ -11,7 +12,12 @@ import { Link } from "react-router-dom";
import { safeImageUrl, useCommonStyles } from "../util"; import { safeImageUrl, useCommonStyles } from "../util";
import { CheckIcon, StarIcon } from "@chakra-ui/icons"; import { CheckIcon, StarIcon } from "@chakra-ui/icons";
function SquareItemCard({ item, tradeMatchingMode = null, ...props }) { function SquareItemCard({
item,
tradeMatchingMode = null,
footer = null,
...props
}) {
const outlineShadowValue = useToken("shadows", "outline"); const outlineShadowValue = useToken("shadows", "outline");
const tradeMatchOwnShadowColor = useColorModeValue("green.500", "green.200"); const tradeMatchOwnShadowColor = useColorModeValue("green.500", "green.200");
@ -64,6 +70,7 @@ function SquareItemCard({ item, tradeMatchingMode = null, ...props }) {
/> />
} }
boxShadow={tradeMatchShadow} boxShadow={tradeMatchShadow}
footer={footer}
/> />
</Link> </Link>
)} )}
@ -74,6 +81,7 @@ function SquareItemCard({ item, tradeMatchingMode = null, ...props }) {
function SquareItemCardLayout({ function SquareItemCardLayout({
name, name,
thumbnailImage, thumbnailImage,
footer,
minHeightNumLines = 2, minHeightNumLines = 2,
boxShadow = null, boxShadow = null,
}) { }) {
@ -118,6 +126,7 @@ function SquareItemCardLayout({
> >
{name} {name}
</div> </div>
{footer && <Box marginTop="2">{footer}</Box>}
</div> </div>
)} )}
</ClassNames> </ClassNames>
@ -350,7 +359,7 @@ function ItemThumbnailKindBadge({ colorScheme, children }) {
); );
} }
export function SquareItemCardSkeleton({ minHeightNumLines }) { export function SquareItemCardSkeleton({ minHeightNumLines, footer = null }) {
return ( return (
<SquareItemCardLayout <SquareItemCardLayout
name={ name={
@ -363,6 +372,7 @@ export function SquareItemCardSkeleton({ minHeightNumLines }) {
} }
thumbnailImage={<Skeleton width="80px" height="80px" />} thumbnailImage={<Skeleton width="80px" height="80px" />}
minHeightNumLines={minHeightNumLines} minHeightNumLines={minHeightNumLines}
footer={footer}
/> />
); );
} }

View file

@ -480,7 +480,100 @@ const buildNewestItemsLoader = (db, loaders) =>
return [entities]; return [entities];
}); });
const buildItemsThatNeedModelsLoader = (db) => async function runItemModelingQuery(db, filterToItemIds) {
let itemIdsCondition;
let itemIdsValues;
if (filterToItemIds === "all") {
// For all items, we use the condition `1`, which matches everything.
itemIdsCondition = "1";
itemIdsValues = [];
} else {
// Or, to filter to certain items, we add their IDs to the WHERE clause.
const qs = filterToItemIds.map((_) => "?").join(", ");
itemIdsCondition = `(item_id IN (${qs}))`;
itemIdsValues = filterToItemIds;
}
return await db.execute(
`
SELECT T_ITEMS.item_id,
T_BODIES.color_id,
T_ITEMS.supports_vandagyre,
COUNT(*) AS modeled_species_count,
GROUP_CONCAT(
T_BODIES.species_id
ORDER BY T_BODIES.species_id
) AS modeled_species_ids,
(
SELECT GROUP_CONCAT(DISTINCT species_id ORDER BY species_id)
FROM pet_types WHERE color_id = T_BODIES.color_id
) AS all_species_ids_for_this_color
FROM (
-- NOTE: I found that extracting this as a separate query that runs
-- first made things WAAAY faster. Less to join/group, I guess?
SELECT DISTINCT items.id AS item_id,
swf_assets.body_id AS body_id,
-- Vandagyre was added on 2014-11-14, so we add some buffer here.
-- TODO: Some later Dyeworks items don't support Vandagyre.
-- Add a manual db flag?
items.created_at >= "2014-12-01" AS supports_vandagyre
FROM items
INNER JOIN parents_swf_assets psa ON psa.parent_type = "Item"
AND psa.parent_id = items.id
INNER JOIN swf_assets ON swf_assets.id = psa.swf_asset_id
INNER JOIN item_translations it ON it.item_id = items.id AND it.locale = "en"
WHERE items.modeling_status_hint IS NULL AND it.name NOT LIKE "%MME%"
AND ${itemIdsCondition}
ORDER BY item_id
) T_ITEMS
INNER JOIN (
SELECT DISTINCT body_id, species_id, color_id
FROM pet_types
WHERE color_id IN (6, 8, 44, 46)
ORDER BY body_id, species_id
) T_BODIES ON T_ITEMS.body_id = T_BODIES.body_id
GROUP BY T_ITEMS.item_id, T_BODIES.color_id
HAVING NOT (
-- No species (either an All Bodies item, or a Capsule type thing)
modeled_species_count = 0
-- Single species (probably just their item)
OR modeled_species_count = 1
-- All species modeled (that are compatible with this color)
OR modeled_species_ids = all_species_ids_for_this_color
-- All species modeled except Vandagyre, for items that don't support it
OR (NOT T_ITEMS.supports_vandagyre AND modeled_species_count = 54 AND modeled_species_ids = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54")
)
ORDER BY T_ITEMS.item_id;
`,
[...itemIdsValues]
);
}
const buildSpeciesThatNeedModelsForItemLoader = (db) =>
new DataLoader(
async (colorIdAndItemIdPairs) => {
// Get the requested item IDs, ignoring color for now. Remove duplicates.
let itemIds = colorIdAndItemIdPairs.map(({ itemId }) => itemId);
itemIds = [...new Set(itemIds)];
// Run the big modeling query, but filtered to specifically these items.
// The filter happens very early in the query, so it runs way faster than
// the full modeling query.
const [rows] = await runItemModelingQuery(db, itemIds);
const entities = rows.map(normalizeRow);
// Finally, the query returned a row for each item combined with each
// color built into the query (well, no row when no models needed!). So,
// find the right row for each color/item pair, or possibly null!
return colorIdAndItemIdPairs.map(({ colorId, itemId }) =>
entities.find((e) => e.itemId === itemId && e.colorId === colorId)
);
},
{ cacheKeyFn: ({ colorId, itemId }) => `${colorId}-${itemId}` }
);
const buildItemsThatNeedModelsLoader = (db, loaders) =>
new DataLoader(async (keys) => { new DataLoader(async (keys) => {
// Essentially, I want to take easy advantage of DataLoader's caching, for // Essentially, I want to take easy advantage of DataLoader's caching, for
// this query that can only run one way ^_^` There might be a better way to // this query that can only run one way ^_^` There might be a better way to
@ -489,62 +582,17 @@ const buildItemsThatNeedModelsLoader = (db) =>
throw new Error(`this loader can only be loaded with the key "all"`); throw new Error(`this loader can only be loaded with the key "all"`);
} }
const [rows] = await db.query( const [rows] = await runItemModelingQuery(db, "all");
`
SELECT T_ITEMS.item_id,
T_BODIES.color_id,
T_ITEMS.supports_vandagyre,
COUNT(*) AS modeled_species_count,
GROUP_CONCAT(
T_BODIES.species_id
ORDER BY T_BODIES.species_id
) AS modeled_species_ids,
(
SELECT GROUP_CONCAT(DISTINCT species_id ORDER BY species_id)
FROM pet_types WHERE color_id = T_BODIES.color_id
) AS all_species_ids_for_this_color
FROM (
-- NOTE: I found that extracting this as a separate query that runs
-- first made things WAAAY faster. Less to join/group, I guess?
SELECT DISTINCT items.id AS item_id,
swf_assets.body_id AS body_id,
-- Vandagyre was added on 2014-11-14, so we add some buffer here.
-- TODO: Some later Dyeworks items don't support Vandagyre.
-- Add a manual db flag?
items.created_at >= "2014-12-01" AS supports_vandagyre
FROM items
INNER JOIN parents_swf_assets psa ON psa.parent_type = "Item"
AND psa.parent_id = items.id
INNER JOIN swf_assets ON swf_assets.id = psa.swf_asset_id
INNER JOIN item_translations it ON it.item_id = items.id AND it.locale = "en"
WHERE items.modeling_status_hint IS NULL AND it.name NOT LIKE "%MME%"
ORDER BY item_id
) T_ITEMS
INNER JOIN (
SELECT DISTINCT body_id, species_id, color_id
FROM pet_types
WHERE color_id IN (6, 8, 44, 46)
ORDER BY body_id, species_id
) T_BODIES ON T_ITEMS.body_id = T_BODIES.body_id
GROUP BY T_ITEMS.item_id, T_BODIES.color_id
HAVING NOT (
-- No species (either an All Bodies item, or a Capsule type thing)
modeled_species_count = 0
-- Single species (probably just their item)
OR modeled_species_count = 1
-- All species modeled (that are compatible with this color)
OR modeled_species_ids = all_species_ids_for_this_color
-- All species modeled except Vandagyre, for items that don't support it
OR (NOT T_ITEMS.supports_vandagyre AND modeled_species_count = 54 AND modeled_species_ids = "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54")
)
ORDER BY T_ITEMS.item_id;
`
);
const entities = rows.map(normalizeRow); const entities = rows.map(normalizeRow);
const result = new Map(); const result = new Map();
for (const { colorId, itemId, ...entity } of entities) { for (const { colorId, itemId, ...entity } of entities) {
loaders.speciesThatNeedModelsForItemLoader.prime(
{ colorId, itemId },
entity
);
if (!result.has(colorId)) { if (!result.has(colorId)) {
result.set(colorId, new Map()); result.set(colorId, new Map());
} }
@ -1309,7 +1357,13 @@ function buildLoaders(db) {
); );
loaders.itemSearchItemsLoader = buildItemSearchItemsLoader(db, loaders); loaders.itemSearchItemsLoader = buildItemSearchItemsLoader(db, loaders);
loaders.newestItemsLoader = buildNewestItemsLoader(db, loaders); loaders.newestItemsLoader = buildNewestItemsLoader(db, loaders);
loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(db); loaders.speciesThatNeedModelsForItemLoader = buildSpeciesThatNeedModelsForItemLoader(
db
);
loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(
db,
loaders
);
loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader( loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader(
db db
); );

View file

@ -457,13 +457,14 @@ const resolvers = {
speciesThatNeedModels: async ( speciesThatNeedModels: async (
{ id }, { id },
{ colorId = "8" }, // Blue { colorId = "8" }, // Blue
{ itemsThatNeedModelsLoader } { speciesThatNeedModelsForItemLoader }
) => { ) => {
const speciesIdsByColorIdAndItemId = await itemsThatNeedModelsLoader.load( // NOTE: If we're running this in the context of `itemsThatNeedModels`,
"all" // this loader should already be primed, no extra query!
); const row = await speciesThatNeedModelsForItemLoader.load({
const speciesIdsByItemId = speciesIdsByColorIdAndItemId.get(colorId); itemId: id,
const row = speciesIdsByItemId && speciesIdsByItemId.get(id); colorId,
});
if (!row) { if (!row) {
return []; return [];
} }