Skip to content

Commit a8118e4

Browse files
committed
Tweaked thumbnail spinner behavior
1 parent 6761dd1 commit a8118e4

6 files changed

Lines changed: 88 additions & 49 deletions

File tree

src/backend/features/thumbnails/keys.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import {
55
} from "../configurations/canonical";
66
import { ThumbnailSize } from "./types";
77

8-
/** Short on purpose: the real render can land at any moment and must take over. */
9-
export const THUMBNAIL_FALLBACK_CACHE_TTL = 60;
8+
/**
9+
* Shorter than the client's poll, so each poll reaches the worker instead of
10+
* the browser cache replaying the stand-in the real render has replaced.
11+
*/
12+
export const THUMBNAIL_FALLBACK_CACHE_TTL = 3;
1013

1114
/** Marks a response as the element default standing in for an unrendered configuration. */
1215
export const THUMBNAIL_FALLBACK_HEADER = "X-Thumbnail-Fallback";

src/frontend/features/insert/components/configurations.tsx

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { useSearch } from "@tanstack/react-router";
1212
import {
1313
type Dispatch,
14+
JSX,
1415
ReactNode,
1516
type SyntheticEvent,
1617
useCallback,
@@ -251,6 +252,8 @@ interface InputLabelProps {
251252
* The id of the input the label describes.
252253
*/
253254
htmlFor: string;
255+
/** True to lead with the input instead of the label. */
256+
inputFirst?: boolean;
254257
children: ReactNode;
255258
}
256259

@@ -259,23 +262,43 @@ interface InputLabelProps {
259262
* input grows to show an error message.
260263
*/
261264
function InputLabel(props: InputLabelProps) {
262-
const { label, htmlFor, children } = props;
265+
const { label, htmlFor, inputFirst = false, children } = props;
266+
const text = (
267+
<Text
268+
size="sm"
269+
style={{
270+
display: "flex",
271+
alignItems: "center",
272+
height: INPUT_HEIGHT,
273+
cursor: "pointer"
274+
}}
275+
component="label"
276+
htmlFor={htmlFor}
277+
>
278+
{label}
279+
</Text>
280+
);
281+
282+
let result: JSX.Element;
283+
if (inputFirst) {
284+
result = (
285+
<>
286+
{children}
287+
{text}
288+
</>
289+
);
290+
} else {
291+
result = (
292+
<>
293+
{text}
294+
{children}
295+
</>
296+
);
297+
}
298+
263299
return (
264300
<Group gap="sm" align="flex-start">
265-
<Text
266-
size="sm"
267-
style={{
268-
display: "flex",
269-
alignItems: "center",
270-
height: INPUT_HEIGHT,
271-
cursor: "pointer"
272-
}}
273-
component="label"
274-
htmlFor={htmlFor}
275-
>
276-
{label}
277-
</Text>
278-
{children}
301+
{result}
279302
</Group>
280303
);
281304
}
@@ -360,7 +383,7 @@ function EnumInput(props: ParameterProps<EnumParameter>): ReactNode {
360383
function BooleanInput(props: ParameterProps<BooleanParameter>): ReactNode {
361384
const { parameter, value, onValueChange } = props;
362385
return (
363-
<InputLabel label={parameter.name} htmlFor={parameter.id}>
386+
<InputLabel label={parameter.name} htmlFor={parameter.id} inputFirst>
364387
<Checkbox
365388
id={parameter.id}
366389
checked={(value ?? parameter.default) === "true"}

src/frontend/features/insert/components/insert-menu.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,14 @@ import { AppModalBody, AppModalFooter } from "../../../components/app-modal";
1010
import { MenuTitle } from "../../../components/app-title";
1111
import { modals } from "@mantine/modals";
1212
import { showQuickInsertTip } from "../quick-insert-tip";
13-
import { useIsFetching } from "@tanstack/react-query";
14-
import { insertableConfigurationQueryMatchKey } from "../../../lib/query-keys";
1513
import { PreviewImageCard } from "../../thumbnails/components/thumbnail";
1614
import { FavoriteButton } from "../../favorites/components/favorite-button";
1715
import { renderNotification } from "../../../lib/notifications";
1816
import { MenuButton } from "../../../components/app-menu";
1917
import { InsertableMenuItems } from "../../library/components/insertable-card";
2018
import { ConfigurationWrapper } from "./configurations";
2119
import { useInsertMutation } from "../insert-hooks";
22-
import { useConfigurationQuery } from "../queries";
20+
import { useConfigurationQuery, useIsFetchingConfiguration } from "../queries";
2321
import {
2422
ParameterValues,
2523
SearchRecord
@@ -192,10 +190,10 @@ function InsertButtons(props: InsertButtonsProps): ReactNode {
192190
});
193191
const [uiState, setUiState] = useUiState();
194192

195-
const isLoadingConfiguration =
196-
useIsFetching({
197-
queryKey: insertableConfigurationQueryMatchKey(insertable.id)
198-
}) > 0;
193+
const isLoadingConfiguration = useIsFetchingConfiguration(
194+
insertable.id,
195+
insertable.microversionId
196+
);
199197

200198
const canFasten =
201199
insertable.supportsFasten &&

src/frontend/features/insert/queries.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useQuery } from "@tanstack/react-query";
1+
import { useIsFetching, useQuery } from "@tanstack/react-query";
22
import { apiGet } from "../../lib/api-client";
33
import {
44
type ConfigurationResult,
@@ -46,3 +46,15 @@ export function useConfigurationQuery(
4646
refetchInterval: false
4747
});
4848
}
49+
50+
/** Whether {@link useConfigurationQuery} is in flight for this insertable. */
51+
export function useIsFetchingConfiguration(
52+
insertableId: string,
53+
microversionId: string
54+
): boolean {
55+
return (
56+
useIsFetching({
57+
queryKey: configurationQueryKey(insertableId, microversionId)
58+
}) > 0
59+
);
60+
}

src/frontend/features/thumbnails/components/thumbnail.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useIsFetching, useQuery } from "@tanstack/react-query";
1+
import { useQuery } from "@tanstack/react-query";
22
import { loadImage, loadImageResult } from "../../../lib/api-client";
33
import { ElementType } from "@backend/lib/onshape/element-type";
44
import { ThumbnailSize } from "@backend/features/thumbnails/types";
@@ -8,10 +8,13 @@ import { Question } from "@phosphor-icons/react";
88

99
import { ComponentPropsWithRef, ReactNode } from "react";
1010
import { DEFAULT_CANONICAL_CONFIGURATION } from "@backend/features/configurations/canonical";
11-
import { thumbnailUrl } from "@backend/features/thumbnails/keys";
12-
import { configurationQueryMatchKey } from "../../../lib/query-keys";
11+
import {
12+
THUMBNAIL_FALLBACK_CACHE_TTL,
13+
thumbnailUrl
14+
} from "@backend/features/thumbnails/keys";
1315
import { SectionError } from "../../../components/app-zero-state";
1416
import { useTargetElementType } from "../../insert/insert-hooks";
17+
import { useIsFetchingConfiguration } from "../../insert/queries";
1518
import { useIsSignedIn } from "../../auth/access-level";
1619
import { useIsConnectedToOnshape } from "../../../lib/onshape-params";
1720

@@ -170,8 +173,11 @@ interface PreviewImageProps {
170173
largeThumbnailUrl?: string;
171174
}
172175

173-
/** How often to re-check while the worker is still standing in the default. */
174-
const PREVIEW_POLL_MS = 4000;
176+
/**
177+
* How often to re-check while the worker is still standing in the default.
178+
* Past the fallback's cache TTL, or the poll never leaves the browser.
179+
*/
180+
const PREVIEW_POLL_MS = (THUMBNAIL_FALLBACK_CACHE_TTL + 1) * 1000;
175181

176182
export function PreviewImage(props: PreviewImageProps): ReactNode {
177183
const {
@@ -185,8 +191,10 @@ export function PreviewImage(props: PreviewImageProps): ReactNode {
185191
const size = ThumbnailSize.LARGE;
186192
const isSignedIn = useIsSignedIn();
187193
const isConnected = useIsConnectedToOnshape();
188-
const isFetchingConfiguration =
189-
useIsFetching({ queryKey: configurationQueryMatchKey() }) > 0;
194+
const isFetchingConfiguration = useIsFetchingConfiguration(
195+
insertableId,
196+
microversionId
197+
);
190198
const targetElementType = useTargetElementType();
191199

192200
const url = thumbnailUrl({
@@ -234,6 +242,11 @@ export function PreviewImage(props: PreviewImageProps): ReactNode {
234242
);
235243
}
236244

245+
// Placeholder data is the previous configuration's render, so the spinner
246+
// has to cover it too: what is on screen is not what was asked for.
247+
const isWaiting =
248+
thumbnailQuery.isPlaceholderData || thumbnailQuery.data?.isFallback;
249+
237250
if (thumbnailQuery.isError) {
238251
const action =
239252
targetElementType === ElementType.ASSEMBLY ? "insert" : "derive";
@@ -249,7 +262,7 @@ export function PreviewImage(props: PreviewImageProps): ReactNode {
249262
/>
250263
</Center>
251264
);
252-
} else if (thumbnailQuery.isPending && !thumbnailQuery.data) {
265+
} else if (!thumbnailQuery.data) {
253266
return (
254267
<Center w={heightAndWidth.width} h={heightAndWidth.height}>
255268
<Loader size={36} />
@@ -265,12 +278,15 @@ export function PreviewImage(props: PreviewImageProps): ReactNode {
265278
h={heightAndWidth.height}
266279
>
267280
<img
281+
// The render lands at the url the stand-in came from, so
282+
// remounting is what makes the browser go get it.
283+
key={String(thumbnailQuery.data.isFallback)}
268284
src={thumbnailQuery.data.url}
269285
{...heightAndWidth}
270286
style={FIT_INSIDE_BOX}
271287
/>
272288
</Box>
273-
{thumbnailQuery.data.isFallback && (
289+
{isWaiting && (
274290
<Loader pos="absolute" bottom={15} right={15} size={18} />
275291
)}
276292
</>

src/frontend/lib/query-keys.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,11 @@ export function accessDataQueryKey() {
99
return ["access-data"];
1010
}
1111

12-
/** Every configuration, whichever insertable and microversion. */
13-
export function configurationQueryMatchKey() {
14-
return ["configuration"];
15-
}
16-
17-
/** One insertable's configuration, whichever microversion is cached. */
18-
export function insertableConfigurationQueryMatchKey(insertableId: string) {
19-
return ["configuration", insertableId];
20-
}
21-
2212
export function configurationQueryKey(
2313
insertableId: string,
2414
microversionId: string
2515
) {
26-
return [
27-
...insertableConfigurationQueryMatchKey(insertableId),
28-
microversionId
29-
];
16+
return ["configuration", insertableId, microversionId];
3017
}
3118

3219
export function unitInfoQueryKey(instancePath: InstancePath) {

0 commit comments

Comments
 (0)