Skip to content

Commit 8b53b30

Browse files
committed
feat(FR-2862): add preset mode for deployment revision creation
1 parent 39b98b4 commit 8b53b30

28 files changed

Lines changed: 997 additions & 197 deletions

react/src/components/DeploymentAddRevisionModal.tsx

Lines changed: 653 additions & 26 deletions
Large diffs are not rendered by default.

react/src/components/ModelCardDeployModal.tsx

Lines changed: 62 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
@license
33
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
44
*/
5-
import { ModelCardDeployModalEndpointPollQuery } from '../__generated__/ModelCardDeployModalEndpointPollQuery.graphql';
65
import { ModelCardDeployModalMutation } from '../__generated__/ModelCardDeployModalMutation.graphql';
76
import { ModelCardDeployModalQuery } from '../__generated__/ModelCardDeployModalQuery.graphql';
87
import { useWebUINavigate } from '../hooks';
98
import { useCurrentProjectValue } from '../hooks/useCurrentProject';
10-
import { App, Form, Typography, theme } from 'antd';
9+
import { Alert, App, Form, Typography, theme } from 'antd';
1110
import type { DefaultOptionType } from 'antd/es/select';
1211
import {
1312
BAIButton,
@@ -19,6 +18,7 @@ import {
1918
useProjectResourceGroups,
2019
} from 'backend.ai-ui';
2120
import * as _ from 'lodash-es';
21+
import { PlusIcon } from 'lucide-react';
2222
import React, {
2323
Suspense,
2424
useEffect,
@@ -28,26 +28,7 @@ import React, {
2828
useState,
2929
} from 'react';
3030
import { useTranslation } from 'react-i18next';
31-
import {
32-
fetchQuery,
33-
graphql,
34-
useLazyLoadQuery,
35-
useMutation,
36-
useRelayEnvironment,
37-
} from 'react-relay';
38-
39-
/**
40-
* Poll-before-navigate configuration for the post-deploy handoff to the
41-
* serving detail page. The v2 `deployModelCardV2` mutation creates a
42-
* ModelDeployment, but the v1 `endpoint(endpoint_id: …)` projection — which
43-
* the serving detail page depends on — is populated a beat later. Navigating
44-
* immediately produces a page full of `-` because the v1 lookup returns
45-
* null (or an endpoint with null fields) until that propagation catches up.
46-
* We poll the same v1 query here so the Relay store is warm and the detail
47-
* page renders complete data on first paint.
48-
*/
49-
const ENDPOINT_POLL_INTERVAL_MS = 300;
50-
const ENDPOINT_POLL_MAX_ATTEMPTS = 10;
31+
import { graphql, useLazyLoadQuery, useMutation } from 'react-relay';
5132

5233
interface AvailablePreset {
5334
readonly id: string;
@@ -63,21 +44,33 @@ interface ModelCardDeployModalProps {
6344
modelCardRowId?: string;
6445
availablePresets: ReadonlyArray<AvailablePreset>;
6546
onDeployed: (deploymentId: string) => void;
47+
/**
48+
* Called when the user wants to fall back to creating a new deployment
49+
* (via `DeploymentSettingModal`) because no preset is available. The
50+
* parent is expected to close this modal and open its own
51+
* `DeploymentSettingModal`.
52+
*/
53+
onRequestCreateDeployment?: () => void;
6654
}
6755

6856
type ModelCardDeployModalContentProps = Omit<ModelCardDeployModalProps, 'open'>;
6957

7058
const ModelCardDeployModalContent: React.FC<
7159
ModelCardDeployModalContentProps
72-
> = ({ onClose, modelCardRowId, availablePresets, onDeployed }) => {
60+
> = ({
61+
onClose,
62+
modelCardRowId,
63+
availablePresets,
64+
onDeployed,
65+
onRequestCreateDeployment,
66+
}) => {
7367
'use memo';
7468

7569
const { t } = useTranslation();
7670
const { token } = theme.useToken();
7771
const { message } = App.useApp();
7872
const navigate = useWebUINavigate();
7973
const { id: projectId, name: projectName } = useCurrentProjectValue();
80-
const relayEnvironment = useRelayEnvironment();
8174

8275
// Fetch resource groups accessible to the current project. Uses the same
8376
// React Query cache as BAIProjectResourceGroupSelect below, so no duplicate
@@ -114,44 +107,6 @@ const ModelCardDeployModalContent: React.FC<
114107
}
115108
`);
116109

117-
// Minimal v1 endpoint projection used only for the post-deploy poll.
118-
// Kept intentionally small — we only need to confirm the endpoint is
119-
// addressable before handing off to the detail page.
120-
const endpointPollQuery = graphql`
121-
query ModelCardDeployModalEndpointPollQuery($endpointId: UUID!) {
122-
endpoint(endpoint_id: $endpointId) {
123-
endpoint_id
124-
name
125-
status
126-
}
127-
}
128-
`;
129-
130-
const waitForEndpointReady = async (endpointId: string): Promise<void> => {
131-
for (let attempt = 0; attempt < ENDPOINT_POLL_MAX_ATTEMPTS; attempt++) {
132-
try {
133-
const result = await fetchQuery<ModelCardDeployModalEndpointPollQuery>(
134-
relayEnvironment,
135-
endpointPollQuery,
136-
{ endpointId },
137-
// Bypass the store on each attempt so we actually hit the server
138-
// — the whole point of the poll is to observe fresh server state.
139-
{ fetchPolicy: 'network-only' },
140-
).toPromise();
141-
if (result?.endpoint?.endpoint_id) {
142-
return;
143-
}
144-
} catch {
145-
// Swallow transient errors and keep polling — on the last attempt
146-
// we fall through and navigate anyway; the detail page will still
147-
// render with whatever state the server can give us.
148-
}
149-
await new Promise((resolve) =>
150-
setTimeout(resolve, ENDPOINT_POLL_INTERVAL_MS),
151-
);
152-
}
153-
};
154-
155110
// Build runtime variant ID → name map for preset grouping
156111
const runtimeVariantNameMap = useMemo(() => {
157112
const map = new Map<string, string>();
@@ -233,15 +188,8 @@ const ModelCardDeployModalContent: React.FC<
233188
const deploymentId = response.deployModelCardV2.deploymentId;
234189
message.success(t('modelStore.DeploySuccess'));
235190
onDeployed(deploymentId);
236-
// Wait for the v1 endpoint projection to become queryable before
237-
// navigating, so the serving detail page doesn't render a blank
238-
// page full of `-` placeholders during the v1/v2 propagation gap.
239-
waitForEndpointReady(deploymentId)
240-
.then(() => {
241-
navigate(`/serving/${deploymentId}`);
242-
resolve();
243-
})
244-
.catch(reject);
191+
navigate(`/deployments/${deploymentId}`);
192+
resolve();
245193
},
246194
onError: (error) => {
247195
message.error(error.message || t('modelStore.DeployFailed'));
@@ -276,6 +224,47 @@ const ModelCardDeployModalContent: React.FC<
276224
return null;
277225
}
278226

227+
// Empty-state: per FR-2862, when no preset is available the modal stays
228+
// open with an inline Alert and a link to the deployment shell creation
229+
// modal (`DeploymentSettingModal`) — same UX as the `/deployments` page
230+
// "Create Deployment" entry.
231+
if (availablePresets.length === 0) {
232+
return (
233+
<BAIModal
234+
title={t('modelStore.DeployModel')}
235+
open
236+
onCancel={onClose}
237+
destroyOnHidden
238+
footer={null}
239+
width={480}
240+
>
241+
<Alert
242+
type="info"
243+
showIcon
244+
title={t('deployment.NoPresetsAvailable')}
245+
description={t('deployment.NoPresetsAvailableDescription')}
246+
action={
247+
onRequestCreateDeployment ? (
248+
<BAIButton
249+
type="primary"
250+
icon={<PlusIcon />}
251+
onClick={() => {
252+
onClose();
253+
onRequestCreateDeployment();
254+
}}
255+
>
256+
{t('deployment.OpenCreateDeploymentModal')}
257+
</BAIButton>
258+
) : undefined
259+
}
260+
/>
261+
<BAIFlex justify="end" gap="sm" style={{ marginTop: token.marginMD }}>
262+
<BAIButton onClick={onClose}>{t('button.Cancel')}</BAIButton>
263+
</BAIFlex>
264+
</BAIModal>
265+
);
266+
}
267+
279268
// Scenario 3: selection UI — render the modal only when selection is needed.
280269
return (
281270
<BAIModal
@@ -351,6 +340,7 @@ const ModelCardDeployModal: React.FC<ModelCardDeployModalProps> = ({
351340
modelCardRowId,
352341
availablePresets,
353342
onDeployed,
343+
onRequestCreateDeployment,
354344
}) => {
355345
'use memo';
356346

@@ -370,6 +360,7 @@ const ModelCardDeployModal: React.FC<ModelCardDeployModalProps> = ({
370360
modelCardRowId={modelCardRowId}
371361
availablePresets={availablePresets}
372362
onDeployed={onDeployed}
363+
onRequestCreateDeployment={onRequestCreateDeployment}
373364
/>
374365
</Suspense>
375366
);

react/src/components/ModelCardDrawer.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,15 @@
55
import { ModelCardDrawerFragment$key } from '../__generated__/ModelCardDrawerFragment.graphql';
66
import { useBackendAIImageMetaData } from '../hooks';
77
import useDeploymentLauncher from '../hooks/useDeploymentLauncher';
8+
import DeploymentSettingModal from './DeploymentSettingModal';
89
import ErrorBoundaryWithNullFallback from './ErrorBoundaryWithNullFallback';
910
import { useFolderExplorerOpener } from './FolderExplorerOpener';
1011
import ModelBrandIcon from './ModelBrandIcon';
1112
import ModelCardDeployModal from './ModelCardDeployModal';
12-
import { BankOutlined, FileOutlined } from '@ant-design/icons';
1313
import VFolderNodeIdenticonV2 from './VFolderNodeIdenticonV2';
14-
import {
15-
Card,
16-
Descriptions,
17-
Drawer,
18-
Skeleton,
19-
Tag,
20-
Typography,
21-
} from 'antd';
14+
import { BankOutlined, FileOutlined } from '@ant-design/icons';
15+
import { useToggle } from 'ahooks';
16+
import { Card, Descriptions, Drawer, Skeleton, Tag, Typography } from 'antd';
2217
import {
2318
BAIButton,
2419
BAIFlex,
@@ -119,6 +114,11 @@ const ModelCardDrawer: React.FC<ModelCardDrawerProps> = ({
119114
const hasNoAvailablePresets =
120115
!modelCard?.availablePresets || modelCard.availablePresets.count === 0;
121116
const [deployModalOpen, setDeployModalOpen] = useState(false);
117+
// FR-2862 — when the user hits the empty-preset state in
118+
// ModelCardDeployModal, escalate to the deployment shell creation modal
119+
// (`DeploymentSettingModal`), same as the `/deployments` page entry.
120+
const [isCreateDeploymentOpen, { toggle: toggleCreateDeployment }] =
121+
useToggle(false);
122122

123123
const presets =
124124
modelCard?.availablePresets?.edges
@@ -350,6 +350,11 @@ const ModelCardDrawer: React.FC<ModelCardDrawerProps> = ({
350350
setDeployModalOpen(false);
351351
onClose();
352352
}}
353+
onRequestCreateDeployment={toggleCreateDeployment}
354+
/>
355+
<DeploymentSettingModal
356+
open={isCreateDeploymentOpen}
357+
onRequestClose={toggleCreateDeployment}
353358
/>
354359
</>
355360
);

0 commit comments

Comments
 (0)