From a84073a1b02f2d310bbbece4c67fbc7b4cd4b24c Mon Sep 17 00:00:00 2001 From: ironAiken2 <51399982+ironAiken2@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:52:50 +0000 Subject: [PATCH] feat(FR-17): update `desired_session_count` field name to `replicas` (#2963) ### This PR resolves [#2957](https://github.com/lablup/backend.ai-webui/issues/2957) issue Since Backend.AI Core version 24.12.0, the desired_session_count field has been replaced with `replicas`. **Changes:** - Added `replicas` field to service creation and update interfaces - Deprecated `desired_session_count` field for manager version >= 24.12.0 - Updated GraphQL queries to handle both `desired_session_count` and `replicas` fields - Added version-based field selection using `@deprecatedSince` and `@since` directives - Added `replicas` feature flag check in client capabilities **How to test:** - Check whether `desired_session_count` or `replicas` is used when creating or modifying a service, depending on the manager version. (From 24.12.0 onwards, replicas is used.) - Check whether desired_session_count is displayed correctly on the Service page and Detail page. --- .cspell.json | 3 +- .../components/ServiceLauncherPageContent.tsx | 30 +-- .../src/components/ServiceValidationView.tsx | 6 +- react/src/pages/EndpointDetailPage.tsx | 49 +++-- react/src/pages/EndpointListPage.tsx | 172 ++++++++++-------- resources/i18n/de.json | 4 +- resources/i18n/el.json | 4 +- resources/i18n/en.json | 4 +- resources/i18n/es.json | 4 +- resources/i18n/fi.json | 4 +- resources/i18n/fr.json | 4 +- resources/i18n/id.json | 4 +- resources/i18n/it.json | 4 +- resources/i18n/ja.json | 4 +- resources/i18n/ko.json | 4 +- resources/i18n/mn.json | 4 +- resources/i18n/ms.json | 4 +- resources/i18n/pl.json | 4 +- resources/i18n/pt-BR.json | 4 +- resources/i18n/pt.json | 4 +- resources/i18n/ru.json | 4 +- resources/i18n/th.json | 4 +- resources/i18n/tr.json | 4 +- resources/i18n/vi.json | 4 +- resources/i18n/zh-CN.json | 4 +- src/lib/backend.ai-client-esm.ts | 1 + 26 files changed, 182 insertions(+), 159 deletions(-) diff --git a/.cspell.json b/.cspell.json index 9326134678..a158350f86 100644 --- a/.cspell.json +++ b/.cspell.json @@ -36,7 +36,8 @@ "Keypairs", "Hyperaccel", "Tensorboard", - "Automount" + "Automount", + "Talkativot" ], "flagWords": [ "데이터레이크", diff --git a/react/src/components/ServiceLauncherPageContent.tsx b/react/src/components/ServiceLauncherPageContent.tsx index 12b36332f7..2089fddd2b 100644 --- a/react/src/components/ServiceLauncherPageContent.tsx +++ b/react/src/components/ServiceLauncherPageContent.tsx @@ -88,7 +88,8 @@ interface ServiceCreateConfigType { } export interface ServiceCreateType { name: string; - desired_session_count: number; + desired_session_count?: number; + replicas?: number; image: string; runtime_variant: string; architecture: string; @@ -106,7 +107,7 @@ export interface ServiceCreateType { interface ServiceLauncherInput extends ImageEnvironmentFormInput { serviceName: string; vFolderID: string; - desiredRoutingCount: number; + replicas: number; openToPublic: boolean; modelMountDestination: string; modelDefinitionPath: string; @@ -153,7 +154,8 @@ const ServiceLauncherPageContent: React.FC = ({ graphql` fragment ServiceLauncherPageContentFragment on Endpoint { endpoint_id - desired_session_count + desired_session_count @deprecatedSince(version: "24.12.0") + replicas @since(version: "24.12.0") resource_group resource_slots resource_opts @@ -264,7 +266,7 @@ const ServiceLauncherPageContent: React.FC = ({ const legacyMutationToUpdateService = useTanMutation({ mutationFn: (values: ServiceLauncherFormValue) => { const body = { - to: values.desiredRoutingCount, + to: values.replicas, }; return baiSignedRequestWithPromise({ method: 'POST', @@ -289,7 +291,8 @@ const ServiceLauncherPageContent: React.FC = ({ } const body: ServiceCreateType = { name: values.serviceName, - desired_session_count: values.desiredRoutingCount, + // REST API does not support `replicas` field. To use `replicas` field, we need `create_endpoint` mutation. + desired_session_count: values.replicas, ...getImageInfoFromInputInCreating( checkManualImageAllowed( baiClient._config.allow_manual_image_name_for_session, @@ -407,7 +410,8 @@ const ServiceLauncherPageContent: React.FC = ({ msg endpoint { endpoint_id - desired_session_count + desired_session_count @deprecatedSince(version: "24.12.0") + replicas @since(version: "24.12.0") resource_group resource_slots resource_opts @@ -500,7 +504,11 @@ const ServiceLauncherPageContent: React.FC = ({ ? 'SINGLE_NODE' : 'MULTI_NODE', cluster_size: values.cluster_size, - desired_session_count: values.desiredRoutingCount, + ...(baiClient.supports('replicas') + ? { replicas: values.replicas } + : { + desired_session_count: values.replicas, + }), ...getImageInfoFromInputInEditing( checkManualImageAllowed( baiClient._config.allow_manual_image_name_for_session, @@ -643,7 +651,7 @@ const ServiceLauncherPageContent: React.FC = ({ serviceName: endpoint?.name, resourceGroup: endpoint?.resource_group, allocationPreset: 'custom', - desiredRoutingCount: endpoint?.desired_session_count ?? 1, + replicas: endpoint?.replicas ?? endpoint?.desired_session_count ?? 1, // FIXME: memory doesn't applied to resource allocation resource: { cpu: parseInt(JSON.parse(endpoint?.resource_slots || '{}')?.cpu), @@ -692,7 +700,7 @@ const ServiceLauncherPageContent: React.FC = ({ // TODO: set mounts alias map according to extra_mounts if possible } : { - desiredRoutingCount: 1, + replicas: 1, runtimeVariant: 'custom', ...RESOURCE_ALLOCATION_INITIAL_FORM_VALUES, ...(baiClient._config?.default_session_environment && { @@ -899,8 +907,8 @@ const ServiceLauncherPageContent: React.FC = ({ )} = ({ const image: string = `${values.environments.image?.registry}/${values.environments.image?.name}:${values.environments.image?.tag}`; const body: ServiceCreateType = { name: values.serviceName, - desired_session_count: values.desiredRoutingCount, + ...(baiClient.supports('replicas') + ? { replicas: values.replicas } + : { + desired_session_count: values.replicas, + }), image: image, runtime_variant: values.runtimeVariant, architecture: values.environments.image?.architecture as string, diff --git a/react/src/pages/EndpointDetailPage.tsx b/react/src/pages/EndpointDetailPage.tsx index c331fdf94c..05bdcbfaf6 100644 --- a/react/src/pages/EndpointDetailPage.tsx +++ b/react/src/pages/EndpointDetailPage.tsx @@ -18,6 +18,7 @@ import { useWebUINavigate, } from '../hooks'; import { useCurrentUserInfo } from '../hooks/backendai'; +import { useBAIPaginationOptionState } from '../hooks/reactPaginationQueryOptions'; import { useTanMutation } from '../hooks/reactQueryAlias'; import { isDestroyingStatus } from './EndpointListPage'; import { @@ -58,7 +59,7 @@ import { BotMessageSquareIcon } from 'lucide-react'; import React, { Suspense, useState, useTransition } from 'react'; import { useTranslation } from 'react-i18next'; import { useLazyLoadQuery } from 'react-relay'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; interface RoutingInfo { route_id: string; @@ -68,7 +69,8 @@ interface RoutingInfo { export interface ModelServiceInfo { endpoint_id: string; name: string; - desired_session_count: number; + desired_session_count?: number; + replicas?: number; active_routes: RoutingInfo[]; service_endpoint: string; is_public: boolean; @@ -91,12 +93,14 @@ const dayDiff = (a: any, b: any) => { const EndpointDetailPage: React.FC = () => { const { t } = useTranslation(); const { token } = theme.useToken(); - const baiClient = useSuspendedBackendaiClient(); - const navigate = useNavigate(); + const { message } = App.useApp(); + const { baiPaginationOption } = useBAIPaginationOptionState({ + current: 1, + pageSize: 100, + }); const { serviceId } = useParams<{ serviceId: string; }>(); - const [fetchKey, updateFetchKey] = useUpdatableState('initial-fetch'); const [isPendingRefetch, startRefetchTransition] = useTransition(); const [isPendingClearError, startClearErrorTransition] = useTransition(); @@ -107,14 +111,7 @@ const EndpointDetailPage: React.FC = () => { const [openChatModal, setOpenChatModal] = useState(false); const [currentUser] = useCurrentUserInfo(); // const curProject = useCurrentProjectValue(); - const [paginationState] = useState<{ - current: number; - pageSize: number; - }>({ - current: 1, - pageSize: 100, - }); - const { message } = App.useApp(); + const baiClient = useSuspendedBackendaiClient(); const webuiNavigate = useWebUINavigate(); const { open } = useFolderExplorerOpener(); const [selectedSessionId, setSelectedSessionId] = useState(); @@ -151,7 +148,8 @@ const EndpointDetailPage: React.FC = () => { size_bytes supported_accelerators } - desired_session_count + desired_session_count @deprecatedSince(version: "24.12.0") + replicas @since(version: "24.12.0") url open_to_public errors { @@ -207,9 +205,8 @@ const EndpointDetailPage: React.FC = () => { } `, { - tokenListOffset: - (paginationState.current - 1) * paginationState.pageSize, - tokenListLimit: paginationState.pageSize, + tokenListOffset: baiPaginationOption.offset, + tokenListLimit: baiPaginationOption.limit, endpointId: serviceId || '', }, { @@ -297,8 +294,8 @@ const EndpointDetailPage: React.FC = () => { children: , }, { - label: t('modelService.DesiredSessionCount'), - children: endpoint?.desired_session_count, + label: t('modelService.NumberOfReplicas'), + children: endpoint?.replicas ?? endpoint?.desired_session_count, }, { label: t('modelService.ServiceEndpoint'), @@ -443,7 +440,7 @@ const EndpointDetailPage: React.FC = () => { title: t('modelService.Services'), onClick: (e) => { e.preventDefault(); - navigate('/serving'); + webuiNavigate('/serving'); }, href: '/serving', }, @@ -481,7 +478,7 @@ const EndpointDetailPage: React.FC = () => { loading={isPendingRefetch} icon={} disabled={isDestroyingStatus( - endpoint?.desired_session_count, + endpoint?.replicas ?? endpoint?.desired_session_count, endpoint?.status, )} onClick={() => { @@ -502,7 +499,7 @@ const EndpointDetailPage: React.FC = () => { icon={} disabled={ isDestroyingStatus( - endpoint?.desired_session_count, + endpoint?.replicas ?? endpoint?.desired_session_count, endpoint?.status, ) || (!!endpoint?.created_user_email && @@ -532,7 +529,7 @@ const EndpointDetailPage: React.FC = () => { type="primary" icon={} disabled={isDestroyingStatus( - endpoint?.desired_session_count, + endpoint?.replicas ?? endpoint?.desired_session_count, endpoint?.status, )} onClick={() => { @@ -618,7 +615,7 @@ const EndpointDetailPage: React.FC = () => { icon={} loading={mutationToSyncRoutes.isPending} disabled={isDestroyingStatus( - endpoint?.desired_session_count, + endpoint?.replicas ?? endpoint?.desired_session_count, endpoint?.status, )} onClick={() => { @@ -674,8 +671,8 @@ const EndpointDetailPage: React.FC = () => { row.status && ( <> {row.status.toUpperCase()} diff --git a/react/src/pages/EndpointListPage.tsx b/react/src/pages/EndpointListPage.tsx index b2d8f847bc..8b8e9ffb40 100644 --- a/react/src/pages/EndpointListPage.tsx +++ b/react/src/pages/EndpointListPage.tsx @@ -3,13 +3,19 @@ import EndpointOwnerInfo from '../components/EndpointOwnerInfo'; import EndpointStatusTag from '../components/EndpointStatusTag'; import Flex from '../components/Flex'; import TableColumnsSettingModal from '../components/TableColumnsSettingModal'; -import { baiSignedRequestWithPromise, filterEmptyItem } from '../helper'; +import { + baiSignedRequestWithPromise, + filterEmptyItem, + filterNonNullItems, + transformSorterToOrderString, +} from '../helper'; import { useSuspendedBackendaiClient, useUpdatableState, useWebUINavigate, } from '../hooks'; import { useCurrentUserInfo, useCurrentUserRole } from '../hooks/backendai'; +import { useBAIPaginationOptionState } from '../hooks/reactPaginationQueryOptions'; // import { getSortOrderByName } from '../hooks/reactPaginationQueryOptions'; import { useTanMutation } from '../hooks/reactQueryAlias'; import { useCurrentProjectValue } from '../hooks/useCurrentProject'; @@ -28,7 +34,7 @@ import { } from '@ant-design/icons'; import { useRafInterval, useToggle } from 'ahooks'; import { Button, Table, Typography, theme, Radio, App } from 'antd'; -import { ColumnsType } from 'antd/es/table'; +import { ColumnType } from 'antd/lib/table'; import graphql from 'babel-plugin-relay/macro'; import { default as dayjs } from 'dayjs'; import _ from 'lodash'; @@ -37,7 +43,6 @@ import React, { useState, useTransition, startTransition as startTransitionWithoutPendingState, - useDeferredValue, } from 'react'; import { useTranslation } from 'react-i18next'; import { useLazyLoadQuery } from 'react-relay'; @@ -51,7 +56,6 @@ export type Endpoint = NonNullable< >['items'] >[0] >; - export const isDestroyingStatus = ( desiredSessionCount: number | null | undefined, status: string | null | undefined, @@ -66,37 +70,29 @@ type LifecycleStage = 'created&destroying' | 'destroyed'; const EndpointListPage: React.FC = ({ children }) => { const { t } = useTranslation(); - const { message, modal } = App.useApp(); - const baiClient = useSuspendedBackendaiClient(); - const webuiNavigate = useWebUINavigate(); const { token } = theme.useToken(); - const curProject = useCurrentProjectValue(); + const { message, modal } = App.useApp(); const [visibleColumnSettingModal, { toggle: toggleColumnSettingModal }] = useToggle(); const [selectedLifecycleStage, setSelectedLifecycleStage] = useState('created&destroying'); - const deferredSelectedLifecycleStage = useDeferredValue( - selectedLifecycleStage, - ); - const [paginationState, setPaginationState] = useState<{ - current: number; - pageSize: number; - }>({ + + const { + baiPaginationOption, + tablePaginationOption, + setTablePaginationOption, + } = useBAIPaginationOptionState({ current: 1, pageSize: 10, }); - - const deferredPaginationState = useDeferredValue(paginationState); - const isPendingPaginationAndFilter = - selectedLifecycleStage !== deferredSelectedLifecycleStage || - paginationState !== deferredPaginationState; const lifecycleStageFilter = - deferredSelectedLifecycleStage === 'created&destroying' + selectedLifecycleStage === 'created&destroying' ? `lifecycle_stage == "created" | lifecycle_stage == "destroying"` - : `lifecycle_stage == "${deferredSelectedLifecycleStage}"`; + : `lifecycle_stage == "${selectedLifecycleStage}"`; const [isRefetchPending, startRefetchTransition] = useTransition(); const [isFilterPending, startFilterTransition] = useTransition(); + const [isPendingPageChange, startPageChangeTransition] = useTransition(); const [servicesFetchKey, updateServicesFetchKey] = useUpdatableState('initial-fetch'); const [optimisticDeletingId, setOptimisticDeletingId] = useState< @@ -104,22 +100,23 @@ const EndpointListPage: React.FC = ({ children }) => { >(); const [filterStr, setFilterStr] = useQueryParam('filter', StringParam); + const [order, setOrder] = useState(); const [currentUser] = useCurrentUserInfo(); const currentUserRole = useCurrentUserRole(); + const curProject = useCurrentProjectValue(); + const baiClient = useSuspendedBackendaiClient(); + const webuiNavigate = useWebUINavigate(); - // const [selectedGeneration, setSelectedGeneration] = useState< - // "current" | "next" - // >("next"); - - const columns: ColumnsType = [ + const columns = filterEmptyItem>([ { title: t('modelService.EndpointName'), - dataIndex: 'endpoint_id', key: 'endpointName', + dataIndex: 'name', fixed: 'left', - render: (endpoint_id, row) => ( - {row.name} + render: (name, row) => ( + {name} ), + sorter: true, }, { title: t('modelService.EndpointId'), @@ -132,12 +129,12 @@ const EndpointListPage: React.FC = ({ children }) => { }, { title: t('modelService.ServiceEndpoint'), - dataIndex: 'endpoint_id', + dataIndex: 'url', key: 'url', - render: (endpoint_id, row) => - row.url ? ( - - {row.url} + render: (url) => + url ? ( + + {url} ) : ( '-' @@ -153,7 +150,10 @@ const EndpointListPage: React.FC = ({ children }) => { type="text" icon={} style={ - isDestroyingStatus(row?.desired_session_count, row?.status) || + isDestroyingStatus( + row.replicas ?? row.desired_session_count, + row.status, + ) || (!!row.created_user_email && row.created_user_email !== currentUser.email) ? { @@ -164,7 +164,10 @@ const EndpointListPage: React.FC = ({ children }) => { } } disabled={ - isDestroyingStatus(row?.desired_session_count, row?.status) || + isDestroyingStatus( + row.replicas ?? row.desired_session_count, + row.status, + ) || (!!row.created_user_email && row.created_user_email !== currentUser.email) } @@ -177,7 +180,10 @@ const EndpointListPage: React.FC = ({ children }) => { icon={ = ({ children }) => { optimisticDeletingId === row.endpoint_id } disabled={isDestroyingStatus( - row?.desired_session_count, - row?.status, + row.replicas ?? row.desired_session_count, + row.status, )} onClick={() => { modal.confirm({ @@ -205,10 +211,10 @@ const EndpointListPage: React.FC = ({ children }) => { type: 'primary', }, onOk: () => { - setOptimisticDeletingId(row.endpoint_id); + setOptimisticDeletingId(row?.endpoint_id); // FIXME: any better idea for handling result? row.endpoint_id && - terminateModelServiceMutation.mutate(row.endpoint_id, { + terminateModelServiceMutation.mutate(row?.endpoint_id, { onSuccess: (res) => { startRefetchTransition(() => { updateServicesFetchKey(); @@ -244,19 +250,15 @@ const EndpointListPage: React.FC = ({ children }) => { key: 'status', render: (text, row) => , }, - ...(baiClient.is_admin - ? [ - { - title: t('modelService.Owner'), - // created_user_email is referred by EndpointOwnerInfoFragment - dataIndex: 'created_user_email', - key: 'session_owner', - render: (_: string, endpoint_info: Endpoint) => ( - - ), - }, - ] - : []), + baiClient.is_admin && { + title: t('modelService.Owner'), + // created_user_email is referred by EndpointOwnerInfoFragment + dataIndex: 'created_user_email', + key: 'session_owner', + render: (_: string, endpoint_info: Endpoint) => ( + + ), + }, { title: t('modelService.CreatedAt'), dataIndex: 'created_at', @@ -273,10 +275,12 @@ const EndpointListPage: React.FC = ({ children }) => { }, }, { - title: t('modelService.DesiredSessionCount'), - dataIndex: 'desired_session_count', + title: t('modelService.NumberOfReplicas'), + dataIndex: baiClient.supports('replicas') + ? 'replicas' + : 'desired_session_count', key: 'desiredSessionCount', - render: (desired_session_count) => { + render: (desired_session_count: number) => { return desired_session_count < 0 ? '-' : desired_session_count; }, }, @@ -311,7 +315,7 @@ const EndpointListPage: React.FC = ({ children }) => { ), }, - ]; + ]); const [hiddenColumnKeys, setHiddenColumnKeys] = useHiddenColumnKeysSetting('EndpointListPage'); @@ -329,12 +333,14 @@ const EndpointListPage: React.FC = ({ children }) => { $limit: Int! $projectID: UUID $filter: String + $order: String ) { endpoint_list( offset: $offset limit: $limit project: $projectID filter: $filter + order: $order ) { total_count items { @@ -349,7 +355,8 @@ const EndpointListPage: React.FC = ({ children }) => { url open_to_public created_at @since(version: "23.09.0") - desired_session_count @required(action: NONE) + desired_session_count @deprecatedSince(version: "24.12.0") + replicas @since(version: "24.12.0") routings { routing_id endpoint @@ -369,10 +376,8 @@ const EndpointListPage: React.FC = ({ children }) => { } `, { - offset: - (deferredPaginationState.current - 1) * - deferredPaginationState.pageSize, - limit: deferredPaginationState.pageSize, + offset: baiPaginationOption.offset, + limit: baiPaginationOption.limit, projectID: curProject.id, filter: baiClient.supports('endpoint-lifecycle-stage-filter') ? [lifecycleStageFilter, filterStr] @@ -380,13 +385,13 @@ const EndpointListPage: React.FC = ({ children }) => { .map((v) => `(${v})`) .join(' & ') : undefined, + order, }, { fetchPolicy: 'network-only', fetchKey: servicesFetchKey, }, ); - const sortedEndpointList = _.sortBy(modelServiceList?.items, 'name'); // FIXME: struggling with sending data when active tab changes! // const runningModelServiceList = modelServiceList?.filter( @@ -439,11 +444,12 @@ const EndpointListPage: React.FC = ({ children }) => { { - setSelectedLifecycleStage(e.target?.value); - // reset pagination state when filter changes - setPaginationState({ - current: 1, - pageSize: paginationState.pageSize, + startPageChangeTransition(() => { + setSelectedLifecycleStage(e.target?.value); + setTablePaginationOption({ + current: 1, + pageSize: 10, + }); }); }} optionType="button" @@ -528,30 +534,36 @@ const EndpointListPage: React.FC = ({ children }) => { , }} scroll={{ x: 'max-content' }} rowKey={'endpoint_id'} - dataSource={(sortedEndpointList || []) as Endpoint[]} + dataSource={filterNonNullItems(modelServiceList?.items)} columns={_.filter( columns, (column) => !_.includes(hiddenColumnKeys, _.toString(column?.key)), )} + sortDirections={['descend', 'ascend', 'descend']} pagination={{ - pageSize: paginationState.pageSize, - current: paginationState.current, + pageSize: tablePaginationOption.pageSize, + current: tablePaginationOption.current, pageSizeOptions: ['10', '20', '50'], total: modelServiceList?.total_count || 0, showSizeChanger: true, - onChange(page, pageSize) { - setPaginationState({ - current: page, - pageSize: pageSize, - }); - }, style: { marginRight: token.marginXS }, }} + onChange={({ pageSize, current }, filter, sorter) => { + startPageChangeTransition(() => { + if (_.isNumber(current) && _.isNumber(pageSize)) { + setTablePaginationOption({ + current, + pageSize, + }); + } + setOrder(transformSorterToOrderString(sorter)); + }); + }} />