From b0d7ea27566689e0babf18846ae7390829167cfe Mon Sep 17 00:00:00 2001 From: agatha197 <28584164+agatha197@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:53:30 +0000 Subject: [PATCH] feat: new image parsing on Environment page (#2785) related PR: https://github.com/lablup/backend.ai/pull/2939 # Extended Image Information Support This PR adds support for extended image information in the ImageList component and related translations. It introduces new fields and modifies the display of image details when the backend supports the 'extended-image-info' feature. **Checklist:** - [ ] Minimum required manager version: 24.09.1 ## Changes 1. Updated `ImageListQuery` to include new fields: `namespace`, `base_image_name`, `tags`, and `version`. 2. Modified the ImageList component to conditionally render columns based on the `supportExtendedImageInfo` flag. 3. Added new translations for "Base image name" and "Tags" in multiple languages. 4. Updated the backend client to support the 'extended-image-info' feature for manager version 24.09.1 and above. ## Impact - Users will see more detailed image information when using a compatible backend version. - The image list will display new columns for base image name and tags when supported. - Existing functionality is preserved for older backend versions. ## Review Notes - Verify that the image list displays correctly with both old and new backend versions. - Check that new translations are applied correctly in different languages. - Ensure that sorting and filtering work properly with the new fields. ## How to test 1. Checkout core branch to `topic/10-22-feature_add_info_field_to_gql_image_schema` https://github.com/lablup/backend.ai/pull/2993 2. Go to Environment page. ## What to check: - [ ] Check the data is valid. - [ ] Since 24.09.1: Full image path, Registry, Architecture, Namespace, Base image name, Version, Tags, Digest, Resource limit, Control. - [ ] Before 24.09.1: Full image path, Registry, Architecture, Name, Language, Version, Base, Constraints, Digest, Resource limit, Control. - [ ] All data is highlightable by searching images. - [ ] Sortable data works fine. ## Screenshots ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/88e5ad80-559a-4b41-a93a-5a08f3a4c062.png) --- .cspell.json | 1 + react/data/schema.graphql | 411 +++++++++++++++++++++++------ react/src/components/DoubleTag.tsx | 4 +- react/src/components/ImageList.tsx | 357 +++++++++++++++---------- react/src/components/ImageTags.tsx | 13 +- react/src/hooks/index.tsx | 27 +- resources/i18n/de.json | 5 +- resources/i18n/el.json | 5 +- resources/i18n/en.json | 7 +- resources/i18n/es.json | 5 +- resources/i18n/fi.json | 7 +- resources/i18n/fr.json | 5 +- resources/i18n/id.json | 7 +- resources/i18n/it.json | 7 +- resources/i18n/ja.json | 5 +- resources/i18n/ko.json | 5 +- resources/i18n/mn.json | 7 +- resources/i18n/ms.json | 7 +- resources/i18n/pl.json | 5 +- resources/i18n/pt-BR.json | 5 +- resources/i18n/pt.json | 5 +- resources/i18n/ru.json | 5 +- resources/i18n/th.json | 5 +- resources/i18n/tr.json | 7 +- resources/i18n/vi.json | 5 +- resources/i18n/zh-CN.json | 5 +- resources/i18n/zh-TW.json | 5 +- resources/image_metadata.json | 4 +- src/lib/backend.ai-client-esm.ts | 3 + 29 files changed, 674 insertions(+), 265 deletions(-) diff --git a/.cspell.json b/.cspell.json index b74cb62ce4..4bc1b8430f 100644 --- a/.cspell.json +++ b/.cspell.json @@ -5,6 +5,7 @@ "Backendai", "backendaiclient", "backendaioptions", + "baseversion", "cssinjs", "cuda", "FGPU", diff --git a/react/data/schema.graphql b/react/data/schema.graphql index 1ef2b25498..ba1c42ef66 100644 --- a/react/data/schema.graphql +++ b/react/data/schema.graphql @@ -14,6 +14,28 @@ type Queries { agents(scaling_group: String, status: String): [Agent] agent_summary(agent_id: String!): AgentSummary agent_summary_list(limit: Int!, offset: Int!, filter: String, order: String, scaling_group: String, status: String): AgentSummaryList + + """Added in 24.12.0.""" + domain_node(id: GlobalIDField!, permission: DomainPermissionValueField = "read_attribute"): DomainNode + + """Added in 24.12.0.""" + domain_nodes(filter: String, order: String, permission: DomainPermissionValueField = "read_attribute", offset: Int, before: String, after: String, first: Int, last: Int): DomainConnection + + """Added in 24.12.0.""" + agent_nodes( + """Added in 24.12.0. Default is `system`.""" + scope: ScopeField + + """Added in 24.12.0. Default is create_compute_session.""" + permission: AgentPermissionField = "create_compute_session" + filter: String + order: String + offset: Int + before: String + after: String + first: Int + last: Int + ): AgentConnection domain(name: String): Domain domains(is_active: Boolean): [Domain] @@ -298,12 +320,24 @@ type ImageNode implements Node { """Added in 24.03.4. The undecoded id value stored in DB.""" row_id: UUID - name: String + name: String @deprecated(reason: "Deprecated since 24.09.1. use `namespace` instead") + + """Added in 24.09.1.""" + namespace: String + + """Added in 24.09.1.""" + base_image_name: String """Added in 24.03.10.""" project: String humanized_name: String tag: String + + """Added in 24.09.1.""" + tags: [KVPair] + + """Added in 24.09.1.""" + version: String registry: String architecture: String is_local: Boolean @@ -360,6 +394,231 @@ type AgentSummaryList implements PaginatedList { total_count: Int! } +"""Added in 24.12.0.""" +type DomainNode implements Node { + """The ID of the object""" + id: ID! + name: String + description: String + is_active: Boolean + created_at: DateTime + modified_at: DateTime + total_resource_slots: JSONString + allowed_vfolder_hosts: JSONString + allowed_docker_registries: [String] + dotfiles: Bytes + integration_id: String + scaling_groups(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): ScalinGroupConnection +} + +"""Added in 24.09.1.""" +scalar Bytes + +"""Added in 24.12.0.""" +type ScalinGroupConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [ScalinGroupEdge]! + + """Total count of the GQL nodes of the query.""" + count: Int +} + +""" +The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. +""" +type PageInfo { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: String + + """When paginating forwards, the cursor to continue.""" + endCursor: String +} + +""" +Added in 24.12.0. A Relay edge containing a `ScalinGroup` and its cursor. +""" +type ScalinGroupEdge { + """The item at the end of the edge""" + node: ScalingGroupNode + + """A cursor for use in pagination""" + cursor: String! +} + +"""Added in 24.12.0.""" +type ScalingGroupNode implements Node { + """The ID of the object""" + id: ID! + name: String + description: String + is_active: Boolean + is_public: Boolean + created_at: DateTime + wsproxy_addr: String + wsproxy_api_token: String + driver: String + driver_opts: JSONString + scheduler: String + scheduler_opts: JSONString + use_host_network: Boolean +} + +""" +Added in 24.09.0. Global ID of GQL relay spec. Base64 encoded version of ":". UUID or string type values are also allowed. +""" +scalar GlobalIDField + +""" +Added in 24.12.0. One of ['read_attribute', 'read_sensitive_attribute', 'update_attribute', 'create_user', 'create_project']. +""" +scalar DomainPermissionValueField + +"""Added in 24.12.0""" +type DomainConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [DomainEdge]! + + """Total count of the GQL nodes of the query.""" + count: Int +} + +"""Added in 24.12.0 A Relay edge containing a `Domain` and its cursor.""" +type DomainEdge { + """The item at the end of the edge""" + node: DomainNode + + """A cursor for use in pagination""" + cursor: String! +} + +"""Added in 24.12.0.""" +type AgentConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [AgentEdge]! + + """Total count of the GQL nodes of the query.""" + count: Int +} + +"""Added in 24.12.0. A Relay edge containing a `Agent` and its cursor.""" +type AgentEdge { + """The item at the end of the edge""" + node: AgentNode + + """A cursor for use in pagination""" + cursor: String! +} + +"""Added in 24.12.0.""" +type AgentNode implements Node { + """The ID of the object""" + id: ID! + row_id: String + status: String + status_changed: DateTime + region: String + scaling_group: String + schedulable: Boolean + available_slots: JSONString + occupied_slots: JSONString + + """Agent's address with port. (bind/advertised host:port)""" + addr: String + architecture: String + first_contact: DateTime + lost_at: DateTime + live_stat: JSONString + version: String + compute_plugins: JSONString + hardware_metadata: JSONString + auto_terminate_abusing_kernel: Boolean + local_config: JSONString + container_count: Int + kernel_nodes(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): KernelConnection + + """ + Added in 24.12.0. One of ['read_attribute', 'update_attribute', 'create_compute_session', 'create_service']. + """ + permissions: [AgentPermissionField] +} + +"""Added in 24.09.0.""" +type KernelConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [KernelEdge]! + + """Total count of the GQL nodes of the query.""" + count: Int +} + +"""Added in 24.09.0. A Relay edge containing a `Kernel` and its cursor.""" +type KernelEdge { + """The item at the end of the edge""" + node: KernelNode + + """A cursor for use in pagination""" + cursor: String! +} + +"""Added in 24.09.0.""" +type KernelNode implements Node { + """The ID of the object""" + id: ID! + + """ID of kernel.""" + row_id: UUID + cluster_idx: Int + local_rank: Int + cluster_role: String + cluster_hostname: String + session_id: UUID + image: ImageNode + status: String + status_changed: DateTime + status_info: String + status_data: JSONString + created_at: DateTime + terminated_at: DateTime + starts_at: DateTime + scheduled_at: DateTime + agent_id: String + agent_addr: String + container_id: String + resource_opts: JSONString + occupied_slots: JSONString + live_stat: JSONString + abusing_report: JSONString + preopen_ports: [Int] +} + +""" +Added in 24.12.0. One of ['read_attribute', 'update_attribute', 'create_compute_session', 'create_service']. +""" +scalar AgentPermissionField + +""" +Added in 24.12.0. A string value in the format ':'. should be one of [system, domain, project, user]. should be the ID value of the scope. e.g. `domain:default`, `user:123e4567-e89b-12d3-a456-426614174000`. +""" +scalar ScopeField + type Domain { name: String description: String @@ -411,23 +670,6 @@ type UserConnection { count: Int } -""" -The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. -""" -type PageInfo { - """When paginating forwards, are there more items?""" - hasNextPage: Boolean! - - """When paginating backwards, are there more items?""" - hasPreviousPage: Boolean! - - """When paginating backwards, the cursor to continue.""" - startCursor: String - - """When paginating forwards, the cursor to continue.""" - endCursor: String -} - """Added in 24.03.0 A Relay edge containing a `User` and its cursor.""" type UserEdge { """The item at the end of the edge""" @@ -515,12 +757,24 @@ type Group { type Image { id: UUID - name: String + name: String @deprecated(reason: "Deprecated since 24.09.1. use `namespace` instead") + + """Added in 24.09.1.""" + namespace: String + + """Added in 24.09.1.""" + base_image_name: String """Added in 24.03.10.""" project: String humanized_name: String tag: String + + """Added in 24.09.1.""" + tags: [KVPair] + + """Added in 24.09.1.""" + version: String registry: String architecture: String is_local: Boolean @@ -966,58 +1220,6 @@ Added in 24.09.0. One of ['read_attribute', 'update_attribute', 'delete_session' """ scalar SessionPermissionValueField -"""Added in 24.09.0.""" -type KernelConnection { - """Pagination data for this connection.""" - pageInfo: PageInfo! - - """Contains the nodes in this connection.""" - edges: [KernelEdge]! - - """Total count of the GQL nodes of the query.""" - count: Int -} - -"""Added in 24.09.0. A Relay edge containing a `Kernel` and its cursor.""" -type KernelEdge { - """The item at the end of the edge""" - node: KernelNode - - """A cursor for use in pagination""" - cursor: String! -} - -"""Added in 24.09.0.""" -type KernelNode implements Node { - """The ID of the object""" - id: ID! - - """ID of kernel.""" - row_id: UUID - cluster_idx: Int - local_rank: Int - cluster_role: String - cluster_hostname: String - session_id: UUID - image: ImageNode - status: String - status_changed: DateTime - status_info: String - status_data: JSONString - created_at: DateTime - terminated_at: DateTime - starts_at: DateTime - scheduled_at: DateTime - agent_id: String - agent_addr: String - container_id: String - resource_opts: JSONString - occupied_slots: JSONString - live_stat: JSONString - abusing_report: JSONString - preopen_ports: [Int] -} - """Added in 24.09.0.""" type ComputeSessionConnection { """Pagination data for this connection.""" @@ -1041,11 +1243,6 @@ type ComputeSessionEdge { cursor: String! } -""" -Added in 24.09.0. Global ID of GQL relay spec. Base64 encoded version of ":". UUID or string type values are also allowed. -""" -scalar GlobalIDField - type ComputeSessionList implements PaginatedList { items: [ComputeSession]! total_count: Int! @@ -1402,6 +1599,12 @@ type Mutations { To purge domain, there should be no users and groups in the target domain. """ purge_domain(name: String!): PurgeDomain + + """Added in 24.12.0.""" + create_domain_node(input: CreateDomainNodeInput!): CreateDomainNode + + """Added in 24.12.0.""" + modify_domain_node(input: ModifyDomainNodeInput!): ModifyDomainNode create_group(name: String!, props: GroupInput!): CreateGroup modify_group(gid: UUID!, props: ModifyGroupInput!): ModifyGroup @@ -1575,6 +1778,9 @@ type Mutations { modify_container_registry(hostname: String!, props: ModifyContainerRegistryInput!): ModifyContainerRegistry delete_container_registry(hostname: String!): DeleteContainerRegistry modify_endpoint(endpoint_id: UUID!, props: ModifyEndpointInput!): ModifyEndpoint + + """Added in 24.09.0.""" + check_and_transit_session_status(input: CheckAndTransitStatusInput!): CheckAndTransitStatus } type ModifyAgent { @@ -1635,6 +1841,47 @@ type PurgeDomain { msg: String } +"""Added in 24.12.0.""" +type CreateDomainNode { + ok: Boolean + msg: String + item: DomainNode +} + +"""Added in 24.12.0.""" +input CreateDomainNodeInput { + name: String! + description: String + is_active: Boolean = true + total_resource_slots: JSONString = "{}" + allowed_vfolder_hosts: JSONString = "{}" + allowed_docker_registries: [String] = [] + integration_id: String = null + dotfiles: Bytes = "90" + scaling_groups: [String] +} + +"""Added in 24.12.0.""" +type ModifyDomainNode { + item: DomainNode + client_mutation_id: String +} + +"""Added in 24.12.0.""" +input ModifyDomainNodeInput { + id: GlobalIDField! + description: String + is_active: Boolean + total_resource_slots: JSONString + allowed_vfolder_hosts: JSONString + allowed_docker_registries: [String] + integration_id: String + dotfiles: Bytes + sgroups_to_add: [String] + sgroups_to_remove: [String] + client_mutation_id: String +} + type CreateGroup { ok: Boolean msg: String @@ -2333,4 +2580,16 @@ input ExtraMountInput { Added in 24.03.4. Set permission of this mount. Should be one of (ro,rw,wd). Default is null """ permission: String +} + +"""Added in 24.12.0""" +type CheckAndTransitStatus { + item: [ComputeSessionNode] + client_mutation_id: String +} + +"""Added in 24.12.0.""" +input CheckAndTransitStatusInput { + ids: [GlobalIDField]! + client_mutation_id: String } \ No newline at end of file diff --git a/react/src/components/DoubleTag.tsx b/react/src/components/DoubleTag.tsx index 17725b0db3..289d145dd3 100644 --- a/react/src/components/DoubleTag.tsx +++ b/react/src/components/DoubleTag.tsx @@ -32,7 +32,7 @@ const DoubleTag: React.FC<{ return ( {_.map(objectValues, (objValue, idx) => { - return ( + return !_.isEmpty(objValue.label) ? ( {objValue.label} - ); + ) : null; })} ); diff --git a/react/src/components/ImageList.tsx b/react/src/components/ImageList.tsx index 53a556f165..61d455d2f1 100644 --- a/react/src/components/ImageList.tsx +++ b/react/src/components/ImageList.tsx @@ -1,8 +1,13 @@ import Flex from '../components/Flex'; -import { filterNonNullItems, getImageFullName } from '../helper'; -import { useBackendAIImageMetaData, useUpdatableState } from '../hooks'; +import { filterNonNullItems, getImageFullName, localeCompare } from '../helper'; +import { + useBackendAIImageMetaData, + useSuspendedBackendaiClient, + useUpdatableState, +} from '../hooks'; +import DoubleTag from './DoubleTag'; import ImageInstallModal from './ImageInstallModal'; -import { ConstraintTags } from './ImageTags'; +import { BaseImageTags, ConstraintTags, LangTags } from './ImageTags'; import ManageAppsModal from './ManageAppsModal'; import ManageImageResourceLimitModal from './ManageImageResourceLimitModal'; import ResourceNumber from './ResourceNumber'; @@ -11,10 +16,8 @@ import { ImageListQuery, ImageListQuery$data, } from './__generated__/ImageListQuery.graphql'; -import CopyButton from './lablupTalkativotUI/CopyButton'; import { AppstoreOutlined, - CopyOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, @@ -37,7 +40,15 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { const [selectedRows, setSelectedRows] = useState([]); const [ , - { getNamespace, getBaseVersion, getLang, getBaseImages, getConstraints }, + { + getNamespace, + getBaseVersion, + getLang, + getBaseImages, + getConstraints, + getBaseImage, + tagAlias, + }, ] = useBackendAIImageMetaData(); const { token } = theme.useToken(); const [managingApp, setManagingApp] = useState(null); @@ -52,13 +63,15 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { const [imageSearch, setImageSearch] = useState(''); const [isPendingRefreshTransition, startRefreshTransition] = useTransition(); const [isPendingSearchTransition, startSearchTransition] = useTransition(); + const baiClient = useSuspendedBackendaiClient(); + const supportExtendedImageInfo = baiClient?.supports('extended-image-info'); const { images } = useLazyLoadQuery( graphql` query ImageListQuery { images { id - name + name @deprecatedSince(version: "24.09.1") tag registry architecture @@ -74,6 +87,13 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { min max } + 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") } } `, @@ -118,146 +138,213 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { ) : null, }, + { + title: t('environment.FullImagePath'), + key: 'fullImagePath', + render: (row) => ( + + + {getImageFullName(row) || ''} + + + ), + sorter: (a, b) => localeCompare(getImageFullName(a), getImageFullName(b)), + }, { title: t('environment.Registry'), dataIndex: 'registry', key: 'registry', - sorter: (a, b) => - a?.registry && b?.registry ? a.registry.localeCompare(b.registry) : 0, - render: (text, row) => ( - {row.registry} + sorter: (a, b) => localeCompare(a?.registry, b?.registry), + render: (text) => ( + {text} ), }, { title: t('environment.Architecture'), dataIndex: 'architecture', key: 'architecture', - sorter: (a, b) => - a?.architecture && b?.architecture - ? a.architecture.localeCompare(b.architecture) - : 0, - render: (text, row) => ( - - {row.architecture} - - ), - }, - { - title: t('environment.Namespace'), - key: 'namespace', - dataIndex: 'namespace', - sorter: (a, b) => { - const namespaceA = getNamespace(getImageFullName(a) || ''); - const namespaceB = getNamespace(getImageFullName(b) || ''); - return namespaceA && namespaceB - ? namespaceA.localeCompare(namespaceB) - : 0; - }, - render: (text, row) => ( - - {getNamespace(getImageFullName(row) || '')} - - ), - }, - { - title: t('environment.Language'), - key: 'lang', - dataIndex: 'lang', - sorter: (a, b) => { - const langA = a?.name ? getLang(a?.name) : ''; - const langB = b?.name ? getLang(b?.name) : ''; - return langA && langB ? langA.localeCompare(langB) : 0; - }, - render: (text, row) => ( - - {row.name ? getLang(row.name) : null} - + sorter: (a, b) => localeCompare(a?.architecture, b?.architecture), + render: (text) => ( + {text} ), }, - { - title: t('environment.Version'), - key: 'baseversion', - dataIndex: 'baseversion', - sorter: (a, b) => { - const baseversionA = getBaseVersion(getImageFullName(a) || ''); - const baseversionB = getBaseVersion(getImageFullName(b) || ''); - return baseversionA && baseversionB - ? baseversionA.localeCompare(baseversionB) - : 0; - }, - render: (text, row) => ( - - {getBaseVersion(getImageFullName(row) || '')} - - ), - }, - { - title: t('environment.Base'), - key: 'baseimage', - dataIndex: 'baseimage', - sorter: (a, b) => { - const baseimageA = - !a?.tag || !a?.name ? '' : getBaseImages(a?.tag, a?.name)[0] || ''; - const baseimageB = - !b?.tag || !b?.name ? '' : getBaseImages(b?.tag, b?.name)[0] || ''; - if (baseimageA === '' && baseimageB === '') return 0; - if (baseimageA === '') return -1; - if (baseimageB === '') return 1; - return baseimageA.localeCompare(baseimageB); - }, - render: (text, row) => ( - - {row?.tag && row?.name - ? getBaseImages(row.tag, row.name).map((baseImage) => ( - - - {baseImage} - - - )) - : null} - - ), - }, - { - 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] || '' - : ''; - if (requirementA === '' && requirementB === '') return 0; - if (requirementA === '') return -1; - if (requirementB === '') return 1; - return requirementA.localeCompare(requirementB); - }, - render: (text, row) => - row?.tag ? ( - - ) : null, - }, + ...(supportExtendedImageInfo + ? [ + { + title: t('environment.Namespace'), + key: 'namespace', + dataIndex: 'namespace', + sorter: (a: EnvironmentImage, b: EnvironmentImage) => + localeCompare(a?.namespace, b?.namespace), + render: (text: string) => ( + {text} + ), + }, + { + title: t('environment.BaseImageName'), + key: 'base_image_name', + dataIndex: 'base_image_name', + sorter: (a: EnvironmentImage, b: EnvironmentImage) => + localeCompare(a?.base_image_name, b?.base_image_name), + render: (text: string, row: EnvironmentImage) => ( + + {tagAlias(text)} + + ), + }, + { + title: t('environment.Version'), + key: 'version', + dataIndex: 'version', + sorter: (a: EnvironmentImage, b: EnvironmentImage) => + localeCompare(a?.version, b?.version), + render: (text: string) => ( + {text} + ), + }, + { + title: t('environment.Tags'), + key: 'tags', + dataIndex: 'tags', + render: ( + text: Array<{ key: string; value: string }>, + row: EnvironmentImage, + ) => { + return ( + + {/* TODO: replace this with AliasedImageDoubleTags after image list query with ImageNode is implemented. */} + {_.map(text, (tag: { key: string; value: string }) => { + const isCustomized = _.includes(tag.key, 'customized_'); + const tagValue = isCustomized + ? _.find(row?.labels, { + key: 'ai.backend.customized-image.name', + })?.value + : tag.value; + return ( + + {tagAlias(tag.key)} + + ), + color: isCustomized ? 'cyan' : 'blue', + }, + { + label: ( + + {tagValue} + + ), + color: isCustomized ? 'cyan' : 'blue', + }, + ]} + /> + ); + })} + + ); + }, + }, + ] + : [ + { + title: t('environment.Namespace'), + key: 'name', + dataIndex: 'name', + sorter: (a: EnvironmentImage, b: EnvironmentImage) => + localeCompare(getImageFullName(a), getImageFullName(b)), + render: (text: string, row: EnvironmentImage) => ( + + {getNamespace(getImageFullName(row) || '')} + + ), + }, + { + title: t('environment.Language'), + key: 'lang', + dataIndex: 'lang', + sorter: (a: EnvironmentImage, b: EnvironmentImage) => + localeCompare(getLang(a.name ?? ''), getLang(b.name ?? '')), + render: (text: string, row: EnvironmentImage) => ( + + ), + }, + { + title: t('environment.Version'), + key: 'baseversion', + dataIndex: 'baseversion', + sorter: (a: EnvironmentImage, b: EnvironmentImage) => + localeCompare( + getBaseVersion(getImageFullName(a) || ''), + getBaseVersion(getImageFullName(b) || ''), + ), + render: (text: string, row: EnvironmentImage) => ( + + {getBaseVersion(getImageFullName(row) || '')} + + ), + }, + { + title: t('environment.Base'), + key: 'baseimage', + dataIndex: 'baseimage', + sorter: (a: EnvironmentImage, b: EnvironmentImage) => + localeCompare( + getBaseImage(getImageFullName(a) || ''), + getBaseImage(getImageFullName(b) || ''), + ), + render: (text: string, row: EnvironmentImage) => ( + + ), + }, + { + title: t('environment.Constraint'), + key: 'constraint', + dataIndex: 'constraint', + sorter: (a: EnvironmentImage, b: EnvironmentImage) => { + 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: string, row: EnvironmentImage) => + row?.tag ? ( + } + /> + ) : null, + }, + ]), { title: t('environment.Digest'), dataIndex: 'digest', key: 'digest', - sorter: (a, b) => - a?.digest && b?.digest ? a.digest.localeCompare(b.digest) : 0, + sorter: (a, b) => localeCompare(a?.digest || '', b?.digest || ''), render: (text, row) => ( {row.digest} ), @@ -294,14 +381,6 @@ const ImageList: React.FC<{ style?: React.CSSProperties }> = ({ style }) => { e.stopPropagation(); }} > - } - style={{ color: token.colorPrimary }} - copyable={{ - text: getImageFullName(row) || '', - }} - >