diff --git a/src/pages/gpu-service/instances/apis/index.ts b/src/pages/gpu-service/instances/apis/index.ts index ba0832a4..cfaed4b1 100644 --- a/src/pages/gpu-service/instances/apis/index.ts +++ b/src/pages/gpu-service/instances/apis/index.ts @@ -1,6 +1,13 @@ import { request } from '@umijs/max'; +import _ from 'lodash'; import { omitPathParams } from '../../utils'; -import { InstanceTypeItem, ListItem } from '../config/types'; +import { + InstanceEvents, + InstanceLog, + InstanceLogQueryParams, + InstanceTypeItem, + ListItem +} from '../config/types'; export const GPU_SERVICE_INSTANCES_API = (params: { namespace: string; @@ -127,6 +134,55 @@ export async function queryGPUServiceInstanceTypes( ); } +export async function queryGPUServiceInstanceEvents( + params: { + namespace: string; + name: string; + clusterID?: number; + pretty?: string; + }, + options?: any +) { + if (!params.clusterID) { + return; + } + return request( + `${GPU_SERVICE_INSTANCES_API({ + namespace: params.namespace, + clusterID: params.clusterID + })}/${params.name}/events`, + { + method: 'GET', + params: omitPathParams(_.omit(params, ['name'])), + cancelToken: options?.token + } + ); +} + +export async function queryGPUServiceInstanceLog( + params: InstanceLogQueryParams & { + namespace: string; + name: string; + clusterID?: number; + }, + options?: any +) { + if (!params.clusterID) { + return; + } + return request( + `${GPU_SERVICE_INSTANCES_API({ + namespace: params.namespace, + clusterID: params.clusterID + })}/${params.name}/log`, + { + method: 'GET', + params: omitPathParams(_.omit(params, ['name'])), + cancelToken: options?.token + } + ); +} + export async function queryGPUServiceInstanceTypeItems( params: { name: string; diff --git a/src/pages/gpu-service/instances/components/view-logs-modal.tsx b/src/pages/gpu-service/instances/components/view-logs-modal.tsx new file mode 100644 index 00000000..a619210b --- /dev/null +++ b/src/pages/gpu-service/instances/components/view-logs-modal.tsx @@ -0,0 +1,106 @@ +import { LogsViewer } from '@gpustack/core-ui'; +import { useIntl } from '@umijs/max'; +import { Modal } from 'antd'; +import React, { useCallback, useEffect } from 'react'; + +type ViewModalProps = { + open: boolean; + url: string; + tail?: number; + status?: string; + onCancel: () => void; +}; + +const ViewLogsModal: React.FC = (props) => { + const intl = useIntl(); + const { open, onCancel, url } = props || {}; + const logsViewerRef = React.useRef(null); + const requestRef = React.useRef(null); + const contentRef = React.useRef(null); + + const handleCancel = useCallback(() => { + logsViewerRef.current?.abort(); + onCancel(); + }, [onCancel]); + + useEffect(() => { + const handleKeyDown = (e: any) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'a') { + e.preventDefault(); + if (contentRef.current) { + const range = document.createRange(); + range.selectNodeContents(contentRef.current); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); + } + } + }; + + if (open) { + document.addEventListener('keydown', handleKeyDown); + } + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [open]); + + useEffect(() => { + if (!url) return; + if (!open) { + logsViewerRef.current?.abort(); + requestRef.current?.current?.cancel?.(); + } + + return () => { + logsViewerRef.current?.abort(); + requestRef.current?.current?.cancel?.(); + }; + }, [url, open]); + + return ( + + + {intl.formatMessage({ id: 'common.button.viewlog' })} + + + } + zIndex={3000} + open={open} + centered={true} + onCancel={handleCancel} + destroyOnHidden={true} + closeIcon={true} + mask={{ + closable: false + }} + keyboard={true} + styles={{ + wrapper: { + borderRadius: 0 + } + }} + width="100%" + footer={null} + > +
+ +
+
+ ); +}; + +export default ViewLogsModal; diff --git a/src/pages/gpu-service/instances/config/index.ts b/src/pages/gpu-service/instances/config/index.ts index 9d3d1b38..e0cdd853 100644 --- a/src/pages/gpu-service/instances/config/index.ts +++ b/src/pages/gpu-service/instances/config/index.ts @@ -1,7 +1,8 @@ import { StatusMaps } from '@/config'; import { StatusType } from '@/config/types'; -import { icons } from '@gpustack/core-ui'; +import { IconFont, icons } from '@gpustack/core-ui'; import _ from 'lodash'; +import React from 'react'; import { InstanceTypeItem, InstanceTypeResource, @@ -45,6 +46,12 @@ export const status: Record = { }; export const rowActionList = [ + { + label: 'common.button.viewlog', + key: 'viewlog', + locale: true, + icon: React.createElement(IconFont, { type: 'icon-logs' }) + }, { label: 'common.button.edit', key: 'edit', diff --git a/src/pages/gpu-service/instances/config/types.ts b/src/pages/gpu-service/instances/config/types.ts index a9945bd7..94ecd3c2 100644 --- a/src/pages/gpu-service/instances/config/types.ts +++ b/src/pages/gpu-service/instances/config/types.ts @@ -134,3 +134,69 @@ export interface InstanceTypeItem { spec: InstanceTypeSpec; status: InstanceTypeStatus; } + +export interface InstanceEventObjectReference { + apiVersion?: string; + fieldPath?: string; + kind?: string; + name?: string; + namespace?: string; + resourceVersion?: string; + uid?: string; +} + +export interface InstanceEventSource { + component?: string; + host?: string; +} + +export interface InstanceEventSeries { + count?: number; + lastObservedTime?: string; +} + +export interface InstanceEventItem { + action?: string; + apiVersion?: string; + count?: number; + eventTime?: string; + firstTimestamp?: string; + involvedObject: InstanceEventObjectReference; + kind?: string; + lastTimestamp?: string; + message?: string; + metadata: { + name?: string; + namespace?: string; + uid?: string; + resourceVersion?: string; + creationTimestamp?: string; + }; + reason?: string; + related?: InstanceEventObjectReference; + reportingComponent?: string; + reportingInstance?: string; + series?: InstanceEventSeries; + source?: InstanceEventSource; + type?: string; +} + +export interface InstanceEvents { + apiVersion?: string; + kind?: string; + metadata?: { + resourceVersion?: string; + }; + items: InstanceEventItem[]; +} + +export interface InstanceLogQueryParams { + follow?: boolean; + limitBytes?: number; + sinceSeconds?: number; + tailLines?: number; + timestamps?: boolean; + pretty?: string; +} + +export type InstanceLog = string; diff --git a/src/pages/gpu-service/instances/hooks/use-view-logs.ts b/src/pages/gpu-service/instances/hooks/use-view-logs.ts new file mode 100644 index 00000000..0e199d01 --- /dev/null +++ b/src/pages/gpu-service/instances/hooks/use-view-logs.ts @@ -0,0 +1,59 @@ +import { currentClusterAtom } from '@/atoms/gpuservice'; +import { getCurrentOrganizationId } from '@/atoms/user'; +import { useAtomValue } from 'jotai'; +import { useState } from 'react'; +import { GPU_SERVICE_INSTANCES_API } from '../apis'; +import { ListItem } from '../config/types'; + +const useViewLogs = () => { + const namespace = getCurrentOrganizationId(); + const currentCluster = useAtomValue(currentClusterAtom); + const clusterID = currentCluster?.id; + + const [openModalStatus, setOpenModalStatus] = useState<{ + open: boolean; + url: string; + tail?: number; + status?: string; + }>({ + open: false, + url: '', + tail: 1000, + status: undefined + }); + + const openModal = (row?: ListItem) => { + const name = row?.metadata?.name; + const rowNamespace = row?.metadata?.namespace || namespace; + setOpenModalStatus({ + open: true, + url: + name && clusterID + ? `${GPU_SERVICE_INSTANCES_API({ + namespace: rowNamespace, + clusterID + })}/${name}/log` + : '', + tail: 1000, + status: row?.status?.phase || undefined + }); + }; + + const closeModal = () => { + setOpenModalStatus({ + open: false, + url: '', + tail: 1000, + status: undefined + }); + }; + + return { + openViewLogsModalStatus: openModalStatus, + setOpenViewLogsModalStatus: setOpenModalStatus, + openViewLogsModal: openModal, + closeViewLogsModal: closeModal + }; +}; + +export default useViewLogs; diff --git a/src/pages/gpu-service/instances/index.tsx b/src/pages/gpu-service/instances/index.tsx index c892f9d6..864b9a16 100644 --- a/src/pages/gpu-service/instances/index.tsx +++ b/src/pages/gpu-service/instances/index.tsx @@ -26,8 +26,10 @@ import { queryGPUServiceInstances } from './apis'; import AddModal from './components/add-modal'; +import ViewLogsModal from './components/view-logs-modal'; import { FormData, ListItem } from './config/types'; import useInstancesColumns from './hooks/use-instances-columns'; +import useViewLogs from './hooks/use-view-logs'; import useCreateInstance from './services/use-create-instance'; import useUpdateInstance from './services/use-update-instance'; @@ -105,6 +107,8 @@ const GPUService: React.FC = () => { const { fetchData: createInstance } = useCreateInstance(); const { fetchData: updateInstance } = useUpdateInstance(); + const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } = + useViewLogs(); const { fetchClusterList, cancelRequest: cancelClusterRequest, @@ -215,6 +219,8 @@ const GPUService: React.FC = () => { name: row.metadata?.name, id: row.metadata?.name as any }); + } else if (val === 'viewlog') { + openViewLogsModal(row); } }); @@ -329,6 +335,12 @@ const GPUService: React.FC = () => { onCancel={closeModal} onOk={handleModalOk} /> + ); diff --git a/src/pages/gpu-service/instances/services/use-query-instance-events.ts b/src/pages/gpu-service/instances/services/use-query-instance-events.ts new file mode 100644 index 00000000..d8ef7166 --- /dev/null +++ b/src/pages/gpu-service/instances/services/use-query-instance-events.ts @@ -0,0 +1,47 @@ +import { currentClusterAtom } from '@/atoms/gpuservice'; +import { getCurrentOrganizationId } from '@/atoms/user'; +import { useQueryData } from '@gpustack/core-ui'; +import { useAtomValue } from 'jotai'; +import { useCallback } from 'react'; +import { queryGPUServiceInstanceEvents } from '../apis'; +import { InstanceEvents } from '../config/types'; + +interface QueryInstanceEventsParams { + name: string; + pretty?: string; +} + +export default function useQueryInstanceEvents() { + const namespace = getCurrentOrganizationId(); + const currentCluster = useAtomValue(currentClusterAtom); + const clusterID = currentCluster?.id; + + const fetchDetail = useCallback( + (params: QueryInstanceEventsParams, options?: any) => + queryGPUServiceInstanceEvents( + { + namespace, + clusterID, + name: params.name, + pretty: params.pretty + }, + options + ), + [namespace, clusterID] + ); + + const { detailData, loading, cancelRequest, fetchData } = useQueryData< + InstanceEvents, + QueryInstanceEventsParams + >({ + fetchDetail, + key: 'instanceEvents' + }); + + return { + detailData, + loading, + cancelRequest, + fetchData + }; +} diff --git a/src/pages/gpu-service/instances/services/use-query-instance-log.ts b/src/pages/gpu-service/instances/services/use-query-instance-log.ts new file mode 100644 index 00000000..5656b6fc --- /dev/null +++ b/src/pages/gpu-service/instances/services/use-query-instance-log.ts @@ -0,0 +1,45 @@ +import { currentClusterAtom } from '@/atoms/gpuservice'; +import { getCurrentOrganizationId } from '@/atoms/user'; +import { useQueryData } from '@gpustack/core-ui'; +import { useAtomValue } from 'jotai'; +import { useCallback } from 'react'; +import { queryGPUServiceInstanceLog } from '../apis'; +import { InstanceLog, InstanceLogQueryParams } from '../config/types'; + +interface QueryInstanceLogParams extends InstanceLogQueryParams { + name: string; +} + +export default function useQueryInstanceLog() { + const namespace = getCurrentOrganizationId(); + const currentCluster = useAtomValue(currentClusterAtom); + const clusterID = currentCluster?.id; + + const fetchDetail = useCallback( + (params: QueryInstanceLogParams, options?: any) => + queryGPUServiceInstanceLog( + { + ...params, + namespace, + clusterID + }, + options + ), + [namespace, clusterID] + ); + + const { detailData, loading, cancelRequest, fetchData } = useQueryData< + InstanceLog, + QueryInstanceLogParams + >({ + fetchDetail, + key: 'instanceLog' + }); + + return { + detailData, + loading, + cancelRequest, + fetchData + }; +}