Skip to content

Commit f375c39

Browse files
committed
feat(FR-2683): add useDeploymentLauncher hook for Quick Deploy
1 parent 335a259 commit f375c39

1 file changed

Lines changed: 241 additions & 0 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/**
2+
@license
3+
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
4+
*/
5+
import { useDeploymentLauncherCreateMutation } from '../__generated__/useDeploymentLauncherCreateMutation.graphql';
6+
import { useCurrentDomainValue, useSuspendedBackendaiClient } from '../hooks';
7+
import { useSetBAINotification } from '../hooks/useBAINotification';
8+
import {
9+
useCurrentProjectValue,
10+
useCurrentResourceGroupValue,
11+
} from '../hooks/useCurrentProject';
12+
import { toLocalId, useBAILogger } from 'backend.ai-ui';
13+
import { useState } from 'react';
14+
import { useTranslation } from 'react-i18next';
15+
import { graphql, useMutation } from 'react-relay';
16+
import { useNavigate } from 'react-router-dom';
17+
18+
export interface QuickDeployInput {
19+
/** Virtual folder (model folder) id that backs the deployment. */
20+
modelFolderId: string;
21+
/** Optional model version; defaults to the folder's latest. */
22+
modelVersion?: string;
23+
/** Optional resource group; falls back to the project's current resource group. */
24+
resourceGroup?: string;
25+
/** Optional resource preset name (passed through as a tag for now). */
26+
resourcePreset?: string;
27+
/** Replica count (default: 1). */
28+
replicas?: number;
29+
/** Public endpoint toggle (default: false). */
30+
openToPublic?: boolean;
31+
}
32+
33+
export interface DeployInstantlyResult {
34+
deploymentId: string;
35+
}
36+
37+
/**
38+
* Hook that encapsulates Quick Deploy logic for model folders (Flow 7 of
39+
* FR-1368). Exposes two entry points:
40+
*
41+
* - `deployInstantly`: fires the GQL `createModelDeployment` mutation with
42+
* sensible defaults (replicas=1, openToPublic=false, current project +
43+
* domain, current resource group as a fallback). Returns the new
44+
* deployment id and raises a BAI notification on success/failure.
45+
* - `openLauncher`: navigates to `/deployments/new?model=<folderId>` so the
46+
* user can configure a deployment in the full launcher UI.
47+
*
48+
* The hook does not wrap the legacy REST-based `useModelServiceLauncher` —
49+
* callers should pick between the two based on `supportsQuickDeploy` (the
50+
* 26.4.2+ gate where `createModelDeployment` is stable).
51+
*/
52+
export const useDeploymentLauncher = (): {
53+
deployInstantly: (input: QuickDeployInput) => Promise<DeployInstantlyResult>;
54+
openLauncher: (input: QuickDeployInput) => void;
55+
isDeploying: boolean;
56+
supportsQuickDeploy: boolean;
57+
} => {
58+
'use memo';
59+
60+
const { t } = useTranslation();
61+
const navigate = useNavigate();
62+
const baiClient = useSuspendedBackendaiClient();
63+
const currentDomain = useCurrentDomainValue();
64+
const { id: projectId } = useCurrentProjectValue();
65+
const currentResourceGroup = useCurrentResourceGroupValue();
66+
const { upsertNotification } = useSetBAINotification();
67+
const { logger } = useBAILogger();
68+
69+
// Track in-flight deploys ourselves so consumers can disable their entry
70+
// point while the mutation is resolving. Relay's `useMutation` does expose
71+
// an `isInFlight` flag but it resets between consecutive calls — we use an
72+
// explicit counter-ish flag so the notification upsert and the return path
73+
// agree on a single boolean.
74+
const [isDeploying, setIsDeploying] = useState<boolean>(false);
75+
76+
const [commitCreateDeployment] =
77+
useMutation<useDeploymentLauncherCreateMutation>(graphql`
78+
mutation useDeploymentLauncherCreateMutation(
79+
$input: CreateDeploymentInput!
80+
) {
81+
createModelDeployment(input: $input) {
82+
deployment {
83+
id
84+
metadata {
85+
name
86+
}
87+
}
88+
}
89+
}
90+
`);
91+
92+
// Gate behind the `model-deployment-extended-filter` feature flag, which
93+
// is wired up in FR-2663 to mark manager 26.4.3+ as supporting the full
94+
// v2 deployment lifecycle (createModelDeployment / addModelRevision /
95+
// richer endpoint polling). Consumers that need the legacy
96+
// `useModelServiceLauncher` path should branch on this flag being false.
97+
const supportsQuickDeploy = baiClient.supports(
98+
'model-deployment-extended-filter',
99+
);
100+
101+
const deployInstantly = async (
102+
input: QuickDeployInput,
103+
): Promise<DeployInstantlyResult> => {
104+
if (!projectId) {
105+
const error = new Error('No current project selected.');
106+
logger.error('[useDeploymentLauncher] deployInstantly failed', error);
107+
throw error;
108+
}
109+
110+
// TODO(needs-backend): FR-2683 — wire `modelFolderId` / `modelVersion` /
111+
// `resourceGroup` / `resourcePreset` into `initialRevision` once the
112+
// Quick Deploy preset contract is finalized. Today
113+
// `createModelDeployment` accepts `initialRevision: null` (nullable in
114+
// the schema), so the Deployment is created empty and FR-2684 callers
115+
// are expected to chain an `addModelRevision` mutation for the actual
116+
// runtime config. We still read `resourceGroup` so consumers can pass
117+
// it through unchanged once that wiring lands.
118+
void input.modelVersion;
119+
void input.resourcePreset;
120+
void (input.resourceGroup ?? currentResourceGroup);
121+
122+
const replicas = input.replicas ?? 1;
123+
const openToPublic = input.openToPublic ?? false;
124+
125+
// Key the notification by folder + timestamp so repeated Quick Deploys
126+
// from the same folder don't collide in the BAI notification store.
127+
const notificationKey = `deployment-launcher-${input.modelFolderId}-${Date.now()}`;
128+
129+
setIsDeploying(true);
130+
upsertNotification({
131+
key: notificationKey,
132+
open: true,
133+
message: t('modelService.StartingModelService'),
134+
description: null,
135+
duration: 0,
136+
backgroundTask: {
137+
status: 'pending',
138+
percent: 0,
139+
},
140+
});
141+
142+
return new Promise<DeployInstantlyResult>((resolve, reject) => {
143+
commitCreateDeployment({
144+
variables: {
145+
input: {
146+
metadata: {
147+
projectId,
148+
domainName: currentDomain,
149+
name: null,
150+
tags: null,
151+
},
152+
networkAccess: {
153+
preferredDomainName: null,
154+
openToPublic,
155+
},
156+
defaultDeploymentStrategy: {
157+
type: 'ROLLING',
158+
},
159+
desiredReplicaCount: replicas,
160+
initialRevision: null,
161+
},
162+
},
163+
onCompleted: (response, errors) => {
164+
setIsDeploying(false);
165+
if (errors && errors.length > 0) {
166+
const message = errors.map((e) => e.message).join('\n');
167+
logger.error(
168+
'[useDeploymentLauncher] createModelDeployment returned errors',
169+
errors,
170+
);
171+
upsertNotification({
172+
key: notificationKey,
173+
open: true,
174+
message: t('modelStore.DeployFailed'),
175+
description: message,
176+
duration: 0,
177+
backgroundTask: {
178+
status: 'rejected',
179+
percent: 99,
180+
},
181+
});
182+
reject(new Error(message));
183+
return;
184+
}
185+
186+
const globalId = response.createModelDeployment.deployment.id;
187+
const deploymentId = toLocalId(globalId) ?? globalId;
188+
189+
upsertNotification({
190+
key: notificationKey,
191+
open: true,
192+
message: t('modelStore.DeploySuccess'),
193+
description: null,
194+
duration: 0,
195+
backgroundTask: {
196+
status: 'resolved',
197+
percent: 100,
198+
},
199+
to: `/deployments/${deploymentId}`,
200+
toText: t('modelService.GoToServiceDetailPage'),
201+
});
202+
203+
resolve({ deploymentId });
204+
},
205+
onError: (error) => {
206+
setIsDeploying(false);
207+
logger.error(
208+
'[useDeploymentLauncher] createModelDeployment failed',
209+
error,
210+
);
211+
upsertNotification({
212+
key: notificationKey,
213+
open: true,
214+
message: t('modelStore.DeployFailed'),
215+
description: error?.message ?? null,
216+
duration: 0,
217+
backgroundTask: {
218+
status: 'rejected',
219+
percent: 99,
220+
},
221+
});
222+
reject(error);
223+
},
224+
});
225+
});
226+
};
227+
228+
const openLauncher = (input: QuickDeployInput): void => {
229+
const params = new URLSearchParams({ model: input.modelFolderId });
230+
navigate(`/deployments/new?${params.toString()}`);
231+
};
232+
233+
return {
234+
deployInstantly,
235+
openLauncher,
236+
isDeploying,
237+
supportsQuickDeploy,
238+
};
239+
};
240+
241+
export default useDeploymentLauncher;

0 commit comments

Comments
 (0)