diff --git a/react/src/components/CustomizedImageList.tsx b/react/src/components/CustomizedImageList.tsx index e727a8f871..53c9f6f23c 100644 --- a/react/src/components/CustomizedImageList.tsx +++ b/react/src/components/CustomizedImageList.tsx @@ -1,9 +1,4 @@ import Flex from '../components/Flex'; -import { - BaseImageTags, - ConstraintTags, - LangTags, -} from '../components/ImageTags'; import TableColumnsSettingModal from '../components/TableColumnsSettingModal'; import { filterEmptyItem, @@ -17,6 +12,7 @@ import { useUpdatableState, } from '../hooks'; import AliasedImageDoubleTags from './AliasedImageDoubleTags'; +import { ImageTags } from './ImageTags'; import TextHighlighter from './TextHighlighter'; import { CustomizedImageListForgetAndUntagMutation } from './__generated__/CustomizedImageListForgetAndUntagMutation.graphql'; import { @@ -63,19 +59,8 @@ const CustomizedImageList: React.FC = ({ children }) => { const [inFlightImageId, setInFlightImageId] = useState(); const [imageSearch, setImageSearch] = useState(''); const [isPendingSearchTransition, startSearchTransition] = useTransition(); - const [ - , - { - getNamespace, - getImageLang, - getBaseVersion, - getBaseImage, - getConstraints, - tagAlias, - getLang, - getBaseImages, - }, - ] = useBackendAIImageMetaData(); + const [, { getBaseVersion, getBaseImages, getBaseImage, tagAlias, getTags }] = + useBackendAIImageMetaData(); const { customized_images } = useLazyLoadQuery( graphql` @@ -142,17 +127,14 @@ const CustomizedImageList: React.FC = ({ children }) => { fullName: getImageFullName(image) || '', digest: image?.digest || '', // ------------ need only before 24.12.0 ------------ - lang: image?.name ? getLang(image.name) : '', baseversion: getBaseVersion(getImageFullName(image) || ''), baseimage: image?.tag && image?.name ? getBaseImages(image.tag, image.name) : [], - constraints: - image?.tag && image?.labels - ? getConstraints( - image.tag, - image.labels as { key: string; value: string }[], - ) - : [], + tag: + getTags( + image?.tag || '', + image?.labels as Array<{ key: string; value: string }>, + ) || [], isCustomized: image?.tag ? image.tag.indexOf('customized') !== -1 : false, @@ -180,14 +162,13 @@ const CustomizedImageList: React.FC = ({ children }) => { const baseImagesMatch = _.some(curFilterValues.baseimage, (value) => regExp.test(value), ); - const constraintsMatch = _.some( - curFilterValues.constraints, - (constraint) => regExp.test(constraint), + const tagMatch = _.some( + curFilterValues.tag, + (tag) => regExp.test(tag.key) || regExp.test(tag.value), ); const customizedMatch = curFilterValues.isCustomized ? regExp.test('customized') : false; - const langMatch = regExp.test(curFilterValues.lang); const namespaceMatch = regExp.test(curFilterValues.namespace || ''); const fullNameMatch = regExp.test(curFilterValues.fullName); const tagsMatch = _.some( @@ -200,8 +181,7 @@ const CustomizedImageList: React.FC = ({ children }) => { return ( baseVersionMatch || baseImagesMatch || - constraintsMatch || - langMatch || + tagMatch || namespaceMatch || customizedMatch || fullNameMatch || @@ -281,7 +261,7 @@ const CustomizedImageList: React.FC = ({ children }) => { title: t('environment.Tags'), key: 'tags', dataIndex: 'tags', - render: (text, row) => ( + render: (text: Array<{ key: string; value: string }>, row) => ( = ({ children }) => { title: t('environment.Namespace'), key: 'name', dataIndex: 'name', - sorter: (a, b) => { - const namespaceA = getNamespace(getImageFullName(a) || ''); - const namespaceB = getNamespace(getImageFullName(b) || ''); - return localeCompare(namespaceA, namespaceB); - }, - render: (text, row) => ( - {getNamespace(getImageFullName(row) || '')} - ), - }, - !supportExtendedImageInfo && { - title: t('environment.Language'), - key: 'lang', - sorter: (a, b) => - localeCompare( - getImageLang(getImageFullName(a) || ''), - getImageLang(getImageFullName(b) || ''), - ), - render: (text, row) => ( - + sorter: (a, b) => localeCompare(getImageFullName(a), getImageFullName(b)), + render: (text) => ( + {text} ), }, !supportExtendedImageInfo && { @@ -340,38 +304,23 @@ const CustomizedImageList: React.FC = ({ children }) => { getBaseImage(getImageFullName(b) || ''), ), render: (text, row) => ( - + + {tagAlias(getBaseImage(getImageFullName(row) || ''))} + ), }, !supportExtendedImageInfo && { - title: t('environment.Constraint'), - key: 'constraint', - dataIndex: 'constraint', - sorter: (a, b) => { - const requirementA = - a?.tag && b?.labels - ? getConstraints( - a?.tag, - a?.labels as { key: string; value: string }[], - )[0] || '' - : ''; - const requirementB = - b?.tag && b?.labels - ? getConstraints( - b?.tag, - b?.labels as { key: string; value: string }[], - )[0] || '' - : ''; - return localeCompare(requirementA, requirementB); - }, - render: (text, row) => - row?.tag ? ( - } - highlightKeyword={imageSearch} - /> - ) : null, + title: t('environment.Tags'), + key: 'tag', + dataIndex: 'tag', + sorter: (a, b) => localeCompare(a?.tag, b?.tag), + render: (text, row) => ( + } + highlightKeyword={imageSearch} + /> + ), }, { title: t('environment.Digest'), diff --git a/react/src/components/ImageList.tsx b/react/src/components/ImageList.tsx index aaf528dd3f..27751b7639 100644 --- a/react/src/components/ImageList.tsx +++ b/react/src/components/ImageList.tsx @@ -12,7 +12,7 @@ import { } from '../hooks'; import DoubleTag from './DoubleTag'; import ImageInstallModal from './ImageInstallModal'; -import { BaseImageTags, ConstraintTags, LangTags } from './ImageTags'; +import { ImageTags } from './ImageTags'; import ManageAppsModal from './ManageAppsModal'; import ManageImageResourceLimitModal from './ManageImageResourceLimitModal'; import ResourceNumber from './ResourceNumber'; @@ -45,18 +45,8 @@ export type EnvironmentImage = NonNullable< const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { const { t } = useTranslation(); const [selectedRows, setSelectedRows] = useState([]); - const [ - , - { - getNamespace, - getBaseVersion, - getLang, - getBaseImages, - getConstraints, - getBaseImage, - tagAlias, - }, - ] = useBackendAIImageMetaData(); + const [, { getBaseVersion, getBaseImages, getBaseImage, tagAlias, getTags }] = + useBackendAIImageMetaData(); const { token } = theme.useToken(); const [managingApp, setManagingApp] = useState(null); const [managingResourceLimit, setManagingResourceLimit] = @@ -194,7 +184,7 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { key: 'base_image_name', dataIndex: 'base_image_name', sorter: (a, b) => localeCompare(a?.base_image_name, b?.base_image_name), - render: (text, row) => ( + render: (text) => ( {tagAlias(text)} @@ -213,7 +203,10 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { title: t('environment.Tags'), key: 'tags', dataIndex: 'tags', - render: (text: Array<{ key: string; value: string }>, row) => { + render: ( + text: Array<{ key: string; value: string }>, + row: EnvironmentImage, + ) => { return ( {/* TODO: replace this with AliasedImageDoubleTags after image list query with ImageNode is implemented. */} @@ -257,20 +250,23 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { key: 'name', dataIndex: 'name', sorter: (a, b) => localeCompare(getImageFullName(a), getImageFullName(b)), - render: (text, row) => ( - - {getNamespace(getImageFullName(row) || '')} - + render: (text) => ( + {text} ), }, !supportExtendedImageInfo && { - title: t('environment.Language'), - key: 'lang', - dataIndex: 'lang', + title: t('environment.Base'), + key: 'baseimage', + dataIndex: 'baseimage', sorter: (a, b) => - localeCompare(getLang(a.name ?? ''), getLang(b.name ?? '')), + localeCompare( + getBaseImage(getImageFullName(a) || ''), + getBaseImage(getImageFullName(b) || ''), + ), render: (text, row) => ( - + + {tagAlias(getBaseImage(getImageFullName(row) || ''))} + ), }, !supportExtendedImageInfo && { @@ -289,48 +285,18 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { ), }, !supportExtendedImageInfo && { - title: t('environment.Base'), - key: 'baseimage', - dataIndex: 'baseimage', - sorter: (a, b) => - localeCompare( - getBaseImage(getImageFullName(a) || ''), - getBaseImage(getImageFullName(b) || ''), - ), + title: t('environment.Tags'), + key: 'tag', + dataIndex: 'tag', + sorter: (a, b) => localeCompare(a?.tag, b?.tag), render: (text, row) => ( - + } + highlightKeyword={imageSearch} + /> ), }, - !supportExtendedImageInfo && { - title: t('environment.Constraint'), - key: 'constraint', - dataIndex: 'constraint', - sorter: (a, b) => { - const requirementA = - a?.tag && b?.labels - ? getConstraints( - a?.tag, - a?.labels as { key: string; value: string }[], - )[0] || '' - : ''; - const requirementB = - b?.tag && b?.labels - ? getConstraints( - b?.tag, - b?.labels as { key: string; value: string }[], - )[0] || '' - : ''; - return localeCompare(requirementA, requirementB); - }, - render: (text, row) => - row?.tag ? ( - } - highlightKeyword={imageSearch} - /> - ) : null, - }, { title: t('environment.Digest'), dataIndex: 'digest', @@ -417,17 +383,14 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { fullName: getImageFullName(image) || '', digest: image?.digest || '', // ------------ need only before 24.12.0 ------------ - lang: image?.name ? getLang(image.name) : '', baseversion: getBaseVersion(getImageFullName(image) || ''), baseimage: image?.tag && image?.name ? getBaseImages(image.tag, image.name) : [], - constraints: - image?.tag && image?.labels - ? getConstraints( - image.tag, - image.labels as { key: string; value: string }[], - ) - : [], + tag: + getTags( + image?.tag || '', + image?.labels as Array<{ key: string; value: string }>, + ) || [], isCustomized: image?.tag ? image.tag.indexOf('customized') !== -1 : false, @@ -455,14 +418,13 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { const baseImagesMatch = _.some(curFilterValues.baseimage, (value) => regExp.test(value), ); - const constraintsMatch = _.some( - curFilterValues.constraints, - (constraint) => regExp.test(constraint), + const tagMatch = _.some( + curFilterValues.tag, + (tag) => regExp.test(tag.key) || regExp.test(tag.value), ); const customizedMatch = curFilterValues.isCustomized ? regExp.test('customized') : false; - const langMatch = regExp.test(curFilterValues.lang); const namespaceMatch = regExp.test(curFilterValues.namespace || ''); const fullNameMatch = regExp.test(curFilterValues.fullName); const tagsMatch = _.some( @@ -475,8 +437,7 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { return ( baseVersionMatch || baseImagesMatch || - constraintsMatch || - langMatch || + tagMatch || namespaceMatch || customizedMatch || fullNameMatch || diff --git a/react/src/components/ImageTags.tsx b/react/src/components/ImageTags.tsx index 91d0d47e74..2c3501d61e 100644 --- a/react/src/components/ImageTags.tsx +++ b/react/src/components/ImageTags.tsx @@ -161,3 +161,49 @@ const SessionKernelTags: React.FC<{ }; export default React.memo(SessionKernelTags); + +interface ImageTagsProps extends TagProps { + tag: string; + labels: Array<{ key: string; value: string }>; + highlightKeyword?: string; +} +export const ImageTags: React.FC = ({ + tag, + labels, + highlightKeyword, + ...props +}) => { + labels = labels || []; + const [, { getTags, tagAlias }] = useBackendAIImageMetaData(); + const tags = getTags(tag, labels); + return ( + + {_.map(tags, (tag: { key: string; value: string }, index) => { + const isCustomized = tag.key === 'Customized'; + return ( + + {tagAlias(tag.key)} + + ), + color: isCustomized ? 'cyan' : 'blue', + }, + { + label: ( + + {tag.value} + + ), + color: isCustomized ? 'cyan' : 'blue', + }, + ]} + /> + ); + })} + + ); +}; diff --git a/react/src/hooks/index.tsx b/react/src/hooks/index.tsx index a934fd2e7d..26534fdb76 100644 --- a/react/src/hooks/index.tsx +++ b/react/src/hooks/index.tsx @@ -242,8 +242,8 @@ export const useBackendAIImageMetaData = () => { } return metadata?.tagAlias[lang] || lang; }, - getImageTags: (imageName: string) => { - // const { key, tags } = getImageMeta(imageName); + getImageTagStr: (imageName: string) => { + return _.last(_.split(_.split(imageName, '@')[0], ':')); }, getFilteredRequirementsTags: (imageName: string) => { const { tags } = getImageMeta(imageName); @@ -261,12 +261,16 @@ export const useBackendAIImageMetaData = () => { return customizedNameLabel; }, getBaseVersion: (imageName: string) => { - const { tags } = getImageMeta(imageName); - return tags[0]; + return ( + _.first(_.split(_.last(_.split(imageName, ':')), /[^a-zA-Z\d.]+/)) || + '' + ); }, getBaseImage: (imageName: string) => { - const { tags } = getImageMeta(imageName); - return tags[1]; + const splitByColon = _.split(imageName, ':'); + const beforeLastColon = _.join(_.initial(splitByColon), ':'); + const lastItemAfterSplitBySlash = _.last(_.split(beforeLastColon, '/')); + return lastItemAfterSplitBySlash || ''; }, getBaseImages: (tag: string, name: string) => { const tags = tag.split('-'); @@ -292,6 +296,41 @@ export const useBackendAIImageMetaData = () => { return baseImageArr; }, getImageMeta, + getTags: (tag: string, labels: Array<{ key: string; value: string }>) => { + // Remove the 'customized_' prefix and its following string from the tag + const cleanedTag = tag.replace(/customized_[a-zA-Z\d.]+/, ''); + // Split the remaining tag into segments based on alphanumeric and '.' characters, ignoring the first segment + const tags = _.tail(_.split(cleanedTag, /[^a-zA-Z\d.]+/)); + const result: Array<{ key: string; value: string }> = []; + + // Process not 'customized_' tags + _.forEach(tags, (currentTag) => { + // Separate the alphabetic prefix from the numeric and '.' suffix for each tag + const match = /^([a-zA-Z]+)(.*)$/.exec(currentTag); + if (match) { + const [, key, value] = match; + // Ensure the value is an empty string if it's undefined + result.push({ key, value: value || '' }); + } + }); + + // Handle the 'customized_' tag separately by finding the custom image name in labels + const customizedNameLabel = _.get( + _.find(labels, { key: 'ai.backend.customized-image.name' }), + 'value', + '', + ); + // If a custom image name exists, add it to the result with the key 'Customized' + if (customizedNameLabel) { + result.push({ key: 'Customized', value: customizedNameLabel }); + } + + // Remove duplicates and entries with an empty 'key' + return _.uniqWith( + _.filter(result, ({ key }) => !_.isEmpty(key)), + _.isEqual, + ); + }, getConstraints: ( tag: string, labels: { key: string; value: string }[],