diff --git a/react/src/components/EnvVarFormList.tsx b/react/src/components/EnvVarFormList.tsx index 0286a3f8ce..b08ac94902 100644 --- a/react/src/components/EnvVarFormList.tsx +++ b/react/src/components/EnvVarFormList.tsx @@ -165,7 +165,7 @@ export function isSensitiveEnv(key: string) { } export function sanitizeSensitiveEnv(envs: EnvVarFormListValue[]) { - return envs.map((env) => { + return _.map(envs, (env) => { if (env && isSensitiveEnv(env.variable)) { return { ...env, value: '' }; } diff --git a/react/src/components/ImageEnvironmentSelectFormItems.tsx b/react/src/components/ImageEnvironmentSelectFormItems.tsx index 8fc325c3a4..3e5027ed16 100644 --- a/react/src/components/ImageEnvironmentSelectFormItems.tsx +++ b/react/src/components/ImageEnvironmentSelectFormItems.tsx @@ -97,7 +97,7 @@ const ImageEnvironmentSelectFormItems: React.FC< const [environmentSearch, setEnvironmentSearch] = useState(''); const [versionSearch, setVersionSearch] = useState(''); const { t } = useTranslation(); - const [metadata, { getImageMeta }] = useBackendAIImageMetaData(); + const [metadata, { getImageMeta, tagAlias }] = useBackendAIImageMetaData(); const { token } = theme.useToken(); const { isDarkMode } = useThemeMode(); @@ -129,6 +129,12 @@ const ImageEnvironmentSelectFormItems: React.FC< value } namespace @since(version: "24.09.1") + base_image_name @since(version: "24.09.1") + tags @since(version: "24.09.1") { + key + value + } + version @since(version: "24.09.1") } } `, @@ -498,7 +504,7 @@ const ImageEnvironmentSelectFormItems: React.FC< }} /> - {environmentGroup.displayName} + {tagAlias(environmentGroup.displayName)} - {t('session.launcher.Version')} - - {t('session.launcher.Base')} - - {t('session.launcher.Architecture')} - - {t('session.launcher.Requirements')} + {supportExtendedImageInfo ? ( + <> + {t('session.launcher.Version')} + + {t('session.launcher.Architecture')} + + {t('session.launcher.Tags')} + + ) : ( + <> + {t('session.launcher.Version')} + + {t('session.launcher.Base')} + + {t('session.launcher.Architecture')} + + {t('session.launcher.Requirements')} + + )} {menu} @@ -602,18 +620,21 @@ const ImageEnvironmentSelectFormItems: React.FC< '-', ) || ['', '', '']; - let tagAlias = metadata?.tagAlias[tag]; - if (!tagAlias) { + let metadataTagAlias = metadata?.tagAlias[tag]; + if (!metadataTagAlias) { for (const [key, replaceString] of Object.entries( metadata?.tagReplace || {}, )) { const pattern = new RegExp(key); if (pattern.test(tag)) { - tagAlias = tag?.replace(pattern, replaceString); + metadataTagAlias = tag?.replace( + pattern, + replaceString, + ); } } - if (!tagAlias) { - tagAlias = tag; + if (!metadataTagAlias) { + metadataTagAlias = tag; } } @@ -695,39 +716,98 @@ const ImageEnvironmentSelectFormItems: React.FC< value={getImageFullName(image)} filterValue={[ version, - tagAlias, + metadataTagAlias, image?.architecture, ...extraFilterValues, ].join('\t')} > - + {supportExtendedImageInfo ? ( - {version} - - - - {tagAlias} + {image?.version} {image?.architecture} + + + {/* TODO: replace this with AliasedImageDoubleTags after image list query with ImageNode is implemented. */} + {_.map( + image?.tags, + (tag: { key: string; value: string }) => { + const isCustomized = _.includes( + tag.key, + 'customized_', + ); + const tagValue = isCustomized + ? _.find(image?.labels, { + key: 'ai.backend.customized-image.name', + })?.value + : tag.value; + return ( + + {tagAlias(tag.key)} + + ), + color: isCustomized ? 'cyan' : 'blue', + }, + { + label: ( + + {tagValue} + + ), + color: isCustomized ? 'cyan' : 'blue', + }, + ]} + /> + ); + }, + )} + - - {requirementTags || '-'} + ) : ( + + + + {version} + + + + {metadataTagAlias} + + + + {image?.architecture} + + + + {requirementTags || '-'} + - + )} ); }, diff --git a/react/src/components/ImageList.tsx b/react/src/components/ImageList.tsx index 9ad821a7bb..e9e4bc802c 100644 --- a/react/src/components/ImageList.tsx +++ b/react/src/components/ImageList.tsx @@ -339,6 +339,7 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { } + highlightKeyword={imageSearch} /> ) : null, }, diff --git a/react/src/components/ResourceAllocationFormItems.test.ts b/react/src/components/ResourceAllocationFormItems.test.ts index be592d9055..5cd4e965c7 100644 --- a/react/src/components/ResourceAllocationFormItems.test.ts +++ b/react/src/components/ResourceAllocationFormItems.test.ts @@ -46,6 +46,9 @@ describe('getAllocatablePresetNames', () => { registry: 'registry1', tag: 'tag1', resource_limits: [{ key: 'cuda.shares', min: '1', max: '1' }], + base_image_name: undefined, + tags: undefined, + version: undefined, }; it('should return presets when currentImage has accelerator limits', () => { diff --git a/react/src/hooks/useBackendAIImageMetaData.test.tsx b/react/src/hooks/useBackendAIImageMetaData.test.tsx index df4ced5a7f..11ca50bb1b 100644 --- a/react/src/hooks/useBackendAIImageMetaData.test.tsx +++ b/react/src/hooks/useBackendAIImageMetaData.test.tsx @@ -145,6 +145,9 @@ describe('useBackendAIImageMetaData', () => { value: 'NVIDIA CORPORATION ', }, ], + base_image_name: undefined, + tags: undefined, + version: undefined, }) || '', ); expect(key).toBe('training'); diff --git a/react/src/pages/SessionLauncherPage.tsx b/react/src/pages/SessionLauncherPage.tsx index b63de932d6..4d96e2b84d 100644 --- a/react/src/pages/SessionLauncherPage.tsx +++ b/react/src/pages/SessionLauncherPage.tsx @@ -39,9 +39,11 @@ import VFolderTableFormItem, { import { compareNumberWithUnits, generateRandomString, + getImageFullName, iSizeToSize, } from '../helper'; import { + useBackendAIImageMetaData, useSuspendedBackendaiClient, useUpdatableState, useWebUINavigate, @@ -73,6 +75,7 @@ import { Checkbox, Col, Descriptions, + Divider, Form, Grid, Input, @@ -184,6 +187,10 @@ const SessionLauncherPage = () => { const currentUserRole = useCurrentUserRole(); const [currentGlobalResourceGroup, setCurrentGlobalResourceGroup] = useCurrentResourceGroupState(); + const [, { tagAlias }] = useBackendAIImageMetaData(); + + const supportExtendedImageInfo = + baiClient?.supports('extended-image-info') ?? false; const [isStartingSession, setIsStartingSession] = useState(false); const INITIAL_FORM_VALUES: DeepPartial = useMemo( @@ -1295,67 +1302,188 @@ const SessionLauncherPage = () => { {currentProject.name} - - - - - - {/* {form.getFieldValue('environments').image} */} - - {form.getFieldValue('environments')?.manual ? ( - - {form.getFieldValue('environments')?.manual} - - ) : ( - <> - + + + + + + {form.getFieldValue('environments') + ?.manual ? ( + + { form.getFieldValue('environments') - ?.version + ?.manual } - /> - {form.getFieldValue('environments') - ?.customizedTag ? ( - + ) : ( + <> + + {tagAlias( + form.getFieldValue('environments') + ?.image?.base_image_name, + )} + + + + { + form.getFieldValue('environments') + ?.image?.version + } + + + + { + form.getFieldValue('environments') + ?.image?.architecture + } + + + {/* TODO: replace this with AliasedImageDoubleTags after image list query with ImageNode is implemented. */} + {_.map( + form.getFieldValue('environments') + ?.image?.tags, + (tag: { + key: string; + value: string; + }) => { + const isCustomized = _.includes( + tag.key, + 'customized_', + ); + const tagValue = isCustomized + ? _.find( + form.getFieldValue( + 'environments', + )?.image?.labels, + { + key: 'ai.backend.customized-image.name', + }, + )?.value + : tag.value; + return ( + + ); + }, + )} + - ) : null} + + )} + + + + ) : ( + + + + + + {/* {form.getFieldValue('environments').image} */} + + {form.getFieldValue('environments') + ?.manual ? ( - - )} - - - + > + { + form.getFieldValue('environments') + ?.manual + } + + ) : ( + <> + + {form.getFieldValue('environments') + ?.customizedTag ? ( + + ) : null} + + + )} + + + + )} {form.getFieldValue('envvars')?.length > 0 && (