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) || '', - }} - >