diff --git a/react/src/components/AccessTokenList.tsx b/react/src/components/AccessTokenList.tsx new file mode 100644 index 0000000000..d29f9be6e2 --- /dev/null +++ b/react/src/components/AccessTokenList.tsx @@ -0,0 +1,93 @@ +import { Tag, Typography } from 'antd'; +import { + BAITable, + filterOutNullAndUndefined, + filterOutEmpty, + BAIColumnType, +} from 'backend.ai-ui'; +import dayjs from 'dayjs'; +import { useTranslation } from 'react-i18next'; +import { graphql, useFragment } from 'react-relay'; +import { + AccessTokenListFragment$key, + AccessTokenListFragment$data, +} from 'src/__generated__/AccessTokenListFragment.graphql'; + +export type AccessTokenInList = NonNullable< + NonNullable[number] +>; + +interface AccessTokenListProps { + accessTokensFrgmt?: AccessTokenListFragment$key; +} + +const AccessTokenList: React.FC = ({ + accessTokensFrgmt, +}) => { + const { t } = useTranslation(); + + const accessTokens = useFragment( + graphql` + fragment AccessTokenListFragment on AccessToken @relay(plural: true) { + id + token + createdAt + validUntil + } + `, + accessTokensFrgmt, + ); + + const filteredAccessTokens = filterOutNullAndUndefined(accessTokens); + + const columns = filterOutEmpty>([ + { + key: 'token', + title: t('deployment.Token'), + dataIndex: 'token', + fixed: 'left', + render: (token) => ( + + {token} + + ), + }, + { + title: t('modelService.Status'), + render: (text, row) => { + const isExpired = dayjs.utc(row.validUntil).isBefore(); + return ( + + {isExpired ? 'Expired' : 'Valid'} + + ); + }, + }, + { + key: 'validUntil', + title: t('deployment.ExpiredDate'), + dataIndex: 'validUntil', + render: (value) => dayjs(value).format('LLL'), + }, + { + key: 'createdAt', + title: t('deployment.CreatedAt'), + dataIndex: 'createdAt', + render: (value) => dayjs(value).format('LLL'), + }, + ]); + + return ( + <> + + + ); +}; + +export default AccessTokenList; diff --git a/react/src/components/DeploymentList.tsx b/react/src/components/DeploymentList.tsx index 8722c77b9b..7996e9d412 100644 --- a/react/src/components/DeploymentList.tsx +++ b/react/src/components/DeploymentList.tsx @@ -188,7 +188,9 @@ const DeploymentList: React.FC = ({ dataIndex: ['revision', 'name'], key: 'revisionName', render: (name, row) => ( - {name} + + {name} + ), defaultHidden: true, }, diff --git a/react/src/components/DeploymentRevisionList.tsx b/react/src/components/DeploymentRevisionList.tsx new file mode 100644 index 0000000000..68b4aae11d --- /dev/null +++ b/react/src/components/DeploymentRevisionList.tsx @@ -0,0 +1,223 @@ +import ImageDetailNodeSimpleTag from './ImageDetailNodeSimpleTag'; +import { MoreOutlined } from '@ant-design/icons'; +import { Button, Dropdown, Tag, Typography } from 'antd'; +import { + BAITable, + filterOutNullAndUndefined, + filterOutEmpty, + BAIColumnType, + convertToBinaryUnit, + BAIFlex, + toLocalId, +} from 'backend.ai-ui'; +import dayjs from 'dayjs'; +import _ from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { graphql, useFragment } from 'react-relay'; +import { + DeploymentRevisionListFragment$key, + DeploymentRevisionListFragment$data, +} from 'src/__generated__/DeploymentRevisionListFragment.graphql'; +import { getAIAcceleratorWithStringifiedKey } from 'src/helper'; +import { useBAISettingUserState } from 'src/hooks/useBAISetting'; +import { ResourceNumbersOfSession } from 'src/pages/SessionLauncherPage'; + +export type RevisionInList = NonNullable< + NonNullable[number] +>; + +interface DeploymentRevisionListProps { + activeRevisionId: string; + revisionsFrgmt?: DeploymentRevisionListFragment$key; + onRevisionSelect?: ( + revision: RevisionInList, + action: 'setActive' | 'createFrom', + ) => void; +} + +const DeploymentRevisionList: React.FC = ({ + activeRevisionId, + revisionsFrgmt, + onRevisionSelect, +}) => { + const { t } = useTranslation(); + + const revisions = useFragment( + graphql` + fragment DeploymentRevisionListFragment on ModelRevision + @relay(plural: true) { + id + name + clusterConfig { + mode + size + } + resourceConfig { + resourceSlots + resourceOpts + } + modelRuntimeConfig { + runtimeVariant + } + image { + ...ImageDetailNodeSimpleTagFragment + } + createdAt + } + `, + revisionsFrgmt, + ); + + const filteredRevisions = filterOutNullAndUndefined(revisions); + + const columns = filterOutEmpty>([ + { + key: 'name', + title: t('deployment.RevisionName'), + dataIndex: 'name', + render: (name, row) => ( + + {name || '-'} + {row.id === activeRevisionId && ( + {t('deployment.ActiveRevision')} + )} + + ), + }, + { + key: 'id', + title: t('deployment.RevisionID'), + dataIndex: 'id', + render: (id) => {toLocalId(id)}, + defaultHidden: true, + }, + { + key: 'environments', + title: t('deployment.Environments'), + dataIndex: 'image', + render: (image) => ( + + ), + }, + { + key: 'runtimeVariant', + title: t('deployment.launcher.RuntimeVariant'), + dataIndex: ['modelRuntimeConfig', 'runtimeVariant'], + }, + { + key: 'resource', + title: t('deployment.launcher.ResourceAllocation'), + dataIndex: ['resourceConfig', 'resourceSlots'], + render: (resourceSlots, row) => { + const resourceSlotsObj = JSON.parse(resourceSlots || '{}'); + const processedResource = _.mapValues( + _.pick(resourceSlotsObj, ['cpu', 'mem']), + (value, key) => + key === 'cpu' + ? _.toInteger(value) || 0 + : convertToBinaryUnit(value, 'g', 3, true)?.value || '0g', + ); + const shmem = convertToBinaryUnit( + JSON.parse(row?.resourceConfig?.resourceOpts || '{}')?.shmem, + 'g', + 3, + true, + )?.value; + const acceleratorInfo = getAIAcceleratorWithStringifiedKey( + _.omit(resourceSlotsObj, ['cpu', 'mem']), + ); + return ( + + ); + }, + }, + // { + // key: 'clusterMode', + // title: t('deployment.launcher.ClusterMode'), + // dataIndex: 'clusterMode', + // render: (mode) => { + // return mode === 'single-node' + // ? t('deployment.launcher.SingleNode') + // : t('deployment.launcher.MultiNode'); + // }, + // }, + { + key: 'createdAt', + title: t('deployment.CreatedAt'), + dataIndex: 'createdAt', + render: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'), + }, + { + key: 'control', + width: 40, + fixed: 'right', + render: (value, row) => ( + + { + onRevisionSelect?.(row, 'setActive'); + }, + }, + { + key: 'clone', + label: t('deployment.CreateRevisionFromSelected'), + onClick: () => { + onRevisionSelect?.(row, 'createFrom'); + }, + }, + ], + }} + trigger={['click']} + > + - } - > + {/* Deployment Info Card */} + - + - - - - } + {/* Tabbed Detail Card */} + - edge.node, + + } + > + {curTabKey === 'revisionHistory' && ( + + + + + edge.node, + )} + onRevisionSelect={(revision, action) => { + const foundRevision = + _.find( + deployment?.revisionHistory?.edges, + (edge) => edge.node.id === revision.id, + )?.node || null; + + if (action === 'createFrom') { + setSelectedRevision(foundRevision); + toggleRevisionCreationModal(); + } else if (action === 'setActive') { + if (!foundRevision?.id) { + message.error(t('message.FailedToUpdate')); + return; + } + modal.confirm({ + title: t('deployment.SetAsActiveRevision'), + content: ( + + ), + okButtonProps: { + loading: isInFlightSetCurrentRevision, + }, + onOk: () => { + commitSetCurrentRevision({ + variables: { + input: { + activeRevisionId: toLocalId(foundRevision.id), + id: deploymentId || '', + }, + }, + onCompleted: (res, errors) => { + const resultID = + res?.updateModelDeployment?.deployment?.revision + ?.id; + if ( + _.isEmpty(resultID) || + resultID !== foundRevision.id + ) { + message.error( + t('deployment.launcher.DeploymentUpdateFailed'), + ); + return; + } + if (errors && errors.length > 0) { + const errorMsgList = _.map( + errors, + (error) => error.message, + ); + for (const error of errorMsgList) { + message.error(error); + } + } else { + message.success(t('message.SuccessfullyUpdated')); + startRefetchTransition(() => { + updateFetchKey(); + }); + } + }, + onError: (err) => { + message.error( + err.message || + t('deployment.launcher.DeploymentUpdateFailed'), + ); + }, + }); + }, + }); + } + }} + /> + )} - pagination={false} - scroll={{ x: 'max-content' }} - bordered + {curTabKey === 'accessTokens' && ( + + + + + + edge.node, + ), + )} + /> + + + )} + {curTabKey === 'autoScalingRules' && ( +
TODO: implement table or description
+ )} + {curTabKey === 'replicas' && ( + + + + + + edge.node, + ), + )} + /> + + + )} +
+ + + { + if (success) { + startRefetchTransition(() => { + updateFetchKey(); + }); + } + setSelectedRevision(null); + toggleRevisionCreationModal(); + }} /> - + + { + if (success) { + startRefetchTransition(() => { + updateFetchKey(); + }); + } + toggleTokenGenerationModal(); + }} + /> ); }; diff --git a/react/src/pages/DeploymentListPage.tsx b/react/src/pages/DeploymentListPage.tsx index 1d88450b40..8afb058e9f 100644 --- a/react/src/pages/DeploymentListPage.tsx +++ b/react/src/pages/DeploymentListPage.tsx @@ -138,9 +138,10 @@ const DeploymentListPage: React.FC = () => { propertyLabel: t('deployment.Status'), type: 'enum', options: [ - { label: 'Created', value: 'CREATED' }, - { label: 'Deploying', value: 'DEPLOYING' }, { label: 'Ready', value: 'READY' }, + { label: 'Pending', value: 'PENDING' }, + { label: 'Scaling', value: 'SCALING' }, + { label: 'Deploying', value: 'DEPLOYING' }, { label: 'Stopping', value: 'STOPPING' }, { label: 'Stopped', value: 'STOPPED' }, ], diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 39c85de936..acbbc382dd 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -490,17 +490,24 @@ } }, "deployment": { + "AccessTokens": "Access Tokens", "Activate": "Activate", "Active": "Active", "ActiveReplicas": "Active Replicas", + "ActiveRevision": "Active Revision", "ActiveRevisions": "Active Revisions", + "AutoScalingRules": "Auto Scaling Rules", "CheckDomainAvailability": "Check domain availability", "ClusterSize": "Cluster Size", + "ConfirmUpdateActiveRevision": "Are you sure you want to set the \"{{name}}\" revision as the current active revision?", "CreateDeployment": "Create Deployment", + "CreateNewRevision": "Create New Revision", + "CreateRevision": "Create Revision", + "CreateRevisionFromSelected": "Create revision from selected", "CreatedAt": "Created At", "CreatedBy": "Created By", "CreatorEmail": "Creator Email", - "DefaultDeploymentStrategy": "Default Deployment Strategy", + "DefaultDeploymentStrategy": "Deployment Strategy", "DeploymentAndNetwork": "Deployment & Network", "DeploymentDetail": "Deployment Detail", "DeploymentInfo": "Deployment Info", @@ -508,7 +515,6 @@ "DeploymentName": "Deployment Name", "DeploymentNamePlaceholder": "Enter deployment name", "DeploymentNameRequired": "Deployment name is required", - "DeploymentStrategy": "Deployment Strategy", "Deployments": "Deployments", "Description": "Description", "DescriptionPlaceholder": "Enter description for this deployment/revision", @@ -517,12 +523,18 @@ "DomainHelp": "If not provided, domain will be auto-generated", "DomainPlaceholder": "api.example.com", "EndpointURL": "Endpoint URL", + "Environments": "Environments", "ExpertMode": "Expert Mode", "ExpertModeDescription": "Direct configuration based", "ExpertModeTooltip": "Expert mode allows direct configuration of all deployment settings", + "ExpiredDate": "Expired Date", + "GenerateNewToken": "Generate New Token", + "GenerateToken": "Generate Token", "Hibernate": "Hibernate", "ImageName": "Image Name", "Inactive": "Inactive", + "LivenessStatus": "Liveness Status", + "LivenessStatusDesc": "Indicates whether the deployment is running and healthy.", "Mode": "Mode", "ModeRequired": "Mode selection is required", "ModeWarning": "Mode cannot be changed after creation", @@ -530,17 +542,36 @@ "Name": "Name", "NumberOfDesiredReplicas": "Number of Desired Replicas", "Owner": "Owner", + "PleaseSelectTime": "Please select time", "PreferredDomainName": "Preferred Domain Name", "Public": "Public", + "ReadinessStatus": "Readiness Status", + "ReadinessStatusDesc": "Indicates whether the deployment is ready to receive traffic.", + "ReplicaError": "Replica Error", + "ReplicaID": "Replica ID", + "ReplicaLog": "Replica Log", "Replicas": "Replicas", "Resources": "Resources", + "RevisionHistory": "Revision History", + "RevisionID": "Revision ID", "RevisionName": "Revision Name", "Revisions": "Revisions", + "RoutesInfo": "Routes Info", + "RoutingID": "Routing ID", + "SessionID": "Session ID", + "SetAsActiveRevision": "Set as active revision", "SimpleMode": "Simple Mode", "SimpleModeDescription": "Preset based", "SimpleModeTooltip": "Simple mode uses predefined presets for easy deployment", "Status": "Status", + "SyncReplicas": "Sync replicas", + "SyncReplicasFailed": "The replica synchronization request failed.", + "SyncReplicasRequested": "The replica synchronization requested.", "Tags": "Tags", + "Token": "Token", + "TokenExpiredDateError": "The token expiration time must be after the current time", + "TokenGenerated": "Token generated successfully", + "TokenGenerationFailed": "Token generation failed ", "TokensLastHour": "Tokens (Last Hour)", "TotalResources": "Total Resources", "TrafficRatio": "Traffic Ratio", @@ -548,6 +579,7 @@ "TrafficRatioEditWarning": "Editing traffic ratios", "URL": "URL", "UpdatedAt": "Updated At", + "Weight": "Weight", "launcher": { "AdditionalMounts": "Additional Mounts", "AdvancedStrategyWarning": "Advanced Strategy", @@ -571,6 +603,7 @@ "Cores": "cores", "CreateDeployment": "Create Deployment", "CreateNewDeployment": "Create new deployment", + "DefaultDeploymentStrategy": "Deployment Strategy", "DefinitionPath": "Definition Path", "Deployment": "Deployment", "DeploymentAndNetwork": "Deployment & Network", @@ -580,7 +613,6 @@ "DeploymentName": "Deployment Name", "DeploymentNamePlaceholder": "my-model-deployment", "DeploymentNameRequired": "Deployment name is required", - "DeploymentStrategy": "Deployment Strategy", "DeploymentStrategyRequired": "Deployment strategy is required", "DeploymentUpdateFailed": "Failed to update deployment", "Devices": "devices",