From a499b908127fd133c73d0efbe88157a7ec64ebaf Mon Sep 17 00:00:00 2001 From: Jong Eun Lee Date: Thu, 13 Feb 2025 22:44:01 +0800 Subject: [PATCH] feat: terminiate sessions in NEO session list --- .../TerminateSessionModal.tsx | 177 +++++++++--------- react/src/components/SessionNodes.tsx | 14 +- react/src/pages/ComputeSessionListPage.tsx | 144 +++++++++----- react/src/usePromiseTracker.ts | 75 ++++++++ 4 files changed, 272 insertions(+), 138 deletions(-) create mode 100644 react/src/usePromiseTracker.ts diff --git a/react/src/components/ComputeSessionNodeItems/TerminateSessionModal.tsx b/react/src/components/ComputeSessionNodeItems/TerminateSessionModal.tsx index 557a5de2d6..1a762f04cb 100644 --- a/react/src/components/ComputeSessionNodeItems/TerminateSessionModal.tsx +++ b/react/src/components/ComputeSessionNodeItems/TerminateSessionModal.tsx @@ -1,9 +1,10 @@ +import { filterEmptyItem } from '../../helper'; import { BackendAIClient, useSuspendedBackendaiClient } from '../../hooks'; import { useCurrentUserRole } from '../../hooks/backendai'; -import { useTanMutation } from '../../hooks/reactQueryAlias'; import { useSetBAINotification } from '../../hooks/useBAINotification'; import { useCurrentProjectValue } from '../../hooks/useCurrentProject'; import { usePainKiller } from '../../hooks/usePainKiller'; +import { usePromiseTracker } from '../../usePromiseTracker'; import BAIModal from '../BAIModal'; import Flex from '../Flex'; import { @@ -21,10 +22,12 @@ import { fetchQuery, useFragment, useRelayEnvironment } from 'react-relay'; interface TerminateSessionModalProps extends Omit { - sessionFrgmts: TerminateSessionModalFragment$key; + sessionFrgmts?: TerminateSessionModalFragment$key; onRequestClose: (success: boolean) => void; } +// Cannot destroy sessions in scheduled/preparing/pulling/prepared/creating/terminating/error status + const useStyle = createStyles(({ css, token }) => { return { custom: css` @@ -173,7 +176,7 @@ const terminateApp = async ( }; const TerminateSessionModal: React.FC = ({ - sessionFrgmts: sessionFrgmt, + sessionFrgmts, onRequestClose, ...modalProps }) => { @@ -198,7 +201,7 @@ const TerminateSessionModal: React.FC = ({ } } `, - sessionFrgmt, + sessionFrgmts, ); const [isForce, setIsForce] = useState(false); @@ -208,39 +211,40 @@ const TerminateSessionModal: React.FC = ({ const currentProject = useCurrentProjectValue(); - const terminateMutation = useTanMutation({ - mutationFn: async (session: Session) => { - return terminateApp( - session, - baiClient._config.accessKey, - currentProject.id, - baiClient, - ) - .catch((e) => { - return { - error: e, - }; - }) - .then((result) => { - const err = result?.error; - if ( - err === undefined || //no error - (err && // Even if wsproxy address is invalid, session must be deleted. - err.message && - (err.statusCode === 404 || err.statusCode === 500)) - ) { - // BAI client destroy try to request 3times as default - return baiClient.destroy( - session.row_id, - baiClient._config.accessKey, - isForce, - ); - } else { - throw err; - } - }); - }, - }); + const { pendingCount, trackPromise } = usePromiseTracker(); + + const terminiateSession = (session: Session) => { + return terminateApp( + session, + baiClient._config.accessKey, + currentProject.id, + baiClient, + ) + .catch((e) => { + return { + error: e, + }; + }) + .then((result) => { + const err = result?.error; + if ( + err === undefined || //no error + (err && // Even if wsproxy address is invalid, session must be deleted. + err.message && + (err.statusCode === 404 || err.statusCode === 500)) + ) { + // BAI client destroy try to request 3times as default + return baiClient.destroy( + session.row_id, + baiClient._config.accessKey, + isForce, + ); + } else { + throw err; + } + }); + }; + const relayEvn = useRelayEnvironment(); const painKiller = usePainKiller(); const { upsertNotification } = useSetBAINotification(); @@ -250,54 +254,57 @@ const TerminateSessionModal: React.FC = ({ centered title={t('session.TerminateSession')} open={openTerminateModal} - confirmLoading={terminateMutation.isPending} + confirmLoading={pendingCount > 0} onOk={() => { - if (sessions[0]?.row_id) { - const session = sessions[0]; - terminateMutation - .mutateAsync(session) - .then(() => { - setIsForce(false); - onRequestClose(true); - }) - .catch((err) => { - upsertNotification({ - message: painKiller.relieve(err?.title), - description: err?.message, - open: true, - }); - }) - .finally(() => { - // TODO: remove below code after session list migration to React - const event = new CustomEvent( - 'backend-ai-session-list-refreshed', - { - detail: 'running', - }, - ); - document.dispatchEvent(event); + const promises = _.map( + filterEmptyItem(_.castArray(sessions)), + (session) => { + return terminiateSession(session) + .catch((err) => { + upsertNotification({ + message: painKiller.relieve(err?.title), + description: err?.message, + open: true, + }); + }) + .finally(() => { + // TODO: remove below code after session list migration to React + const event = new CustomEvent( + 'backend-ai-session-list-refreshed', + { + detail: 'running', + }, + ); + document.dispatchEvent(event); - // refetch session node - return fetchQuery( - relayEvn, - graphql` - query TerminateSessionModalRefetchQuery( - $id: GlobalIDField! - $project_id: UUID! - ) { - compute_session_node(id: $id, project_id: $project_id) { - id - status + // refetch session node + return fetchQuery( + relayEvn, + graphql` + query TerminateSessionModalRefetchQuery( + $id: GlobalIDField! + $project_id: UUID! + ) { + compute_session_node(id: $id, project_id: $project_id) { + id + status + } } - } - `, - { - id: session.id, - project_id: currentProject.id, - }, - ).toPromise(); - }); - } + `, + { + id: session.id, + project_id: currentProject.id, + }, + ).toPromise(); + }); + }, + ); + promises.map(trackPromise); + Promise.allSettled(promises).then((results) => { + setIsForce(false); + + onRequestClose(true); + }); }} okText={isForce ? t('button.ForceTerminate') : t('session.Terminate')} okType="danger" @@ -320,9 +327,9 @@ const TerminateSessionModal: React.FC = ({ {t('userSettings.SessionTerminationDialog')} - {sessions.length === 1 - ? sessions[0]?.name - : `${sessions.length} sessions`} + {sessions?.length === 1 + ? sessions?.[0]?.name + : `${sessions?.length} sessions`} { +export type SessionNodeInList = NonNullable; +interface SessionNodesProps + extends Omit, 'dataSource' | 'columns'> { sessionsFrgmt: SessionNodesFragment$key; } + const SessionNodes: React.FC = ({ sessionsFrgmt, ...tableProps @@ -28,7 +34,7 @@ const SessionNodes: React.FC = ({ const sessions = useFragment( graphql` fragment SessionNodesFragment on ComputeSessionNode @relay(plural: true) { - id + id @required(action: NONE) row_id @required(action: NONE) name ...SessionStatusTagFragment @@ -49,7 +55,7 @@ const SessionNodes: React.FC = ({ neoStyle // TODO: fix type // @ts-ignore - rowKey={(record) => record.row_id as string} + rowKey={(record) => record.id as string} size="small" dataSource={filteredSessions} scroll={{ x: 'max-content' }} diff --git a/react/src/pages/ComputeSessionListPage.tsx b/react/src/pages/ComputeSessionListPage.tsx index 90a38ba5f2..b423f48785 100644 --- a/react/src/pages/ComputeSessionListPage.tsx +++ b/react/src/pages/ComputeSessionListPage.tsx @@ -2,9 +2,14 @@ import BAILink from '../components/BAILink'; import BAIPropertyFilter, { mergeFilterValues, } from '../components/BAIPropertyFilter'; +import TerminateSessionModal from '../components/ComputeSessionNodeItems/TerminateSessionModal'; import Flex from '../components/Flex'; import SessionNodes from '../components/SessionNodes'; -import { filterNonNullItems, transformSorterToOrderString } from '../helper'; +import { + filterEmptyItem, + filterNonNullItems, + transformSorterToOrderString, +} from '../helper'; import { useUpdatableState } from '../hooks'; import { useBAIPaginationOptionState } from '../hooks/reactPaginationQueryOptions'; import { useCurrentProjectValue } from '../hooks/useCurrentProject'; @@ -12,20 +17,27 @@ import { useDeferredQueryParams } from '../hooks/useDeferredQueryParams'; import { useInterval } from '../hooks/useIntervalValue'; import { ComputeSessionListPageQuery } from './__generated__/ComputeSessionListPageQuery.graphql'; import { LoadingOutlined } from '@ant-design/icons'; -import { Badge, Button, Card, Radio, Spin, Tabs, theme } from 'antd'; +import { useDynamicList } from 'ahooks'; +import { Badge, Button, Card, Radio, Spin, Tabs, theme, Tooltip } from 'antd'; import graphql from 'babel-plugin-relay/macro'; import _ from 'lodash'; -import { startTransition, useRef, useTransition } from 'react'; +import { PowerOffIcon } from 'lucide-react'; +import { Key, startTransition, useRef, useState, useTransition } from 'react'; import { useTranslation } from 'react-i18next'; import { useLazyLoadQuery } from 'react-relay'; import { StringParam, withDefault } from 'use-query-params'; type TypeFilterType = 'all' | 'interactive' | 'batch' | 'inference' | 'system'; +// type SessionNode = NonNullableNodeOnEdges< +// ComputeSessionListPageQuery$data['compute_session_nodes'] +// >; const ComputeSessionListPage = () => { const currentProject = useCurrentProjectValue(); const { t } = useTranslation(); const { token } = theme.useToken(); + const selectedSessionIdList = useDynamicList(); + const [isOpenTerminateModal, setOpenTerminateModal] = useState(false); const { baiPaginationOption, @@ -84,8 +96,9 @@ const ComputeSessionListPage = () => { ) { edges @required(action: THROW) { node @required(action: THROW) { - id + id @required(action: NONE) ...SessionNodesFragment + ...TerminateSessionModalFragment } } count @@ -195,56 +208,72 @@ const ComputeSessionListPage = () => { /> }> - - { - startFilterChangeTransition(() => { - setQuery({ statusCategory: e.target.value }, 'replaceIn'); - setTablePaginationOption({ current: 1 }); - }); - }} - options={[ - { - label: 'Running', - value: 'running', - }, - { - label: 'Finished', - value: 'finished', - }, - ]} - /> - { - startFilterChangeTransition(() => { - setQuery({ filter: value }, 'replaceIn'); - setTablePaginationOption({ current: 1 }); - }); - }} - /> + + + { + startFilterChangeTransition(() => { + setQuery({ statusCategory: e.target.value }, 'replaceIn'); + setTablePaginationOption({ current: 1 }); + }); + }} + options={[ + { + label: 'Running', + value: 'running', + }, + { + label: 'Finished', + value: 'finished', + }, + ]} + /> + { + startFilterChangeTransition(() => { + setQuery({ filter: value }, 'replaceIn'); + setTablePaginationOption({ current: 1 }); + }); + }} + /> + + + {selectedSessionIdList.list.length > 0 && ( + <> + {selectedSessionIdList.list.length} selected + +