diff --git a/.cspell.json b/.cspell.json
index 2391993bdb..b74cb62ce4 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -10,6 +10,7 @@
"FGPU",
"filebrowser",
"Frgmt",
+ "Frgmts",
"Gaudi",
"keypair",
"Lablup",
@@ -19,17 +20,14 @@
"RNGD",
"shmem",
"superadmin",
+ "textbox",
+ "vaadin",
"vfolder",
"vfolders",
"Warboy",
"webcomponent",
"webui",
- "wsproxy",
- "vfolders",
- "vfolder",
- "filebrowser",
- "vaadin",
- "textbox"
+ "wsproxy"
],
"flagWords": [
"데이터레이크",
diff --git a/react/data/schema.graphql b/react/data/schema.graphql
index 18912ef865..1ef2b25498 100644
--- a/react/data/schema.graphql
+++ b/react/data/schema.graphql
@@ -21,7 +21,18 @@ type Queries {
group_node(id: String!): GroupNode
"""Added in 24.03.0."""
- group_nodes(filter: String, order: String, offset: Int, before: String, after: String, first: Int, last: Int): GroupConnection
+ group_nodes(
+ """Added in 24.09.0."""
+ filter: String
+
+ """Added in 24.09.0."""
+ order: String
+ offset: Int
+ before: String
+ after: String
+ first: Int
+ last: Int
+ ): GroupConnection
group(
id: UUID!
domain_name: String
@@ -1258,9 +1269,7 @@ type ContainerRegistryNode implements Node {
"""The ID of the object"""
id: ID!
- """
- Added in 24.09.0. The undecoded UUID type id of DB container_registries row.
- """
+ """Added in 24.09.0. The UUID type id of DB container_registries row."""
row_id: UUID
name: String
diff --git a/react/relay.config.js b/react/relay.config.js
index c2d899a18f..a3e7218c08 100644
--- a/react/relay.config.js
+++ b/react/relay.config.js
@@ -31,5 +31,6 @@ module.exports = {
// https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#number-string-boolean-symbol-and-object
DateTime: 'string',
UUID: 'string',
+ JSONString: 'string',
},
};
diff --git a/react/src/App.tsx b/react/src/App.tsx
index 7b445f9b43..3e2da8daf0 100644
--- a/react/src/App.tsx
+++ b/react/src/App.tsx
@@ -57,6 +57,10 @@ const InteractiveLoginPage = React.lazy(
);
const ImportAndRunPage = React.lazy(() => import('./pages/ImportAndRunPage'));
+const ComputeSessionList = React.lazy(
+ () => import('./components/ComputeSessionList'),
+);
+
const RedirectToSummary = () => {
useSuspendedBackendaiClient();
const pathName = '/summary';
@@ -139,6 +143,11 @@ const router = createBrowserRouter([
{
path: '/job',
handle: { labelKey: 'webui.menu.Sessions' },
+ element: (
+
+
+
+ ),
},
{
path: '/serving',
diff --git a/react/src/components/BAIModal.tsx b/react/src/components/BAIModal.tsx
index 7e715b7e4b..e162dbc3e6 100644
--- a/react/src/components/BAIModal.tsx
+++ b/react/src/components/BAIModal.tsx
@@ -9,9 +9,7 @@ import Draggable from 'react-draggable';
export const DEFAULT_BAI_MODAL_Z_INDEX = 1001;
export interface BAIModalProps extends ModalProps {
- okText?: string; // customize text of ok button with adequate content
draggable?: boolean; // modal can be draggle
- className?: string;
}
const BAIModal: React.FC = ({
className,
diff --git a/react/src/components/ComputeSessionList.tsx b/react/src/components/ComputeSessionList.tsx
new file mode 100644
index 0000000000..046a13f5f1
--- /dev/null
+++ b/react/src/components/ComputeSessionList.tsx
@@ -0,0 +1,18 @@
+import SessionDetailDrawer from './SessionDetailDrawer';
+import React from 'react';
+import { StringParam, useQueryParam } from 'use-query-params';
+
+const ComputeSessionList = () => {
+ const [sessionId, setSessionId] = useQueryParam('sessionDetail', StringParam);
+ return (
+ {
+ setSessionId(null, 'replaceIn');
+ }}
+ />
+ );
+};
+
+export default ComputeSessionList;
diff --git a/react/src/components/ComputeSessionNodeItems/EditableSessionName.tsx b/react/src/components/ComputeSessionNodeItems/EditableSessionName.tsx
new file mode 100644
index 0000000000..5d7d8755c4
--- /dev/null
+++ b/react/src/components/ComputeSessionNodeItems/EditableSessionName.tsx
@@ -0,0 +1,84 @@
+import { EditableSessionNameFragment$key } from './__generated__/EditableSessionNameFragment.graphql';
+import { EditableSessionNameMutation } from './__generated__/EditableSessionNameMutation.graphql';
+import { theme } from 'antd';
+import Text, { TextProps } from 'antd/es/typography/Text';
+import Title, { TitleProps } from 'antd/es/typography/Title';
+import graphql from 'babel-plugin-relay/macro';
+import React, { useState } from 'react';
+import { useFragment, useMutation } from 'react-relay';
+
+type EditableSessionNameProps = {
+ sessionFrgmt: EditableSessionNameFragment$key;
+} & (
+ | ({ component?: typeof Text } & Omit)
+ | ({ component: typeof Title } & Omit)
+);
+
+const EditableSessionName: React.FC = ({
+ component: Component = Text,
+ sessionFrgmt,
+ style,
+ ...otherProps
+}) => {
+ const session = useFragment(
+ graphql`
+ fragment EditableSessionNameFragment on ComputeSessionNode {
+ id
+ name
+ priority
+ }
+ `,
+ sessionFrgmt,
+ );
+ const [optimisticName, setOptimisticName] = useState(session.name);
+ const { token } = theme.useToken();
+ const [commitEditMutation, isPendingEditMutation] =
+ useMutation(graphql`
+ mutation EditableSessionNameMutation($input: ModifyComputeSessionInput!) {
+ modify_compute_session(input: $input) {
+ item {
+ id
+ name
+ }
+ }
+ }
+ `);
+ return (
+ session && (
+ {
+ setOptimisticName(newName);
+ commitEditMutation({
+ variables: {
+ input: {
+ id: session.id,
+ name: newName,
+ // TODO: Setting the priority is not needed here. However, due to an API bug, we will keep it.
+ priority: session.priority,
+ },
+ },
+ onCompleted(response, errors) {},
+ onError(error) {},
+ });
+ },
+ triggerType: ['icon', 'text'],
+ }
+ }
+ copyable
+ style={{
+ ...style,
+ color: isPendingEditMutation ? token.colorTextTertiary : style?.color,
+ }}
+ {...otherProps}
+ >
+ {isPendingEditMutation ? optimisticName : session.name}
+
+ )
+ );
+};
+
+export default EditableSessionName;
diff --git a/react/src/components/ComputeSessionNodeItems/SessionActionButtons.tsx b/react/src/components/ComputeSessionNodeItems/SessionActionButtons.tsx
new file mode 100644
index 0000000000..3bb7c8ed36
--- /dev/null
+++ b/react/src/components/ComputeSessionNodeItems/SessionActionButtons.tsx
@@ -0,0 +1,122 @@
+import { useBackendAIAppLauncher } from '../../hooks/useBackendAIAppLauncher';
+import TerminateSessionModal from './TerminateSessionModal';
+import {
+ SessionActionButtonsFragment$data,
+ SessionActionButtonsFragment$key,
+} from './__generated__/SessionActionButtonsFragment.graphql';
+import { Tooltip, Button, theme } from 'antd';
+import graphql from 'babel-plugin-relay/macro';
+import { TerminalIcon, PowerOffIcon } from 'lucide-react';
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useFragment } from 'react-relay';
+
+interface SessionActionButtonsProps {
+ sessionFrgmt: SessionActionButtonsFragment$key | null;
+}
+// const isRunning = (session:SessionActionButtonsFragment$data) => {
+// return [
+// 'batch',
+// 'interactive',
+// 'inference',
+// 'system',
+// 'running',
+// 'others',
+// ].includes(session);
+// }
+
+const isActive = (session: SessionActionButtonsFragment$data) => {
+ return !['TERMINATED', 'CANCELLED'].includes(session?.status || '');
+};
+// const isTransitional = (session: SessionActionButtonsFragment$data) => {
+// return [
+// 'RESTARTING',
+// 'TERMINATING',
+// 'PENDING',
+// 'PREPARING',
+// 'PULLING',
+// ].includes(session?.status || '');
+// };
+
+const SessionActionButtons: React.FC = ({
+ sessionFrgmt,
+}) => {
+ const { token } = theme.useToken();
+ const appLauncher = useBackendAIAppLauncher();
+
+ const { t } = useTranslation();
+
+ const session = useFragment(
+ graphql`
+ fragment SessionActionButtonsFragment on ComputeSessionNode {
+ id
+ row_id @required(action: NONE)
+ status
+ access_key
+ service_ports
+ commit_status
+
+ ...TerminateSessionModalFragment
+ }
+ `,
+ sessionFrgmt,
+ );
+ const [openTerminateModal, setOpenTerminateModal] = useState(false);
+
+ // const isDisabledTermination = !['PENDING'].includes(session?.status || '') && session?.commit_status === 'ongoing'
+ // ${(this._isRunning && !this._isPreparing(rowData.item.status)) ||
+ // this._isError(rowData.item.status)
+ return (
+ session && (
+ <>
+ {/*
+ } onClick={()=>{
+ appLauncher.showLauncher({
+ "access-key": session?.access_key || '',
+ "service-ports": session?.service_ports || '',
+ })
+ }} />
+ */}
+
+ }
+ onClick={() => {
+ appLauncher.runTerminal(session?.row_id);
+ }}
+ />
+
+ {/* Don't put this modal to end of the return array(<>>). */}
+ {
+ setOpenTerminateModal(false);
+ }}
+ />
+ {/*
+
+ } />
+
+
+ } />
+ */}
+
+
+ }
+ onClick={() => {
+ setOpenTerminateModal(true);
+ }}
+ />
+
+ >
+ )
+ );
+};
+
+export default SessionActionButtons;
diff --git a/react/src/components/ComputeSessionNodeItems/SessionReservation.tsx b/react/src/components/ComputeSessionNodeItems/SessionReservation.tsx
new file mode 100644
index 0000000000..68d775d7e8
--- /dev/null
+++ b/react/src/components/ComputeSessionNodeItems/SessionReservation.tsx
@@ -0,0 +1,49 @@
+import { useSuspendedBackendaiClient } from '../../hooks';
+import BAIIntervalText from '../BAIIntervalText';
+import DoubleTag from '../DoubleTag';
+import { SessionReservationFragment$key } from './__generated__/SessionReservationFragment.graphql';
+import graphql from 'babel-plugin-relay/macro';
+import dayjs from 'dayjs';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { useFragment } from 'react-relay';
+
+const SessionReservation: React.FC<{
+ sessionFrgmt: SessionReservationFragment$key;
+}> = ({ sessionFrgmt }) => {
+ const baiClient = useSuspendedBackendaiClient();
+ const { t } = useTranslation();
+ const session = useFragment(
+ graphql`
+ fragment SessionReservationFragment on ComputeSessionNode {
+ id
+ created_at
+ terminated_at
+ }
+ `,
+ sessionFrgmt,
+ );
+ return (
+ <>
+ {dayjs(session.created_at).format('lll')}
+ {
+ return session?.created_at
+ ? baiClient.utils.elapsedTime(
+ session.created_at,
+ session?.terminated_at,
+ )
+ : '-';
+ }}
+ delay={1000}
+ />,
+ ]}
+ />
+ >
+ );
+};
+
+export default SessionReservation;
diff --git a/react/src/components/ComputeSessionNodeItems/SessionResourceNumbers.tsx b/react/src/components/ComputeSessionNodeItems/SessionResourceNumbers.tsx
new file mode 100644
index 0000000000..f5c72688f0
--- /dev/null
+++ b/react/src/components/ComputeSessionNodeItems/SessionResourceNumbers.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const SessionResourceNumbers = () => {
+ return SessionResourceNumbers
;
+};
+
+export default SessionResourceNumbers;
diff --git a/react/src/components/ComputeSessionNodeItems/SessionStatusTag.tsx b/react/src/components/ComputeSessionNodeItems/SessionStatusTag.tsx
new file mode 100644
index 0000000000..a21dd639a1
--- /dev/null
+++ b/react/src/components/ComputeSessionNodeItems/SessionStatusTag.tsx
@@ -0,0 +1,109 @@
+import Flex from '../Flex';
+import {
+ SessionStatusTagFragment$data,
+ SessionStatusTagFragment$key,
+} from './__generated__/SessionStatusTagFragment.graphql';
+import { LoadingOutlined } from '@ant-design/icons';
+import { Tag, theme } from 'antd';
+import graphql from 'babel-plugin-relay/macro';
+import _ from 'lodash';
+import React from 'react';
+import { useFragment } from 'react-relay';
+
+interface SessionStatusTagProps {
+ sessionFrgmt?: SessionStatusTagFragment$key | null;
+}
+const statusTagColor = {
+ //prepare
+ RESTARTING: 'blue',
+ PREPARING: 'blue',
+ PULLING: 'blue',
+ //running
+ RUNNING: 'green',
+ PENDING: 'green',
+ SCHEDULED: 'green',
+ //error
+ ERROR: 'red',
+ //finished return undefined
+};
+
+const isTransitional = (session: SessionStatusTagFragment$data) => {
+ return [
+ 'RESTARTING',
+ 'TERMINATING',
+ 'PENDING',
+ 'PREPARING',
+ 'PULLING',
+ ].includes(session?.status || '');
+};
+
+const statusInfoTagColor = {
+ // 'idle-timeout': undefined,
+ // 'user-requested': undefined,
+ // scheduled: undefined,
+ // 'self-terminated': undefined,
+ 'no-available-instances': 'red',
+ 'failed-to-start': 'red',
+ 'creation-failed': 'red',
+};
+const SessionStatusTag: React.FC = ({
+ sessionFrgmt,
+}) => {
+ const session = useFragment(
+ graphql`
+ fragment SessionStatusTagFragment on ComputeSessionNode {
+ id
+ name
+ status
+ status_info
+ }
+ `,
+ sessionFrgmt,
+ );
+ const { token } = theme.useToken();
+
+ return session ? (
+ _.isEmpty(session.status_info) ? (
+ : undefined}
+ >
+ {session.status || ' '}
+
+ ) : (
+
+
+ {session.status}
+
+
+ {session.status_info}
+
+
+ )
+ ) : null;
+};
+
+export default SessionStatusTag;
diff --git a/react/src/components/ComputeSessionNodeItems/SessionTypeTag.tsx b/react/src/components/ComputeSessionNodeItems/SessionTypeTag.tsx
new file mode 100644
index 0000000000..1be423c2bb
--- /dev/null
+++ b/react/src/components/ComputeSessionNodeItems/SessionTypeTag.tsx
@@ -0,0 +1,35 @@
+import { SessionTypeTagFragment$key } from './__generated__/SessionTypeTagFragment.graphql';
+import { Tag } from 'antd';
+import graphql from 'babel-plugin-relay/macro';
+import _ from 'lodash';
+import React from 'react';
+import { useFragment } from 'react-relay';
+
+const typeTagColor = {
+ INTERACTIVE: 'green',
+ BATCH: 'darkgreen',
+ INFERENCE: 'blue',
+};
+
+interface SessionTypeTagProps {
+ sessionFrgmt: SessionTypeTagFragment$key;
+}
+
+const SessionTypeTag: React.FC = ({ sessionFrgmt }) => {
+ const session = useFragment(
+ graphql`
+ fragment SessionTypeTagFragment on ComputeSessionNode {
+ type
+ }
+ `,
+ sessionFrgmt,
+ );
+
+ return (
+
+ {_.toUpper(session.type || '')}
+
+ );
+};
+
+export default SessionTypeTag;
diff --git a/react/src/components/ComputeSessionNodeItems/TerminateSessionModal.tsx b/react/src/components/ComputeSessionNodeItems/TerminateSessionModal.tsx
new file mode 100644
index 0000000000..c9cef41539
--- /dev/null
+++ b/react/src/components/ComputeSessionNodeItems/TerminateSessionModal.tsx
@@ -0,0 +1,378 @@
+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 BAIModal from '../BAIModal';
+import Flex from '../Flex';
+import {
+ TerminateSessionModalFragment$data,
+ TerminateSessionModalFragment$key,
+} from './__generated__/TerminateSessionModalFragment.graphql';
+import { TerminateSessionModalRefetchQuery } from './__generated__/TerminateSessionModalRefetchQuery.graphql';
+import { Card, Checkbox, ModalProps, Typography } from 'antd';
+import { createStyles } from 'antd-style';
+import graphql from 'babel-plugin-relay/macro';
+import _ from 'lodash';
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { fetchQuery, useFragment, useRelayEnvironment } from 'react-relay';
+
+interface TerminateSessionModalProps
+ extends Omit {
+ sessionFrgmts: TerminateSessionModalFragment$key;
+ onRequestClose: (success: boolean) => void;
+}
+
+const useStyle = createStyles(({ css, token }) => {
+ return {
+ custom: css`
+ ul {
+ list-style-type: circle;
+ padding-left: ${token.paddingMD}px;
+ }
+ `,
+ };
+});
+
+type KernelType = NonNullableNodeOnEdges<
+ NonNullable['kernel_nodes']
+>;
+
+type Session = NonNullable;
+
+const sendRequest = async (
+ rqst: {
+ uri: string;
+ } & RequestInit,
+) => {
+ let resp;
+ let body;
+ try {
+ if (rqst.method === 'GET') {
+ rqst.body = undefined;
+ }
+ resp = await fetch(rqst.uri, rqst);
+ const contentType = resp.headers.get('Content-Type');
+ if (contentType === null) {
+ body = resp.ok;
+ if (!resp.ok) {
+ // @ts-ignore
+ throw new Error(resp);
+ }
+ } else if (
+ contentType.startsWith('application/json') ||
+ contentType.startsWith('application/problem+json')
+ ) {
+ body = await resp.json();
+ } else if (contentType.startsWith('text/')) {
+ body = await resp.text();
+ } else {
+ body = await resp.blob();
+ }
+ if (!resp.ok) {
+ throw body;
+ }
+ } catch (e) {
+ return resp;
+ }
+ return body;
+};
+
+const getWSProxyVersion = async (
+ resourceGroupIdOfSession: string,
+ projectId: string,
+ baiClient: BackendAIClient,
+) => {
+ // TODO: remove globalThis.appLauncher(backend-ai-app-launcher) dependency after migration to React
+ if (baiClient.debug === true) {
+ // @ts-ignore
+ if (globalThis.appLauncher?.forceUseV1Proxy?.checked) return 'v1';
+ // @ts-ignore
+ else if (globalThis.appLauncher?.forceUseV2Proxy?.checked) return 'v2';
+ }
+
+ // @ts-ignore
+ if (globalThis.isElectron) {
+ return 'v1';
+ }
+ return baiClient.scalingGroup
+ .getWsproxyVersion(resourceGroupIdOfSession, projectId)
+ .then((result: { wsproxy_version: string }) => {
+ return result.wsproxy_version;
+ });
+};
+
+const getProxyURL = async (
+ resourceGroupIdOfSession: string,
+ projectId: string,
+ baiClient: BackendAIClient,
+) => {
+ let url = 'http://127.0.0.1:5050/';
+ if (
+ // @ts-ignore
+ globalThis.__local_proxy !== undefined &&
+ // @ts-ignore
+ globalThis.__local_proxy.url !== undefined
+ ) {
+ // @ts-ignore
+ url = globalThis.__local_proxy.url;
+ } else if (baiClient._config.proxyURL !== undefined) {
+ url = baiClient._config.proxyURL;
+ }
+ if (resourceGroupIdOfSession !== undefined && projectId !== undefined) {
+ const wsproxyVersion = await getWSProxyVersion(
+ resourceGroupIdOfSession,
+ projectId,
+ baiClient,
+ );
+ if (wsproxyVersion !== 'v1') {
+ url = new URL(`${wsproxyVersion}/`, url).href;
+ }
+ }
+ return url;
+};
+
+const terminateApp = async (
+ session: Session,
+ accessKey: string,
+ currentProjectId: string,
+ baiClient: BackendAIClient,
+) => {
+ const proxyURL = await getProxyURL(
+ session.scaling_group,
+ currentProjectId,
+ baiClient,
+ );
+
+ const rqst = {
+ method: 'GET',
+ uri: new URL(`proxy/${accessKey}/${session.row_id}`, proxyURL).href,
+ };
+
+ return sendRequest(rqst).then((response) => {
+ let uri = new URL(`proxy/${accessKey}/${session.row_id}/delete`, proxyURL);
+ if (localStorage.getItem('backendaiwebui.appproxy-permit-key')) {
+ uri.searchParams.set(
+ 'permit_key',
+ localStorage.getItem('backendaiwebui.appproxy-permit-key') || '',
+ );
+ uri = new URL(uri.href);
+ }
+ if (response !== undefined && response.code !== 404) {
+ return sendRequest({
+ method: 'GET',
+ uri: uri.href,
+ credentials: 'include',
+ mode: 'cors',
+ });
+ }
+ return true;
+ });
+};
+
+const TerminateSessionModal: React.FC = ({
+ sessionFrgmts: sessionFrgmt,
+ onRequestClose,
+ ...modalProps
+}) => {
+ const openTerminateModal = false;
+ const { t } = useTranslation();
+ const { styles } = useStyle();
+ const sessions = useFragment(
+ graphql`
+ fragment TerminateSessionModalFragment on ComputeSessionNode
+ @relay(plural: true) {
+ id
+ row_id
+ name
+ scaling_group @required(action: NONE)
+ kernel_nodes {
+ edges {
+ node {
+ container_id
+ agent_id
+ }
+ }
+ }
+ }
+ `,
+ sessionFrgmt,
+ );
+ const [isForce, setIsForce] = useState(false);
+ const userRole = useCurrentUserRole();
+
+ const baiClient = useSuspendedBackendaiClient();
+
+ 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 relayEvn = useRelayEnvironment();
+ const painKiller = usePainKiller();
+ const { upsertNotification } = useSetBAINotification();
+
+ return (
+ {
+ 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);
+
+ // 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();
+ });
+ }
+ }}
+ okText={isForce ? t('button.ForceTerminate') : t('session.Terminate')}
+ okType="danger"
+ okButtonProps={{
+ type: isForce ? 'primary' : 'default',
+ }}
+ onCancel={() => {
+ setIsForce(false);
+ onRequestClose(false);
+ }}
+ {...modalProps}
+ >
+
+
+ {t('usersettings.SessionTerminationDialog')}
+
+
+ {sessions.length === 1
+ ? sessions[0]?.name
+ : `${sessions.length} sessions`}
+
+ {
+ setIsForce(e.target.checked);
+ }}
+ >
+ {t('button.ForceTerminate')}
+
+ {isForce && (
+
+
+ {t('session.ForceTerminateWarningMsg')}
+
+
+ - {t('session.ForceTerminateWarningMsg2')}
+ - {t('session.ForceTerminateWarningMsg3')}
+
+ {userRole === 'superadmin' && (
+ <>
+
+ {_.chain(sessions)
+ .map((s) => s?.kernel_nodes?.edges)
+ .map((edges) => edges?.map((e) => e?.node))
+ .flatten()
+ .groupBy('agent_id')
+ .map((kernels: Array, agentId: string) => {
+ return (
+ <>
+ {agentId}
+
+ {kernels.map((k) => (
+ -
+
+ {k.container_id}
+
+
+ ))}
+
+ >
+ );
+ })
+ .value()}
+
+ >
+ )}
+
+ )}
+
+
+ );
+};
+
+export default TerminateSessionModal;
diff --git a/react/src/components/KeypairResourcePolicyList.tsx b/react/src/components/KeypairResourcePolicyList.tsx
index f3f0743c80..c5a89eedaa 100644
--- a/react/src/components/KeypairResourcePolicyList.tsx
+++ b/react/src/components/KeypairResourcePolicyList.tsx
@@ -119,16 +119,19 @@ const KeypairResourcePolicyList: React.FC = (
render: (text, row) => (
{!_.isEmpty(JSON.parse(row?.total_resource_slots || '{}'))
- ? _.map(JSON.parse(row?.total_resource_slots), (value, type) => {
- return (
-
- );
- })
+ ? _.map(
+ JSON.parse(row?.total_resource_slots || '{}'),
+ (value, type) => {
+ return (
+
+ );
+ },
+ )
: '-'}
),
diff --git a/react/src/components/ResourceNumber.tsx b/react/src/components/ResourceNumber.tsx
index 4d170cdb57..c98c5e38c5 100644
--- a/react/src/components/ResourceNumber.tsx
+++ b/react/src/components/ResourceNumber.tsx
@@ -138,6 +138,8 @@ export const ResourceTypeIcon: React.FC = ({
type as KnownAcceleratorResourceSlotName
] ?? ;
+ const { mergedResourceSlots } = useResourceSlotsDetails();
+
const content =
typeof targetIcon === 'string' ? (
= ({
);
return showTooltip ? (
- {content}
+
+ {content}
+
) : (
{content}
);
diff --git a/react/src/components/ResourcePresetSelect.tsx b/react/src/components/ResourcePresetSelect.tsx
index e293a4a182..e5f119e59b 100644
--- a/react/src/components/ResourcePresetSelect.tsx
+++ b/react/src/components/ResourcePresetSelect.tsx
@@ -124,7 +124,7 @@ const ResourcePresetSelect: React.FC = ({
options: _.map(resource_presets, (preset, index) => {
const slotsInfo: {
[key in ResourceSlotName]: string;
- } = JSON.parse(preset?.resource_slots);
+ } = JSON.parse(preset?.resource_slots || '{}');
const disabled = allocatablePresetNames
? !allocatablePresetNames.includes(preset?.name || '')
: undefined;
diff --git a/react/src/components/ServiceLauncherPageContent.tsx b/react/src/components/ServiceLauncherPageContent.tsx
index a8d67d6b51..cae562b34a 100644
--- a/react/src/components/ServiceLauncherPageContent.tsx
+++ b/react/src/components/ServiceLauncherPageContent.tsx
@@ -646,22 +646,25 @@ const ServiceLauncherPageContent: React.FC = ({
desiredRoutingCount: endpoint?.desired_session_count ?? 1,
// FIXME: memory doesn't applied to resource allocation
resource: {
- cpu: parseInt(JSON.parse(endpoint?.resource_slots)?.cpu),
+ cpu: parseInt(JSON.parse(endpoint?.resource_slots || '{}')?.cpu),
mem: iSizeToSize(
- JSON.parse(endpoint?.resource_slots)?.mem + 'b',
+ JSON.parse(endpoint?.resource_slots || '{}')?.mem + 'b',
'g',
3,
true,
)?.numberUnit,
shmem: iSizeToSize(
- JSON.parse(endpoint?.resource_opts)?.shmem ||
+ JSON.parse(endpoint?.resource_opts || '{}')?.shmem ||
AUTOMATIC_DEFAULT_SHMEM,
'g',
3,
true,
)?.numberUnit,
...getAIAcceleratorWithStringifiedKey(
- _.omit(JSON.parse(endpoint?.resource_slots), ['cpu', 'mem']),
+ _.omit(JSON.parse(endpoint?.resource_slots || '{}'), [
+ 'cpu',
+ 'mem',
+ ]),
),
},
cluster_mode:
@@ -974,7 +977,9 @@ const ServiceLauncherPageContent: React.FC = ({
key={type}
type={type}
value={value}
- opts={endpoint?.resource_opts}
+ opts={JSON.parse(
+ endpoint?.resource_opts || '{}',
+ )}
/>
);
},
diff --git a/react/src/components/SessionDetailContent.tsx b/react/src/components/SessionDetailContent.tsx
new file mode 100644
index 0000000000..3545aa219c
--- /dev/null
+++ b/react/src/components/SessionDetailContent.tsx
@@ -0,0 +1,196 @@
+import SessionKernelTags from '../components/ImageTags';
+import { toGlobalId } from '../helper';
+import { useCurrentUserRole } from '../hooks/backendai';
+import { useCurrentProjectValue } from '../hooks/useCurrentProject';
+import { ResourceNumbersOfSession } from '../pages/SessionLauncherPage';
+import EditableSessionName from './ComputeSessionNodeItems/EditableSessionName';
+import SessionActionButtons from './ComputeSessionNodeItems/SessionActionButtons';
+import SessionReservation from './ComputeSessionNodeItems/SessionReservation';
+import SessionStatusTag from './ComputeSessionNodeItems/SessionStatusTag';
+import SessionTypeTag from './ComputeSessionNodeItems/SessionTypeTag';
+import Flex from './Flex';
+import ImageMetaIcon from './ImageMetaIcon';
+import { SessionDetailContentQuery } from './__generated__/SessionDetailContentQuery.graphql';
+import {
+ Alert,
+ Button,
+ Descriptions,
+ Grid,
+ theme,
+ Tooltip,
+ Typography,
+} from 'antd';
+import Title from 'antd/es/typography/Title';
+import graphql from 'babel-plugin-relay/macro';
+import { useTranslation } from 'react-i18next';
+import { useLazyLoadQuery } from 'react-relay';
+
+const SessionDetailContent: React.FC<{
+ id: string;
+ fetchKey?: string;
+}> = ({ id, fetchKey = 'initial' }) => {
+ const { t } = useTranslation();
+ const { token } = theme.useToken();
+ const currentProject = useCurrentProjectValue();
+ const userRole = useCurrentUserRole();
+
+ const { md } = Grid.useBreakpoint();
+ const { session, legacy_session } =
+ useLazyLoadQuery(
+ // In compute_session_node, there are missing fields. We need to use `compute_session` to get the missing fields.
+ graphql`
+ query SessionDetailContentQuery(
+ $id: GlobalIDField!
+ $uuid: UUID!
+ $project_id: UUID!
+ ) {
+ session: compute_session_node(id: $id, project_id: $project_id) {
+ id
+ row_id
+ name
+ project_id
+ user_id
+ resource_opts
+ status
+ kernel_nodes {
+ edges {
+ node {
+ id
+ image {
+ id
+ name
+ architecture
+ tag
+ }
+ }
+ }
+ }
+ vfolder_mounts
+ created_at @required(action: NONE)
+ terminated_at
+ scaling_group
+ agent_ids
+ requested_slots
+
+ ...SessionStatusTagFragment
+ ...SessionActionButtonsFragment
+ ...SessionTypeTagFragment
+ ...EditableSessionNameFragment
+ ...SessionReservationFragment
+ }
+ legacy_session: compute_session(id: $uuid) {
+ image
+ mounts
+ user_email
+ architecture
+ }
+ }
+ `,
+ {
+ id: toGlobalId('ComputeSessionNode', id),
+ uuid: id,
+ project_id: currentProject.id,
+ },
+ {
+ fetchPolicy: 'network-only',
+ fetchKey: fetchKey,
+ },
+ );
+
+ const imageFullName =
+ legacy_session?.image &&
+ legacy_session?.architecture &&
+ legacy_session.image + '@' + legacy_session.architecture;
+ return session ? (
+
+ {/* {JSON.stringify(compute_session_node.requested_slots, null, 2)} */}
+
+
+
+
+
+
+
+
+
+
+ {session.row_id}
+
+
+ {(userRole === 'admin' || userRole === 'superadmin') && (
+
+ {legacy_session?.user_email}
+
+ )}
+
+
+ {/* } /> */}
+
+
+
+
+
+ {imageFullName ? (
+
+
+
+
+
+
+ ) : (
+ '-'
+ )}
+
+
+ {legacy_session?.mounts?.join(', ')}
+
+
+
+
+ {session.scaling_group}
+
+
+
+
+
+ {session.agent_ids || '-'}
+
+
+
+
+
+
+
+
+ ) : (
+
+ );
+};
+
+export default SessionDetailContent;
diff --git a/react/src/components/SessionDetailDrawer.tsx b/react/src/components/SessionDetailDrawer.tsx
new file mode 100644
index 0000000000..743f581c05
--- /dev/null
+++ b/react/src/components/SessionDetailDrawer.tsx
@@ -0,0 +1,58 @@
+import { useSuspendedBackendaiClient, useUpdatableState } from '../hooks';
+import SessionDetailContent from './SessionDetailContent';
+import { ReloadOutlined } from '@ant-design/icons';
+import { Button, Drawer, Skeleton, Tooltip } from 'antd';
+import { DrawerProps } from 'antd/lib';
+import React, { Suspense, useTransition } from 'react';
+import { useTranslation } from 'react-i18next';
+
+// import { StringParam, useQueryParam } from 'use-query-params';
+
+interface SessionDetailDrawerProps extends DrawerProps {
+ sessionId?: string;
+}
+const SessionDetailDrawer: React.FC = ({
+ sessionId,
+ ...drawerProps
+}) => {
+ const { t } = useTranslation();
+ // const [sessionId, setSessionId] = useQueryParam('sessionDetail', StringParam);
+ useSuspendedBackendaiClient();
+
+ const [isPendingReload, startReloadTransition] = useTransition();
+
+ const [fetchKey, updateFetchKey] = useUpdatableState('first');
+ return (
+ {
+ drawerProps.onClose?.(e);
+ // setSessionId(null, 'pushIn');
+ }}
+ extra={
+
+ }
+ onClick={() => {
+ startReloadTransition(() => {
+ updateFetchKey();
+ });
+ }}
+ />
+
+ }
+ >
+ }>
+ {sessionId && (
+
+ )}
+
+
+ );
+};
+
+export default SessionDetailDrawer;
diff --git a/react/src/helper/index.tsx b/react/src/helper/index.tsx
index 01899189f3..34dca0e8c1 100644
--- a/react/src/helper/index.tsx
+++ b/react/src/helper/index.tsx
@@ -392,3 +392,7 @@ export function formatToUUID(str: string) {
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`;
}
+
+export const toGlobalId = (type: string, id: string): string => {
+ return btoa(`${type}:${id}`);
+};
diff --git a/react/src/hooks/index.tsx b/react/src/hooks/index.tsx
index 54774e5a45..d66fea4021 100644
--- a/react/src/hooks/index.tsx
+++ b/react/src/hooks/index.tsx
@@ -84,6 +84,26 @@ export const useAnonymousBackendaiClient = ({
return client;
};
+export type BackendAIClient = {
+ vfolder: {
+ list: (path: string) => Promise;
+ list_hosts: () => Promise;
+ list_all_hosts: () => Promise;
+ list_files: (path: string, id: string) => Promise;
+ list_allowed_types: () => Promise;
+ clone: (input: any, name: string) => Promise;
+ };
+ supports: (feature: string) => boolean;
+ [key: string]: any;
+ _config: BackendAIConfig;
+ isManagerVersionCompatibleWith: (version: string) => boolean;
+ utils: {
+ elapsedTime: (
+ start: string | Date | number,
+ end?: string | Date | number | null,
+ ) => string;
+ };
+};
export const useSuspendedBackendaiClient = () => {
const { data: client } = useSuspenseTanQuery({
queryKey: ['backendai-client-for-suspense'],
@@ -112,20 +132,7 @@ export const useSuspendedBackendaiClient = () => {
// enabled: false,
});
- return client as {
- vfolder: {
- list: (path: string) => Promise;
- list_hosts: () => Promise;
- list_all_hosts: () => Promise;
- list_files: (path: string, id: string) => Promise;
- list_allowed_types: () => Promise;
- clone: (input: any, name: string) => Promise;
- };
- supports: (feature: string) => boolean;
- [key: string]: any;
- _config: BackendAIConfig;
- isManagerVersionCompatibleWith: (version: string) => boolean;
- };
+ return client as BackendAIClient;
};
interface ImageMetadata {
diff --git a/react/src/hooks/useBackendAIAppLauncher.tsx b/react/src/hooks/useBackendAIAppLauncher.tsx
new file mode 100644
index 0000000000..738fa8ad23
--- /dev/null
+++ b/react/src/hooks/useBackendAIAppLauncher.tsx
@@ -0,0 +1,23 @@
+export const useBackendAIAppLauncher = () => {
+ // This is not use any React hooks, so it's not a React hook.
+ // But keep it here for the future refactoring.
+
+ // @ts-ignore
+ return {
+ runTerminal: (sessionId: string) => {
+ // @ts-ignore
+ globalThis.appLauncher.runTerminal(sessionId);
+ },
+ showLauncher: (params: {
+ 'session-uuid'?: string;
+ 'access-key'?: string;
+ 'app-services'?: string;
+ mode?: string;
+ 'app-services-option'?: string;
+ 'service-ports'?: string;
+ runtime?: string;
+ filename?: string;
+ arguments?: string;
+ }) => {},
+ };
+};
diff --git a/react/src/pages/EndpointDetailPage.tsx b/react/src/pages/EndpointDetailPage.tsx
index f67165e6c9..1cd4ed6507 100644
--- a/react/src/pages/EndpointDetailPage.tsx
+++ b/react/src/pages/EndpointDetailPage.tsx
@@ -7,6 +7,7 @@ import { useFolderExplorerOpener } from '../components/FolderExplorerOpener';
import ImageMetaIcon from '../components/ImageMetaIcon';
import InferenceSessionErrorModal from '../components/InferenceSessionErrorModal';
import ResourceNumber from '../components/ResourceNumber';
+import SessionDetailDrawer from '../components/SessionDetailDrawer';
import VFolderLazyView from '../components/VFolderLazyView';
import { InferenceSessionErrorModalFragment$key } from '../components/__generated__/InferenceSessionErrorModalFragment.graphql';
import ChatUIModal from '../components/lablupTalkativotUI/ChatUIModal';
@@ -116,6 +117,7 @@ const EndpointDetailPage: React.FC = () => {
const { message } = App.useApp();
const webuiNavigate = useWebUINavigate();
const { open } = useFolderExplorerOpener();
+ const [selectedSessionId, setSelectedSessionId] = useState();
const { endpoint, endpoint_token_list } =
useLazyLoadQuery(
graphql`
@@ -404,7 +406,9 @@ const EndpointDetailPage: React.FC = () => {
label: t('session.launcher.EnvironmentVariable'),
children: (
- {_.isEmpty(JSON.parse(endpoint?.environ)) ? '-' : endpoint?.environ}
+ {_.isEmpty(JSON.parse(endpoint?.environ || '{}'))
+ ? '-'
+ : endpoint?.environ}
),
span: {
@@ -651,6 +655,17 @@ const EndpointDetailPage: React.FC = () => {
{
title: t('modelService.SessionId'),
dataIndex: 'session',
+ render: (sessionId) => {
+ return (
+ {
+ setSelectedSessionId(sessionId);
+ }}
+ >
+ {sessionId}
+
+ );
+ },
},
{
title: t('modelService.Status'),
@@ -713,6 +728,13 @@ const EndpointDetailPage: React.FC = () => {
setOpenChatModal(false);
}}
/>
+ {
+ setSelectedSessionId(undefined);
+ }}
+ />
);
};
diff --git a/react/src/pages/SessionLauncherPage.tsx b/react/src/pages/SessionLauncherPage.tsx
index e75d7e2855..b63de932d6 100644
--- a/react/src/pages/SessionLauncherPage.tsx
+++ b/react/src/pages/SessionLauncherPage.tsx
@@ -1874,7 +1874,7 @@ export const ResourceNumbersOfSession: React.FC = ({
{_.map(
_.omit(resource, 'shmem', 'accelerator', 'acceleratorType'),
(value, type) => {
- return (
+ return value === '0' ? null : (