diff --git a/react/src/App.tsx b/react/src/App.tsx
index 823d0b3775..b9005d32f7 100644
--- a/react/src/App.tsx
+++ b/react/src/App.tsx
@@ -57,8 +57,8 @@ const InteractiveLoginPage = React.lazy(
);
const ImportAndRunPage = React.lazy(() => import('./pages/ImportAndRunPage'));
-const ComputeSessionList = React.lazy(
- () => import('./components/ComputeSessionList'),
+const ComputeSessionListPage = React.lazy(
+ () => import('./pages/ComputeSessionListPage'),
);
const AgentSummaryPage = React.lazy(() => import('./pages/AgentSummaryPage'));
@@ -139,7 +139,7 @@ const router = createBrowserRouter([
handle: { labelKey: 'webui.menu.Sessions' },
element: (
-
+
),
},
diff --git a/react/src/components/BAILink.tsx b/react/src/components/BAILink.tsx
index 6bfd558e82..21b663ba04 100644
--- a/react/src/components/BAILink.tsx
+++ b/react/src/components/BAILink.tsx
@@ -15,7 +15,7 @@ const useStyles = createStyles(({ css, token }) => ({
}));
interface BAILinkProps extends LinkProps {
- type: 'hover';
+ type?: 'hover';
}
const BAILink: React.FC = ({ type, ...linkProps }) => {
const { styles } = useStyles();
diff --git a/react/src/components/ComputeSessionList.tsx b/react/src/components/ComputeSessionList.tsx
deleted file mode 100644
index e67c4273df..0000000000
--- a/react/src/components/ComputeSessionList.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import BAIModal from './BAIModal';
-import ContainerLogModalWithLazyQueryLoader from './ComputeSessionNodeItems/ContainerLogModalWithLazyQueryLoader';
-import SessionDetailDrawer from './SessionDetailDrawer';
-import { Skeleton } from 'antd';
-import { Suspense, useEffect, useState } from 'react';
-import { StringParam, useQueryParam } from 'use-query-params';
-
-const ComputeSessionList = () => {
- const [sessionId, setSessionId] = useQueryParam('sessionDetail', StringParam);
-
- const [containerLogModalSessionId, setContainerLogModalSessionId] =
- useState();
- useEffect(() => {
- const handler = (e: any) => {
- setContainerLogModalSessionId(e.detail);
- };
- document.addEventListener('bai-open-session-log', handler);
- return () => {
- document.removeEventListener('bai-open-session-log', handler);
- };
- }, []);
- return (
- <>
- {
- setSessionId(null, 'replaceIn');
- }}
- />
- {containerLogModalSessionId && (
-
-
-
- }
- >
- {
- setContainerLogModalSessionId(undefined);
- }}
- />
-
- )}
- >
- );
-};
-
-export default ComputeSessionList;
diff --git a/react/src/components/ComputeSessionNodeItems/SessionOccupiedSlot.tsx b/react/src/components/ComputeSessionNodeItems/SessionOccupiedSlot.tsx
new file mode 100644
index 0000000000..c10d39883e
--- /dev/null
+++ b/react/src/components/ComputeSessionNodeItems/SessionOccupiedSlot.tsx
@@ -0,0 +1,54 @@
+import { convertBinarySizeUnit } from '../../helper';
+import { useResourceSlotsDetails } from '../../hooks/backendai';
+import { SessionOccupiedSlotFragment$key } from './__generated__/SessionOccupiedSlotFragment.graphql';
+import { Divider, Typography } from 'antd';
+import graphql from 'babel-plugin-relay/macro';
+import _ from 'lodash';
+import React from 'react';
+import { useFragment } from 'react-relay';
+
+interface OccupiedSlotViewProps {
+ sessionFrgmt: SessionOccupiedSlotFragment$key;
+ type: 'cpu' | 'mem' | 'accelerator';
+}
+const SessionOccupiedSlot: React.FC = ({
+ type,
+ sessionFrgmt,
+}) => {
+ const { deviceMetadata } = useResourceSlotsDetails();
+ const session = useFragment(
+ graphql`
+ fragment SessionOccupiedSlotFragment on ComputeSessionNode {
+ id
+ occupied_slots
+ }
+ `,
+ sessionFrgmt,
+ );
+
+ const occupiedSlots = JSON.parse(session.occupied_slots || '{}');
+
+ if (type === 'cpu') {
+ return occupiedSlots.cpu ?? '-';
+ } else if (type === 'mem') {
+ const mem = occupiedSlots.mem ?? '-';
+ return mem === '-' ? mem : convertBinarySizeUnit(mem, 'G')?.number + ' GiB';
+ } else if (type === 'accelerator') {
+ const occupiedAccelerators = _.omit(occupiedSlots, ['cpu', 'mem']);
+ return _.isEmpty(occupiedAccelerators)
+ ? '-'
+ : _.map(occupiedAccelerators, (value, key) => {
+ return (
+ <>
+ {value}
+
+
+ {deviceMetadata?.[key]?.human_readable_name}
+
+ >
+ );
+ });
+ }
+};
+
+export default SessionOccupiedSlot;
diff --git a/react/src/components/ComputeSessionNodeItems/SessionReservation.tsx b/react/src/components/ComputeSessionNodeItems/SessionReservation.tsx
index 9f0d9aed9f..c53f949a27 100644
--- a/react/src/components/ComputeSessionNodeItems/SessionReservation.tsx
+++ b/react/src/components/ComputeSessionNodeItems/SessionReservation.tsx
@@ -10,7 +10,8 @@ import { useFragment } from 'react-relay';
const SessionReservation: React.FC<{
sessionFrgmt: SessionReservationFragment$key;
-}> = ({ sessionFrgmt }) => {
+ mode?: 'simple-elapsed' | 'detail';
+}> = ({ sessionFrgmt, mode = 'detail' }) => {
const baiClient = useSuspendedBackendaiClient();
const { t } = useTranslation();
const session = useFragment(
@@ -25,7 +26,7 @@ const SessionReservation: React.FC<{
);
return (
<>
- {dayjs(session.created_at).format('lll')}
+ {mode !== 'simple-elapsed' && dayjs(session.created_at).format('lll')}
{
return session?.created_at
@@ -36,14 +37,18 @@ const SessionReservation: React.FC<{
: '-';
}}
delay={1000}
- render={(intervalValue) => (
-
- )}
+ render={(intervalValue) =>
+ mode === 'simple-elapsed' ? (
+ intervalValue
+ ) : (
+
+ )
+ }
/>
>
);
diff --git a/react/src/components/ComputeSessionNodeItems/SessionStatusTag.tsx b/react/src/components/ComputeSessionNodeItems/SessionStatusTag.tsx
index bc1c7291d2..4d8fb5ed68 100644
--- a/react/src/components/ComputeSessionNodeItems/SessionStatusTag.tsx
+++ b/react/src/components/ComputeSessionNodeItems/SessionStatusTag.tsx
@@ -12,6 +12,7 @@ import { useFragment } from 'react-relay';
interface SessionStatusTagProps {
sessionFrgmt?: SessionStatusTagFragment$key | null;
+ showInfo?: boolean;
}
const statusTagColor = {
//prepare
@@ -49,6 +50,7 @@ const statusInfoTagColor = {
};
const SessionStatusTag: React.FC = ({
sessionFrgmt,
+ showInfo,
}) => {
const session = useFragment(
graphql`
@@ -64,7 +66,7 @@ const SessionStatusTag: React.FC = ({
const { token } = theme.useToken();
return session ? (
- _.isEmpty(session.status_info) ? (
+ _.isEmpty(session.status_info) || !showInfo ? (
= ({
`,
sessionFrgmt,
);
+
const [isForce, setIsForce] = useState(false);
const userRole = useCurrentUserRole();
diff --git a/react/src/components/ComputeSessionNodeList.tsx b/react/src/components/ComputeSessionNodeList.tsx
new file mode 100644
index 0000000000..9ff22837e2
--- /dev/null
+++ b/react/src/components/ComputeSessionNodeList.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const ComputeSessionNodeList = () => {
+ return ComputeSessionNodeList
;
+};
+
+export default ComputeSessionNodeList;
diff --git a/react/src/components/SessionDetailContent.tsx b/react/src/components/SessionDetailContent.tsx
index fb7d4cfd67..b228dceb0d 100644
--- a/react/src/components/SessionDetailContent.tsx
+++ b/react/src/components/SessionDetailContent.tsx
@@ -166,7 +166,7 @@ const SessionDetailContent: React.FC<{
label={t('session.Status')}
contentStyle={{ display: 'flex', gap: token.marginSM }}
>
-
+
{/* } /> */}
diff --git a/react/src/components/SessionNodes.tsx b/react/src/components/SessionNodes.tsx
new file mode 100644
index 0000000000..139f9acd0c
--- /dev/null
+++ b/react/src/components/SessionNodes.tsx
@@ -0,0 +1,132 @@
+import BAILink from './BAILink';
+import BAITable from './BAITable';
+import SessionOccupiedSlot from './ComputeSessionNodeItems/SessionOccupiedSlot';
+import SessionReservation from './ComputeSessionNodeItems/SessionReservation';
+import SessionStatusTag from './ComputeSessionNodeItems/SessionStatusTag';
+import SessionDetailDrawer from './SessionDetailDrawer';
+import { SessionNodesFragment$key } from './__generated__/SessionNodesFragment.graphql';
+import { TableProps } from 'antd/lib';
+import graphql from 'babel-plugin-relay/macro';
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useFragment } from 'react-relay';
+
+interface SessionNodesProps extends Omit {
+ sessionsFrgmt: SessionNodesFragment$key;
+}
+const SessionNodes: React.FC = ({
+ sessionsFrgmt,
+ ...tableProps
+}) => {
+ const { t } = useTranslation();
+ const [selectedSessionId, setSelectedSessionId] = useState();
+
+ const sessions = useFragment(
+ graphql`
+ fragment SessionNodesFragment on ComputeSessionNode @relay(plural: true) {
+ id
+ row_id
+ name
+ ...SessionStatusTagFragment
+ ...SessionReservationFragment
+ ...SessionOccupiedSlotFragment
+ }
+ `,
+ sessionsFrgmt,
+ );
+
+ return (
+ <>
+ record.rowId}
+ dataSource={sessions}
+ columns={[
+ {
+ key: 'name',
+ title: t('session.SessionName'),
+ dataIndex: 'name',
+ render: (name: string, session) => {
+ return (
+ {
+ session.row_id && setSelectedSessionId(session.row_id);
+ }}
+ >
+ {name}
+
+ );
+ },
+ sorter: true,
+ },
+ {
+ key: 'status',
+ title: t('session.Status'),
+ dataIndex: 'status',
+ render: (status: string, session) => {
+ // @ts-expect-error
+ return ;
+ },
+ },
+ {
+ key: 'utils',
+ title: t('session.Utilization'),
+ },
+ {
+ key: 'accelerator',
+ title: t('session.launcher.AIAccelerator'),
+ render: (__, session) => {
+ return (
+
+ );
+ },
+ },
+ {
+ key: 'cpu',
+ title: t('session.launcher.CPU'),
+ render: (__, session) => {
+ // @ts-expect-error
+ return ;
+ },
+ },
+ {
+ key: 'mem',
+ title: t('session.launcher.Memory'),
+ render: (__, session) => {
+ // @ts-expect-error
+ return ;
+ },
+ },
+ {
+ key: 'elapsedTime',
+ title: t('session.ElapsedTime'),
+ render: (__, session) => {
+ return (
+
+ );
+ },
+ },
+ ]}
+ {...tableProps}
+ />
+ {
+ setSelectedSessionId(undefined);
+ }}
+ />
+ >
+ );
+};
+
+export default SessionNodes;
diff --git a/react/src/hooks/reactPaginationQueryOptions.tsx b/react/src/hooks/reactPaginationQueryOptions.tsx
index 0359904054..f1f0e3b0e0 100644
--- a/react/src/hooks/reactPaginationQueryOptions.tsx
+++ b/react/src/hooks/reactPaginationQueryOptions.tsx
@@ -281,6 +281,7 @@ export const useBAIPaginationQueryOptions = ({
interface BAIPaginationOption {
limit: number;
offset: number;
+ first?: number;
// filter?: string;
// order?: string;
}
@@ -297,13 +298,16 @@ export const useBAIPaginationOptionState = (
): {
baiPaginationOption: BAIPaginationOption;
tablePaginationOption: AntdBasicPaginationOption;
- setTablePaginationOption: (pagination: AntdBasicPaginationOption) => void;
+ setTablePaginationOption: (
+ pagination: Partial,
+ ) => void;
} => {
const [options, setOptions] =
useState(initialOptions);
return {
baiPaginationOption: {
limit: options.pageSize,
+ first: options.pageSize,
offset:
options.current > 1 ? (options.current - 1) * options.pageSize : 0,
},
diff --git a/react/src/pages/ComputeSessionListPage.tsx b/react/src/pages/ComputeSessionListPage.tsx
new file mode 100644
index 0000000000..4c671313f8
--- /dev/null
+++ b/react/src/pages/ComputeSessionListPage.tsx
@@ -0,0 +1,202 @@
+import BAIModal from '../components/BAIModal';
+import BAIPropertyFilter, {
+ mergeFilterValues,
+} from '../components/BAIPropertyFilter';
+import ContainerLogModalWithLazyQueryLoader from '../components/ComputeSessionNodeItems/ContainerLogModalWithLazyQueryLoader';
+import Flex from '../components/Flex';
+import SessionDetailDrawer from '../components/SessionDetailDrawer';
+import SessionNodes from '../components/SessionNodes';
+import { filterNonNullItems } from '../helper';
+import { useBAIPaginationOptionState } from '../hooks/reactPaginationQueryOptions';
+import { useCurrentProjectValue } from '../hooks/useCurrentProject';
+import { ComputeSessionListPageQuery } from './__generated__/ComputeSessionListPageQuery.graphql';
+import { LoadingOutlined } from '@ant-design/icons';
+import { Card, Radio, Skeleton } from 'antd';
+import graphql from 'babel-plugin-relay/macro';
+import _ from 'lodash';
+import { Suspense, useEffect, useState, useTransition } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useLazyLoadQuery } from 'react-relay';
+import { StringParam, useQueryParam } from 'use-query-params';
+
+const ComputeSessionList = () => {
+ const [sessionId, setSessionId] = useQueryParam('sessionDetail', StringParam);
+ const currentProject = useCurrentProjectValue();
+ const [containerLogModalSessionId, setContainerLogModalSessionId] =
+ useState();
+ useEffect(() => {
+ const handler = (e: any) => {
+ setContainerLogModalSessionId(e.detail);
+ };
+ document.addEventListener('bai-open-session-log', handler);
+ return () => {
+ document.removeEventListener('bai-open-session-log', handler);
+ };
+ }, []);
+
+ const { t } = useTranslation();
+
+ const {
+ baiPaginationOption,
+ tablePaginationOption,
+ setTablePaginationOption,
+ } = useBAIPaginationOptionState({
+ current: 1,
+ pageSize: 10,
+ });
+ const [isPendingPageChange, startPageChangeTransition] = useTransition();
+ const [filterString, setFilterString] = useState();
+ const [isPendingFilter, startFilterTransition] = useTransition();
+ const [runningFilterType, setRunningFilterType] = useState<
+ 'running' | 'finished'
+ >('running');
+ const statusFilter =
+ runningFilterType === 'running'
+ ? 'status != "TERMINATED" & status != "CANCELLED"'
+ : 'status == "TERMINATED" | status == "CANCELLED"';
+
+ const { compute_session_nodes } =
+ useLazyLoadQuery(
+ graphql`
+ query ComputeSessionListPageQuery(
+ $projectId: UUID!
+ $first: Int = 20
+ $offset: Int = 0
+ $filter: String
+ ) {
+ compute_session_nodes(
+ project_id: $projectId
+ first: $first
+ offset: $offset
+ filter: $filter
+ ) {
+ edges @required(action: THROW) {
+ node @required(action: THROW) {
+ id
+ ...SessionNodesFragment
+ }
+ }
+ count
+ }
+ }
+ `,
+ {
+ projectId: currentProject.id,
+ offset: baiPaginationOption.offset,
+ first: baiPaginationOption.first,
+ filter: mergeFilterValues([statusFilter, filterString]),
+ },
+ {
+ fetchPolicy: 'network-only',
+ },
+ );
+
+ return (
+ <>
+
+
+
+ {
+ startFilterTransition(() => {
+ setRunningFilterType(e.target.value);
+ });
+ }}
+ options={[
+ {
+ label: 'Running',
+ value: 'running',
+ },
+ {
+ label: 'Finished',
+ value: 'finished',
+ },
+ ]}
+ />
+ {
+ startFilterTransition(() => {
+ setFilterString(value);
+ setTablePaginationOption({ current: 1 });
+ });
+ }}
+ />
+
+ e?.node),
+ )}
+ pagination={{
+ pageSize: tablePaginationOption.pageSize,
+ current: tablePaginationOption.current,
+ total: compute_session_nodes?.count ?? 0,
+ showTotal: (total) => {
+ return total;
+ },
+ }}
+ loading={{
+ spinning: isPendingPageChange || isPendingFilter,
+ indicator: ,
+ }}
+ onChange={({ current, pageSize }) => {
+ if (_.isNumber(current) && _.isNumber(pageSize)) {
+ startPageChangeTransition(() => {
+ setTablePaginationOption({ current, pageSize });
+ });
+ }
+ }}
+ />
+
+
+ {
+ setSessionId(null, 'replaceIn');
+ }}
+ />
+ {containerLogModalSessionId && (
+
+
+
+ }
+ >
+ {
+ setContainerLogModalSessionId(undefined);
+ }}
+ />
+
+ )}
+ >
+ );
+};
+
+export default ComputeSessionList;
diff --git a/resources/theme.json b/resources/theme.json
index 1b2c28a8b9..442d32d645 100644
--- a/resources/theme.json
+++ b/resources/theme.json
@@ -14,7 +14,10 @@
"borderRadiusSM": 1
},
"Table": {
- "headerBorderRadius": 0
+ "headerBorderRadius": 0,
+ "headerBg": "#E3E3E3",
+ "cellPaddingBlock": 6,
+ "algorithm": true
},
"Layout": {
"lightSiderBg": "#FFF",