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