Skip to content

Commit ec74a6f

Browse files
committed
fix(FR-2831): disable Deploy as service action when no compatible presets exist
1 parent fc0b319 commit ec74a6f

26 files changed

Lines changed: 149 additions & 3 deletions

react/src/components/VFolderNodes.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,23 @@ interface VFolderNameCellProps {
103103
* additionally redirects admins (project/domain/super) to that page.
104104
*/
105105
disableProjectFolderActions?: boolean;
106+
/**
107+
* When true, the row-level "Deploy as service" action for model folders
108+
* is rendered disabled with a tooltip explaining that no deployment
109+
* presets are available. Computed once at the page level (via the page's
110+
* `useLazyLoadQuery` selecting `deploymentRevisionPresets(first: 0) { count }`)
111+
* and forwarded down through `VFolderNodes` so we don't fire one query
112+
* per row. The `VFolderDeployModal` retains a `null`-return fallback for
113+
* the same condition as defense in depth.
114+
*
115+
* TODO(needs-backend): the schema exposes
116+
* `modelCardAvailablePresets(scope: { modelCardId })`, but we have a
117+
* vfolder, not a model card, in this row. Either add a vfolder scope
118+
* (`VFolderAvailablePresetsScope`) or expose a vfolder→modelCard link
119+
* so we can narrow this check per row. Today this boolean reflects
120+
* "any preset exists in this project."
121+
*/
122+
hasNoCompatiblePresets?: boolean;
106123
}
107124

