Skip to content

Commit a718976

Browse files
committed
fix(FR-2869): lazy-load model card drawer details to speed up ModelStoreListPageV2
1 parent 58b9dae commit a718976

4 files changed

Lines changed: 61 additions & 67 deletions

File tree

react/src/components/ModelCardDeployModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ interface AvailablePreset {
4747
readonly id: string;
4848
readonly name: string;
4949
readonly description: string | null;
50-
readonly rank: number;
5150
readonly runtimeVariantId: string;
5251
readonly ' $fragmentSpreads': FragmentRefs<'DeploymentPresetDetailContentFragment'>;
5352
}
@@ -392,7 +391,8 @@ const ModelCardDeployModal: React.FC<ModelCardDeployModalProps> = ({
392391
// Deploy. The content component suspends on its data query, then decides
393392
// whether to render the selection modal or auto-deploy silently — for the
394393
// auto-deploy path no modal is ever rendered, so the user goes directly
395-
// from the Deploy button to the serving detail page without a flash.
394+
// from the Deploy button to the new deployment detail page
395+
// (`/deployments/${deploymentId}`) without a flash.
396396
if (!open) {
397397
return null;
398398
}

react/src/components/ModelCardDrawer.tsx

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ const ModelCardDrawer: React.FC<ModelCardDrawerProps> = ({
4444
const { t } = useTranslation();
4545
const [imageMetaData] = useBackendAIImageMetaData();
4646
const { generateFolderPath } = useFolderExplorerOpener();
47+
const [deployModalOpen, setDeployModalOpen] = useState(false);
48+
const [
49+
isCreateDeploymentOpen,
50+
{ toggle: toggleCreateDeployment, setLeft: closeCreateDeployment },
51+
] = useToggle(false);
4752

4853
const modelCard = useFragment(
4954
graphql`
@@ -77,29 +82,12 @@ const ModelCardDrawer: React.FC<ModelCardDrawerProps> = ({
7782
...VFolderNodeIdenticonV2Fragment
7883
}
7984
availablePresets(orderBy: [{ field: RANK, direction: "ASC" }]) {
80-
count
8185
edges {
8286
node {
8387
id
8488
name
8589
description
86-
rank
8790
runtimeVariantId
88-
runtimeVariant {
89-
name
90-
}
91-
execution {
92-
imageId
93-
startupCommand
94-
}
95-
cluster {
96-
clusterMode
97-
clusterSize
98-
}
99-
deploymentDefaults {
100-
openToPublic
101-
replicaCount
102-
}
10391
...DeploymentPresetDetailContentFragment
10492
}
10593
}
@@ -109,13 +97,6 @@ const ModelCardDrawer: React.FC<ModelCardDrawerProps> = ({
10997
modelCardDrawerFrgmt,
11098
);
11199

112-
const [deployModalOpen, setDeployModalOpen] = useState(false);
113-
// FR-2862 — when the user hits the empty-preset state in
114-
// ModelCardDeployModal, escalate to the deployment shell creation modal
115-
// (`DeploymentSettingModal`), same as the `/deployments` page entry.
116-
const [isCreateDeploymentOpen, { toggle: toggleCreateDeployment }] =
117-
useToggle(false);
118-
119100
const presets =
120101
modelCard?.availablePresets?.edges
121102
?.map((e) => e?.node)
@@ -125,12 +106,21 @@ const ModelCardDrawer: React.FC<ModelCardDrawerProps> = ({
125106
<>
126107
<Drawer
127108
open={open}
128-
onClose={onClose}
109+
onClose={() => {
110+
setDeployModalOpen(false);
111+
closeCreateDeployment();
112+
onClose();
113+
}}
129114
destroyOnHidden
130115
placement="right"
131116
size={800}
132117
title={
133-
<BAIFlex direction="row" align="center" gap="xs">
118+
<BAIFlex
119+
direction="row"
120+
align="center"
121+
gap="xs"
122+
style={{ flex: 1, minWidth: 0 }}
123+
>
134124
<ModelBrandIcon modelName={modelCard?.name ?? ''} />
135125
<Typography.Text strong ellipsis>
136126
{modelCard?.metadata?.title || modelCard?.name}

react/src/components/VFolderDeployModal.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ const VFolderDeployModalContent: React.FC<VFolderDeployModalContentProps> = ({
195195

196196
// Auto-deploy on mount when there's exactly one preset and one resource
197197
// group — same shortcut as ModelCardDeployModal. No modal is rendered;
198-
// the user goes straight from the trigger to the serving detail page.
198+
// the user goes straight from the trigger to the new deployment detail
199+
// page (`/deployments/${deploymentId}`).
199200
const isAutoDeployScenario =
200201
availablePresets.length === 1 && resourceGroups.length === 1;
201202

@@ -453,8 +454,8 @@ const VFolderDeployModal: React.FC<VFolderDeployModalProps> = ({
453454
// opened it. The content component suspends on its data query, then
454455
// decides whether to render the selection modal or auto-deploy silently —
455456
// for the auto-deploy path no modal is ever rendered, so the user goes
456-
// directly from clicking Start Service to the serving detail page without
457-
// a flash.
457+
// directly from clicking Start Service to the new deployment detail page
458+
// (`/deployments/${deploymentId}`) without a flash.
458459
if (!open) {
459460
return null;
460461
}

react/src/pages/ModelStoreListPageV2.tsx

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
@license
33
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
44
*/
5-
import { ModelCardDrawerFragment$key } from '../__generated__/ModelCardDrawerFragment.graphql';
5+
import { ModelStoreListPageV2DrawerQuery } from '../__generated__/ModelStoreListPageV2DrawerQuery.graphql';
66
import {
77
ModelCardV2Filter,
88
ModelStoreListPageV2Query,
@@ -34,6 +34,7 @@ import {
3434
BAIGraphQLPropertyFilter,
3535
BAISelect,
3636
type GraphQLFilter,
37+
safeDecodeUuid,
3738
useUpdatableState,
3839
} from 'backend.ai-ui';
3940
import dayjs from 'dayjs';
@@ -43,7 +44,7 @@ import {
4344
parseAsStringLiteral,
4445
useQueryStates,
4546
} from 'nuqs';
46-
import React, { useDeferredValue, useEffectEvent, useState } from 'react';
47+
import React, { useDeferredValue, useEffectEvent } from 'react';
4748
import { useTranslation } from 'react-i18next';
4849
import { graphql, useFragment, useLazyLoadQuery } from 'react-relay';
4950

@@ -204,9 +205,7 @@ const ModelCardV2Grid: React.FC<{
204205
pageSize: number;
205206
offset: number;
206207
onTotalChange: (total: number) => void;
207-
onCardClick?: (id: string, frgmt: ModelCardDrawerFragment$key) => void;
208-
selectedModelCardId?: string | null;
209-
onSelectedModelCardFound?: (frgmt: ModelCardDrawerFragment$key) => void;
208+
onCardClick?: (id: string) => void;
210209
}> = ({
211210
projectId,
212211
filter,
@@ -218,8 +217,6 @@ const ModelCardV2Grid: React.FC<{
218217
offset,
219218
onTotalChange,
220219
onCardClick,
221-
selectedModelCardId,
222-
onSelectedModelCardFound,
223220
}) => {
224221
'use memo';
225222

@@ -246,7 +243,6 @@ const ModelCardV2Grid: React.FC<{
246243
node {
247244
id
248245
...ModelStoreListPageV2_ModelCardV2Fragment
249-
...ModelCardDrawerFragment
250246
}
251247
}
252248
}
@@ -276,23 +272,6 @@ const ModelCardV2Grid: React.FC<{
276272
onTotalChanged();
277273
}, [total]);
278274

279-
// When items load and a selectedModelCardId is set (e.g. after refresh),
280-
// find the matching fragment and report it to the parent.
281-
const onResolveSelectedModelCard = useEffectEvent(() => {
282-
if (selectedModelCardId) {
283-
const match = items.find(
284-
(edge) => edge?.node?.id === selectedModelCardId,
285-
);
286-
if (match?.node) {
287-
onSelectedModelCardFound?.(match.node);
288-
}
289-
}
290-
});
291-
292-
React.useEffect(() => {
293-
onResolveSelectedModelCard();
294-
}, [selectedModelCardId, result]);
295-
296275
if (items.length === 0) {
297276
return (
298277
<Empty
@@ -312,7 +291,7 @@ const ModelCardV2Grid: React.FC<{
312291
<ModelCardV2Card
313292
modelCardV2Frgmt={item}
314293
searchKeyword={searchKeyword}
315-
onClick={() => onCardClick?.(item.id, item)}
294+
onClick={() => onCardClick?.(item.id)}
316295
/>
317296
</Col>
318297
);
@@ -343,9 +322,6 @@ const ModelStoreListPageV2: React.FC = () => {
343322
queryParams.sort,
344323
);
345324

346-
const [selectedModelCard, setSelectedModelCard] =
347-
useState<ModelCardDrawerFragment$key | null>(null);
348-
349325
const filter: GraphQLFilter | undefined = queryParams.filter ?? undefined;
350326
const deferredFilter = useDeferredValue(filter);
351327
const deferredSortField = useDeferredValue(sortField);
@@ -364,6 +340,37 @@ const ModelStoreListPageV2: React.FC = () => {
364340
const deferredLimit = useDeferredValue(baiPaginationOption.limit);
365341
const deferredOffset = useDeferredValue(baiPaginationOption.offset);
366342

343+
// Drawer data is intentionally NOT fetched as part of the main list query
344+
// — the per-card drawer fragment (readme, all presets, vfolder metadata)
345+
// is heavy, and including it via `...ModelCardDrawerFragment` on every
346+
// edge multiplies the list-page payload. Instead we run a separate query
347+
// here gated by `useDeferredValue(drawerOpen)`: while no card is selected
348+
// (`deferredDrawerOpen` falsy), `fetchPolicy: 'store-only'` keeps the
349+
// network quiet and the empty-string `id` is never sent. Same pattern as
350+
// `FolderExplorerOpener` → `FolderExplorerModalV2`.
351+
const selectedModelCardId = queryParams.modelCard;
352+
const drawerOpen = !!selectedModelCardId;
353+
const deferredDrawerOpen = useDeferredValue(drawerOpen);
354+
const localSelectedModelCardId = selectedModelCardId
355+
? safeDecodeUuid(selectedModelCardId)
356+
: undefined;
357+
const drawerData = useLazyLoadQuery<ModelStoreListPageV2DrawerQuery>(
358+
graphql`
359+
query ModelStoreListPageV2DrawerQuery($id: UUID!) {
360+
modelCardV2(id: $id) {
361+
...ModelCardDrawerFragment
362+
}
363+
}
364+
`,
365+
{ id: localSelectedModelCardId ?? '' },
366+
{
367+
fetchPolicy:
368+
deferredDrawerOpen && localSelectedModelCardId
369+
? 'store-and-network'
370+
: 'store-only',
371+
},
372+
);
373+
367374
const isPendingPage =
368375
deferredLimit !== baiPaginationOption.limit ||
369376
deferredOffset !== baiPaginationOption.offset;
@@ -481,10 +488,7 @@ const ModelStoreListPageV2: React.FC = () => {
481488
pageSize={deferredLimit}
482489
offset={deferredOffset}
483490
onTotalChange={setTotal}
484-
selectedModelCardId={queryParams.modelCard}
485-
onSelectedModelCardFound={(frgmt) => setSelectedModelCard(frgmt)}
486-
onCardClick={(id, frgmt) => {
487-
setSelectedModelCard(frgmt);
491+
onCardClick={(id) => {
488492
setQueryParams({ modelCard: id });
489493
}}
490494
/>
@@ -525,10 +529,9 @@ const ModelStoreListPageV2: React.FC = () => {
525529
)}
526530

527531
<ModelCardDrawer
528-
modelCardDrawerFrgmt={selectedModelCard}
529-
open={!!queryParams.modelCard && !!selectedModelCard}
532+
modelCardDrawerFrgmt={drawerData.modelCardV2 ?? null}
533+
open={drawerOpen}
530534
onClose={() => {
531-
setSelectedModelCard(null);
532535
setQueryParams({ modelCard: null });
533536
}}
534537
/>

0 commit comments

Comments
 (0)