108125
const VFolderNameCell: React.FC<VFolderNameCellProps> = ({
@@ -113,6 +130,7 @@ const VFolderNameCell: React.FC<VFolderNameCellProps> = ({
113130
onDeleteForever,
114131
onStartServiceFallback,
115132
disableProjectFolderActions = false,
133+
hasNoCompatiblePresets = false,
116134
}) => {
117135
'use memo';
118136
const { t } = useTranslation();
@@ -148,6 +166,10 @@ const VFolderNameCell: React.FC<VFolderNameCellProps> = ({
148166
key: 'start-service',
149167
title: t('modelService.DeployAsService'),
150168
icon: <BAIEndpointsIcon />,
169+
disabled: hasNoCompatiblePresets,
170+
disabledReason: hasNoCompatiblePresets
171+
? t('data.folders.NoCompatibleDeploymentPresets')
172+
: undefined,
151173
onClick: () => onStartServiceFallback(vfolderId),
152174
}
153175
: null,
@@ -248,12 +270,29 @@ interface VFolderNodesProps extends Omit<
248270
* component. This prop is the V1-friendly stopgap.
249271
*/
250272
disableProjectFolderActions?: boolean;
273+
/**
274+
* When true, the row-level "Deploy as service" action for model folders
275+
* is rendered disabled with a tooltip explaining that no deployment
276+
* presets are available. Derived at the page-level `useLazyLoadQuery`
277+
* from `deploymentRevisionPresets(first: 0) { count }` so we don't fire
278+
* one query per row. Defaults to `false` so existing call sites are
279+
* additive — page hosts opt in by passing the derived value.
280+
*
281+
* TODO(needs-backend): the schema exposes
282+
* `modelCardAvailablePresets(scope: { modelCardId })`, but we have a
283+
* vfolder, not a model card, in this row. Either add a vfolder scope
284+
* (`VFolderAvailablePresetsScope`) or expose a vfolder→modelCard link
285+
* so we can narrow this check per row. Today this boolean reflects
286+
* "any preset exists in this project."
287+
*/
288+
hasNoCompatiblePresets?: boolean;
251289
}
252290

253291
const VFolderNodes: React.FC<VFolderNodesProps> = ({
254292
vfoldersFrgmt,
255293
onRemoveRow,
256294
disableProjectFolderActions,
295+
hasNoCompatiblePresets = false,
257296
...tableProps
258297
}) => {
259298
const { t } = useTranslation();
@@ -346,6 +385,7 @@ const VFolderNodes: React.FC<VFolderNodesProps> = ({
346385
<VFolderNameCell
347386
vfolder={vfolder}
348387
disableProjectFolderActions={disableProjectFolderActions}
388+
hasNoCompatiblePresets={hasNoCompatiblePresets}
349389
onShare={() => {
350390
vfolder?.user === currentUser?.uuid
351391
? setInviteFolderId(toLocalId(vfolder?.id ?? null))

react/src/components/VFolderNodesV2.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,23 @@ interface VFolderNameCellProps {
119119
* (FR-2599) for the given vfolder instead of navigating away.
120120
*/
121121
onStartServiceFallback: (vfolderId: string) => void;
122+
/**
123+
* When true, the row-level "Deploy as service" action for model folders
124+
* is rendered disabled with a tooltip explaining that no deployment
125+
* presets are available. Computed once at the page level (via the page's
126+
* `useLazyLoadQuery` selecting `deploymentRevisionPresets(first: 0) { count }`)
127+
* and forwarded down through `VFolderNodesV2` so we don't fire one query
128+
* per row. The `VFolderDeployModal` retains a `null`-return fallback for
129+
* the same condition as defense in depth.
130+
*
131+
* TODO(needs-backend): the schema exposes
132+
* `modelCardAvailablePresets(scope: { modelCardId })`, but we have a
133+
* vfolder, not a model card, in this row. Either add a vfolder scope
134+
* (`VFolderAvailablePresetsScope`) or expose a vfolder→modelCard link
135+
* so we can narrow this check per row. Today this boolean reflects
136+
* "any preset exists in this project."
137+
*/
138+
hasNoCompatiblePresets?: boolean;
122139
}
123140

124141
const VFolderNameCell: React.FC<VFolderNameCellProps> = ({
@@ -128,6 +145,7 @@ const VFolderNameCell: React.FC<VFolderNameCellProps> = ({
128145
onRestore,
129146
onDeleteForever,
130147
onStartServiceFallback,
148+
hasNoCompatiblePresets = false,
131149
}) => {
132150
'use memo';
133151
const { t } = useTranslation();
@@ -147,6 +165,10 @@ const VFolderNameCell: React.FC<VFolderNameCellProps> = ({
147165
key: 'start-service',
148166
title: t('modelService.DeployAsService'),
149167
icon: <BAIEndpointsIcon />,
168+
disabled: hasNoCompatiblePresets,
169+
disabledReason: hasNoCompatiblePresets
170+
? t('data.folders.NoCompatibleDeploymentPresets')
171+
: undefined,
150172
onClick: () => onStartServiceFallback(vfolderId),
151173
}
152174
: null,
@@ -409,11 +431,28 @@ interface VFolderNodesV2Props extends Omit<
409431
vfoldersFrgmt: VFolderNodesV2Fragment$key;
410432
// Callback when a row is removed from current list
411433
onRemoveRow?: (updatedFolderId?: string) => void;
434+
/**
435+
* When true, the row-level "Deploy as service" action for model folders
436+
* is rendered disabled with a tooltip explaining that no deployment
437+
* presets are available. Derived at the page-level `useLazyLoadQuery`
438+
* from `deploymentRevisionPresets(first: 0) { count }` so we don't fire
439+
* one query per row. Defaults to `false` so existing call sites are
440+
* additive — page hosts opt in by passing the derived value.
441+
*
442+
* TODO(needs-backend): the schema exposes
443+
* `modelCardAvailablePresets(scope: { modelCardId })`, but we have a
444+
* vfolder, not a model card, in this row. Either add a vfolder scope
445+
* (`VFolderAvailablePresetsScope`) or expose a vfolder→modelCard link
446+
* so we can narrow this check per row. Today this boolean reflects
447+
* "any preset exists in this project."
448+
*/
449+
hasNoCompatiblePresets?: boolean;
412450
}
413451

414452
const VFolderNodesV2: React.FC<VFolderNodesV2Props> = ({
415453
vfoldersFrgmt,
416454
onRemoveRow,
455+
hasNoCompatiblePresets = false,
417456
...tableProps
418457
}) => {
419458
'use memo';
@@ -570,6 +609,7 @@ const VFolderNodesV2: React.FC<VFolderNodesV2Props> = ({
570609
return (
571610
<VFolderNameCell
572611
vfolder={vfolder}
612+
hasNoCompatiblePresets={hasNoCompatiblePresets}
573613
onShare={() => {
574614
vfolder?.ownership?.userId === currentUser?.uuid
575615
? setInviteFolderId(toLocalId(vfolder?.id ?? null))

react/src/pages/AdminVFolderNodeListPage.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
147147
const deferredQueryVariables = useDeferredValue(queryVariables);
148148
const deferredFetchKey = useDeferredValue(fetchKey);
149149

150-
const { vfolder_nodes, ...folderCounts } =
150+
const { vfolder_nodes, deploymentRevisionPresets, ...folderCounts } =
151151
useLazyLoadQuery<AdminVFolderNodeListPageQuery>(
152152
graphql`
153153
query AdminVFolderNodeListPageQuery(
@@ -198,6 +198,17 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
198198
) {
199199
count
200200
}
201+
# Project-scoped check: gates the row-level Deploy as service
202+
# action so model folders show a disabled tooltip instead of a
203+
# fully interactive button that opens a modal which then returns
204+
# null (FR-2831). Hoisted to the page-level query so we do not
205+
# fire one query per row or per table.
206+
# first: 1 rather than 0 because the backend's
207+
# SearchDeploymentRevisionPresetsInput validates first >= 1.
208+
# We only read count, so the single returned edge is unused.
209+
deploymentRevisionPresets(first: 1) {
210+
count
211+
}
201212
}
202213
`,
203214
deferredQueryVariables,
@@ -459,6 +470,10 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
459470
<VFolderNodes
460471
order={queryParams.order}
461472
loading={deferredQueryVariables !== queryVariables}
473+
// True only when we have a definitive zero — `null`/`undefined`
474+
// (loading or query error) keeps the action enabled so we don't
475+
// surface a misleading "no presets" tooltip in those cases.
476+
hasNoCompatiblePresets={deploymentRevisionPresets?.count === 0}
462477
vfoldersFrgmt={filterOutNullAndUndefined(
463478
_.map(vfolder_nodes?.edges, 'node'),
464479
)}

react/src/pages/ProjectAdminDataPage.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ const ProjectAdminDataContent: React.FC<ProjectAdminDataContentProps> = ({
185185
const deferredQueryVariables = useDeferredValue(queryVariables);
186186
const deferredFetchKey = useDeferredValue(fetchKey);
187187

188-
const { projectVfolders, ...folderCounts } =
188+
const { projectVfolders, deploymentRevisionPresets, ...folderCounts } =
189189
useLazyLoadQuery<ProjectAdminDataPageQuery>(
190190
graphql`
191191
query ProjectAdminDataPageQuery(
@@ -229,6 +229,17 @@ const ProjectAdminDataContent: React.FC<ProjectAdminDataContentProps> = ({
229229
) {
230230
count
231231
}
232+
# Project-scoped check: gates the row-level Deploy as service
233+
# action so model folders show a disabled tooltip instead of a
234+
# fully interactive button that opens a modal which then returns
235+
# null (FR-2831). Hoisted to the page-level query so we do not
236+
# fire one query per row or per table.
237+
# first: 1 rather than 0 because the backend's
238+
# SearchDeploymentRevisionPresetsInput validates first >= 1.
239+
# We only read count, so the single returned edge is unused.
240+
deploymentRevisionPresets(first: 1) {
241+
count
242+
}
232243
}
233244
`,
234245
deferredQueryVariables,
@@ -433,6 +444,10 @@ const ProjectAdminDataContent: React.FC<ProjectAdminDataContentProps> = ({
433444
<VFolderNodesV2
434445
order={queryParams.order}
435446
loading={deferredQueryVariables !== queryVariables}
447+
// True only when we have a definitive zero — `null`/`undefined`
448+
// (loading or query error) keeps the action enabled so we don't
449+
// surface a misleading "no presets" tooltip in those cases.
450+
hasNoCompatiblePresets={deploymentRevisionPresets?.count === 0}
436451
vfoldersFrgmt={filterOutNullAndUndefined(
437452
_.map(projectVfolders?.edges, 'node'),
438453
)}

react/src/pages/VFolderNodeListPage.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ const VFolderNodeListPage: React.FC<VFolderNodeListPageProps> = ({
181181
// eslint-disable-next-line react-hooks/exhaustive-deps
182182
}, [invitations.length]);
183183

184-
const { vfolder_nodes, ...folderCounts } =
184+
const { vfolder_nodes, deploymentRevisionPresets, ...folderCounts } =
185185
useLazyLoadQuery<VFolderNodeListPageQuery>(
186186
graphql`
187187
query VFolderNodeListPageQuery(
@@ -236,6 +236,17 @@ const VFolderNodeListPage: React.FC<VFolderNodeListPageProps> = ({
236236
) {
237237
count
238238
}
239+
# Project-scoped check: gates the row-level Deploy as service
240+
# action so model folders show a disabled tooltip instead of a
241+
# fully interactive button that opens a modal which then returns
242+
# null (FR-2831). Hoisted to the page-level query so we do not
243+
# fire one query per row or per table.
244+
# first: 1 rather than 0 because the backend's
245+
# SearchDeploymentRevisionPresetsInput validates first >= 1.
246+
# We only read count, so the single returned edge is unused.
247+
deploymentRevisionPresets(first: 1) {
248+
count
249+
}
239250
}
240251
`,
241252
deferredQueryVariables,
@@ -509,6 +520,10 @@ const VFolderNodeListPage: React.FC<VFolderNodeListPageProps> = ({
509520
order={queryParams.order}
510521
loading={deferredQueryVariables !== queryVariables}
511522
disableProjectFolderActions
523+
// True only when we have a definitive zero — `null`/`undefined`
524+
// (loading or query error) keeps the action enabled so we don't
525+
// surface a misleading "no presets" tooltip in those cases.
526+
hasNoCompatiblePresets={deploymentRevisionPresets?.count === 0}
512527
vfoldersFrgmt={filterOutNullAndUndefined(
513528
_.map(vfolder_nodes?.edges, 'node'),
514529
)}

resources/i18n/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@
790790
"MultipleFolderDeletedForever": "{{count}} von {{total}} Ordnern wurden dauerhaft gelöscht.",
791791
"MultipleFolderRestored": "Ausgewählte {{ folderLength }} Ordner wurden wiederhergestellt.",
792792
"Name": "Name",
793+
"NoCompatibleDeploymentPresets": "No deployment presets available in this project.",
793794
"NoDeletePermission": "Keine Löschberechtigung",
794795
"NoFolderToDisplay": "Keine Ordner anzeigen",
795796
"NoRestorePermission": "Keine Wiederherstellungsberechtigung",

resources/i18n/el.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@
790790
"MultipleFolderDeletedForever": "Διαγράφηκαν οριστικά {{count}} από τους {{total}} φακέλους.",
791791
"MultipleFolderRestored": "Επιλεγμένα {{ folderLength }} Φάκελοι έχει αποκατασταθεί.",
792792
"Name": "Ονομα",
793+
"NoCompatibleDeploymentPresets": "No deployment presets available in this project.",
793794
"NoDeletePermission": "Δεν υπάρχει δικαίωμα διαγραφής",
794795
"NoFolderToDisplay": "Δεν εμφανίζονται φάκελοι",
795796
"NoRestorePermission": "Δεν υπάρχει δικαίωμα επαναφοράς",

resources/i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,7 @@
791791
"MultipleFolderDeletedForever": "Permanently deleted {{count}} folders out of {{total}}.",
792792
"MultipleFolderRestored": "Selected {{ folderLength }} folders has been restored.",
793793
"Name": "Name",
794+
"NoCompatibleDeploymentPresets": "No deployment presets available in this project.",
794795
"NoDeletePermission": "No delete permission",
795796
"NoFolderToDisplay": "No Folders to display",
796797
"NoRestorePermission": "No restore permission",

resources/i18n/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@
790790
"MultipleFolderDeletedForever": "Se eliminaron permanentemente {{count}} carpetas de {{total}}.",
791791
"MultipleFolderRestored": "Se han restaurado las carpetas seleccionadas {{ folderLength }}.",
792792
"Name": "Nombre",
793+
"NoCompatibleDeploymentPresets": "No deployment presets available in this project.",
793794
"NoDeletePermission": "Sin permiso de eliminación",
794795
"NoFolderToDisplay": "No hay carpetas que mostrar",
795796
"NoRestorePermission": "Sin permiso de restauración",

resources/i18n/fi.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@
790790
"MultipleFolderDeletedForever": "Poistettiin pysyvästi {{count}}/{{total}} kansiota.",
791791
"MultipleFolderRestored": "Valitut {{ folderLength }} kansiot on palautettu.",
792792
"Name": "Nimi",
793+
"NoCompatibleDeploymentPresets": "No deployment presets available in this project.",
793794
"NoDeletePermission": "Ei poistolupaa",
794795
"NoFolderToDisplay": "Ei näytettäviä kansioita",
795796
"NoRestorePermission": "Ei palautuslupaa",

0 commit comments

Comments
 (0)