feat: add instance recreate action
This commit is contained in:
@@ -17,8 +17,27 @@ export const GPU_SERVICE_INSTANCES_API = (params: {
|
||||
|
||||
export const GPU_SERVICE_INSTANCES_TYPE_API = (params: {
|
||||
clusterID?: number;
|
||||
}) =>
|
||||
`/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/instancetypes`;
|
||||
}) => {
|
||||
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/instancetypes`;
|
||||
};
|
||||
|
||||
export const GPU_SERVICE_INSTANCES_LOG_API = (params: {
|
||||
namespace: string;
|
||||
name: string;
|
||||
clusterID?: number;
|
||||
}) => {
|
||||
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/log`;
|
||||
};
|
||||
|
||||
export const GPU_SERVICE_INSTANCES_EVENTS_API = (params: {
|
||||
namespace: string;
|
||||
name: string;
|
||||
clusterID?: number;
|
||||
}) => {
|
||||
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/events`;
|
||||
};
|
||||
|
||||
// =========== Instances ===========
|
||||
|
||||
export async function queryGPUServiceInstances(
|
||||
params: Global.K8sSearchParams & {
|
||||
@@ -147,10 +166,11 @@ export async function queryGPUServiceInstanceEvents(
|
||||
return;
|
||||
}
|
||||
return request<InstanceEvents>(
|
||||
`${GPU_SERVICE_INSTANCES_API({
|
||||
`${GPU_SERVICE_INSTANCES_EVENTS_API({
|
||||
namespace: params.namespace,
|
||||
clusterID: params.clusterID
|
||||
})}/${params.name}/events`,
|
||||
clusterID: params.clusterID,
|
||||
name: params.name
|
||||
})}`,
|
||||
{
|
||||
method: 'GET',
|
||||
params: omitPathParams(_.omit(params, ['name'])),
|
||||
@@ -171,10 +191,11 @@ export async function queryGPUServiceInstanceLog(
|
||||
return;
|
||||
}
|
||||
return request<InstanceLog>(
|
||||
`${GPU_SERVICE_INSTANCES_API({
|
||||
`${GPU_SERVICE_INSTANCES_LOG_API({
|
||||
namespace: params.namespace,
|
||||
clusterID: params.clusterID
|
||||
})}/${params.name}/log`,
|
||||
clusterID: params.clusterID,
|
||||
name: params.name
|
||||
})}`,
|
||||
{
|
||||
method: 'GET',
|
||||
params: omitPathParams(_.omit(params, ['name'])),
|
||||
|
||||
@@ -135,7 +135,7 @@ const InstanceTypeItem: React.FC<InstanceTypeItemProps> = ({
|
||||
<>
|
||||
<span className="dot"></span>
|
||||
<span className="meta-item">
|
||||
<IconFont type="icon-cube" className="meta-icon" />
|
||||
<IconFont type="icon-database" className="meta-icon" />
|
||||
<Flex align="center" gap={4}>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { createAxiosToken } from '@/hooks/use-chunk-request';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
AlertBlockInfo,
|
||||
AutoTooltip,
|
||||
ScrollerModal,
|
||||
StatusTag
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Table } from 'antd';
|
||||
import type { ColumnsType } from 'antd/lib/table';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { queryGPUServiceInstanceEvents } from '../apis';
|
||||
import { InstanceEventItem } from '../config/types';
|
||||
|
||||
type ViewEventsModalProps = {
|
||||
open: boolean;
|
||||
name: string;
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const formatRelative = (text?: string) => (text ? dayjs(text).fromNow() : '-');
|
||||
|
||||
const eventTypeStatus: Record<string, 'success' | 'warning' | 'error'> = {
|
||||
Normal: 'success',
|
||||
Warning: 'warning'
|
||||
};
|
||||
|
||||
const ViewEventsModal: React.FC<ViewEventsModalProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const { open, onCancel, name, namespace, clusterID } = props || {};
|
||||
const [events, setEvents] = useState<InstanceEventItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const tokenRef = useRef<ReturnType<typeof createAxiosToken> | null>(null);
|
||||
|
||||
const fetchEvents = useCallback(async () => {
|
||||
if (!name || !clusterID || !namespace) return;
|
||||
tokenRef.current?.cancel?.();
|
||||
const source = createAxiosToken();
|
||||
tokenRef.current = source;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await queryGPUServiceInstanceEvents(
|
||||
{ name, namespace, clusterID },
|
||||
{ token: source.token }
|
||||
);
|
||||
setEvents(res?.items ?? []);
|
||||
} catch (e) {
|
||||
// request cancellation or network failure — keep prior items
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [name, namespace, clusterID]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
fetchEvents();
|
||||
} else {
|
||||
tokenRef.current?.cancel?.();
|
||||
setEvents([]);
|
||||
}
|
||||
return () => {
|
||||
tokenRef.current?.cancel?.();
|
||||
};
|
||||
}, [open, fetchEvents]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
tokenRef.current?.cancel?.();
|
||||
onCancel();
|
||||
}, [onCancel]);
|
||||
|
||||
const columns: ColumnsType<InstanceEventItem> = [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.type' }),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 110,
|
||||
render: (value: string) => (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: eventTypeStatus[value] ?? 'inactive',
|
||||
text: value || '-'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'gpuservice.instance.event.reason' }),
|
||||
dataIndex: 'reason',
|
||||
key: 'reason',
|
||||
width: 180,
|
||||
ellipsis: { showTitle: false },
|
||||
render: (text: string) => (
|
||||
<AutoTooltip ghost style={{ maxWidth: 180 }}>
|
||||
{text || '-'}
|
||||
</AutoTooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'gpuservice.instance.event.message' }),
|
||||
dataIndex: 'message',
|
||||
key: 'message',
|
||||
ellipsis: { showTitle: false },
|
||||
render: (text: string) => <AutoTooltip ghost>{text || '-'}</AutoTooltip>
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'gpuservice.instance.event.source' }),
|
||||
key: 'source',
|
||||
width: 200,
|
||||
ellipsis: { showTitle: false },
|
||||
render: (_text, record) => {
|
||||
const src =
|
||||
record.source?.component ||
|
||||
record.reportingComponent ||
|
||||
record.reportingInstance ||
|
||||
'-';
|
||||
return (
|
||||
<AutoTooltip ghost style={{ maxWidth: 200 }}>
|
||||
{src}
|
||||
</AutoTooltip>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'gpuservice.instance.event.count' }),
|
||||
dataIndex: 'count',
|
||||
key: 'count',
|
||||
width: 80,
|
||||
render: (value?: number) => value ?? '-'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'gpuservice.instance.event.lastSeen' }),
|
||||
key: 'lastTimestamp',
|
||||
width: 180,
|
||||
render: (_text, record) =>
|
||||
formatRelative(
|
||||
record.lastTimestamp ||
|
||||
record.series?.lastObservedTime ||
|
||||
record.eventTime
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ScrollerModal
|
||||
title={
|
||||
<span className="flex flex-center" style={{ gap: 8 }}>
|
||||
<span style={{ fontWeight: 'var(--font-weight-bold)' }}>
|
||||
{intl.formatMessage({ id: 'common.button.viewevent' })}
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchEvents}
|
||||
loading={loading}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
zIndex={3000}
|
||||
open={open}
|
||||
centered={true}
|
||||
onCancel={handleCancel}
|
||||
destroyOnHidden={true}
|
||||
closeIcon={true}
|
||||
mask={{
|
||||
closable: false
|
||||
}}
|
||||
keyboard={true}
|
||||
styles={{
|
||||
wrapper: {
|
||||
borderRadius: 0
|
||||
}
|
||||
}}
|
||||
width="1000px"
|
||||
maxContentHeight="100vh"
|
||||
footer={null}
|
||||
>
|
||||
<div style={{ marginBlock: '8px 16px' }}>
|
||||
<AlertBlockInfo
|
||||
message={intl.formatMessage({
|
||||
id: 'gpuservice.instance.event.recentHourTip'
|
||||
})}
|
||||
type="warning"
|
||||
contentStyle={{ paddingInline: 0 }}
|
||||
></AlertBlockInfo>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
className={'scroll-table'}
|
||||
tableLayout={'auto'}
|
||||
style={{ width: '100%', minHeight: 400 }}
|
||||
dataSource={events || []}
|
||||
rowKey={(record) =>
|
||||
`${record.metadata?.name}-${record.metadata.namespace}`
|
||||
}
|
||||
loading={{
|
||||
spinning: loading,
|
||||
size: 'middle'
|
||||
}}
|
||||
virtual
|
||||
scroll={{ y: 'calc(100vh - 80px)' }}
|
||||
pagination={false}
|
||||
></Table>
|
||||
</ScrollerModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewEventsModal;
|
||||
@@ -95,6 +95,9 @@ const ViewLogsModal: React.FC<ViewModalProps> = (props) => {
|
||||
enableScorllLoad={true}
|
||||
isDownloading={false}
|
||||
params={{
|
||||
watchable: false,
|
||||
watch: false,
|
||||
tailLines: 1000,
|
||||
follow: true
|
||||
}}
|
||||
></LogsViewer>
|
||||
|
||||
@@ -52,12 +52,24 @@ export const rowActionList = [
|
||||
locale: true,
|
||||
icon: React.createElement(IconFont, { type: 'icon-logs' })
|
||||
},
|
||||
{
|
||||
label: 'common.button.viewevent',
|
||||
key: 'viewevent',
|
||||
locale: true,
|
||||
icon: icons.ProfileOutlined
|
||||
},
|
||||
{
|
||||
label: 'common.button.edit',
|
||||
key: 'edit',
|
||||
locale: true,
|
||||
icon: icons.EditOutlined
|
||||
},
|
||||
{
|
||||
label: 'common.button.recreate',
|
||||
key: 'recreate',
|
||||
locale: true,
|
||||
icon: icons.RetweetOutlined
|
||||
},
|
||||
{
|
||||
label: 'common.button.delete',
|
||||
key: 'delete',
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface InstanceStatus {
|
||||
}[];
|
||||
ports?: {
|
||||
port: number;
|
||||
name: string;
|
||||
nodePort?: number;
|
||||
protocol?: InstanceServicePortProtocol;
|
||||
}[];
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { validateLabelNameRegxFor63 } from '@/config';
|
||||
import { PageAction, validateLabelNameRegxFor63 } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { Input as CInput } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import { FormData } from '../config/types';
|
||||
|
||||
const Basic = () => {
|
||||
const Basic = ({ action }: { action: PageActionType }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<>
|
||||
@@ -25,6 +26,7 @@ const Basic = () => {
|
||||
]}
|
||||
>
|
||||
<CInput.Input
|
||||
disabled={action === PageAction.EDIT}
|
||||
label={intl.formatMessage({ id: 'gpuservice.instance.name' })}
|
||||
required
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Form } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
@@ -50,7 +51,7 @@ const parseQuantityToNumber = (value?: string): number | null => {
|
||||
const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value));
|
||||
if (!match) return null;
|
||||
const num = Number(match[1]);
|
||||
return Number.isFinite(num) && num > 0 ? num : null;
|
||||
return Number.isFinite(num) && num > 0 ? _.floor(num, 1) : null;
|
||||
};
|
||||
|
||||
const TABKeysMap = {
|
||||
@@ -92,7 +93,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm<InstanceFormValues>();
|
||||
const scrollTabsRef = useRef<any>(null);
|
||||
const { detailData, fetchData } = useGetSshkey();
|
||||
const { detailData: sshKeyData, fetchData: fetchSSHData } = useGetSshkey();
|
||||
const { getScrollElementScrollableHeight } = useWrapperContext();
|
||||
const {
|
||||
activeKey,
|
||||
@@ -111,9 +112,9 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
fetchData({});
|
||||
fetchSSHData({});
|
||||
}
|
||||
}, [open]);
|
||||
}, [open, action]);
|
||||
|
||||
const segmentOptions = useMemo(
|
||||
() => [
|
||||
@@ -223,7 +224,13 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
}, [action, currentData, form, open, namespace]);
|
||||
|
||||
const handleFinish = async (values: InstanceFormValues) => {
|
||||
await onFinish(values);
|
||||
await onFinish({
|
||||
...values,
|
||||
spec: {
|
||||
...values.spec,
|
||||
sshPublicKey: { name: sshKeyData?.name }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -272,7 +279,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
accelerator: '1'
|
||||
},
|
||||
sshPublicKey: {
|
||||
name: 'gpustack-organization-ssh-public-key'
|
||||
name: 'gpustack-ssh-public-key'
|
||||
},
|
||||
volume: {
|
||||
ephemeral: {
|
||||
@@ -286,7 +293,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
enable_ssh: false
|
||||
}}
|
||||
>
|
||||
<Basic />
|
||||
<Basic action={action} />
|
||||
<CollapsePanel
|
||||
activeKey={collapseKeys}
|
||||
accordion={false}
|
||||
@@ -309,6 +316,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
children: (
|
||||
<TemplateBasicForm
|
||||
page="instance"
|
||||
disabled={action === PageAction.EDIT}
|
||||
onceMaxRequest={onceMaxRequest}
|
||||
/>
|
||||
)
|
||||
@@ -319,7 +327,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
id: 'gpuservice.instance.section.storage'
|
||||
}),
|
||||
forceRender: true,
|
||||
children: <StorageVolume />
|
||||
children: (
|
||||
<StorageVolume
|
||||
disabled={action === PageAction.EDIT}
|
||||
action={action}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { PageAction } from '@/config';
|
||||
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Flex, Form, Radio } from 'antd';
|
||||
@@ -19,7 +20,13 @@ const FieldBlock = styled.div`
|
||||
|
||||
const DEFAULT_TEMP_CAPACITY_GB = 50;
|
||||
|
||||
const StorageVolume = () => {
|
||||
const StorageVolume = ({
|
||||
disabled,
|
||||
action
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
action: PageActionType;
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { fetchData: createStorage } = useCreateStorage();
|
||||
const { detailData: storageData, fetchData: fetchStorage } =
|
||||
@@ -82,6 +89,7 @@ const StorageVolume = () => {
|
||||
}}
|
||||
>
|
||||
<Radio.Group
|
||||
disabled={disabled}
|
||||
style={{ marginBottom: 12 }}
|
||||
value={storageMode}
|
||||
onChange={(e) => handleModeChange(e.target.value)}
|
||||
@@ -98,14 +106,16 @@ const StorageVolume = () => {
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ marginBottom: 6 }}
|
||||
onClick={() => setOverlayOpen(true)}
|
||||
>
|
||||
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
||||
</Button>
|
||||
{action === PageAction.CREATE && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ marginBottom: 6 }}
|
||||
onClick={() => setOverlayOpen(true)}
|
||||
>
|
||||
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{storageMode === StorageModeValueMap.Existing && (
|
||||
@@ -121,6 +131,7 @@ const StorageVolume = () => {
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
label={intl.formatMessage({
|
||||
id: 'gpuservice.storage.persistentVolume'
|
||||
})}
|
||||
@@ -149,6 +160,7 @@ const StorageVolume = () => {
|
||||
]}
|
||||
>
|
||||
<CInputNumber
|
||||
disabled={disabled}
|
||||
min={1}
|
||||
precision={0}
|
||||
label={intl.formatMessage({
|
||||
|
||||
@@ -9,13 +9,27 @@ import { useIntl } from '@umijs/max';
|
||||
import { Button, Flex } from 'antd';
|
||||
import type { ColumnsType } from 'antd/lib/table';
|
||||
import dayjs from 'dayjs';
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { InstanceStatusLabelMap, rowActionList, status } from '../config';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
type ConnectEntry =
|
||||
| { type: 'ssh'; key: string; command: string }
|
||||
| { type: 'http'; key: string; port: number; url: string };
|
||||
| {
|
||||
type: 'ssh';
|
||||
key: string;
|
||||
command: string;
|
||||
name: string;
|
||||
protocol: string;
|
||||
}
|
||||
| {
|
||||
type: 'http';
|
||||
key: string;
|
||||
port: number;
|
||||
url: string;
|
||||
name: string;
|
||||
protocol: string;
|
||||
};
|
||||
|
||||
const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
||||
const ip = record.status?.hostIPs?.[0]?.ip;
|
||||
@@ -33,13 +47,17 @@ const getConnectEntries = (record: ListItem): ConnectEntry[] => {
|
||||
return isSsh
|
||||
? {
|
||||
type: 'ssh',
|
||||
name: 'SSH',
|
||||
key: `ssh-${p.nodePort}`,
|
||||
protocol: _.toUpper(p.protocol),
|
||||
command: `ssh root@${ip} -p ${p.nodePort}`
|
||||
}
|
||||
: {
|
||||
type: 'http',
|
||||
name: p.name || 'HTTP',
|
||||
key: `http-${p.nodePort}`,
|
||||
port: p.port,
|
||||
protocol: _.toUpper(p.protocol),
|
||||
url: `http://${ip}:${p.nodePort}`
|
||||
};
|
||||
});
|
||||
@@ -116,7 +134,7 @@ const useInstancesColumns = ({
|
||||
className="text-tertiary"
|
||||
style={{ fontSize: 12, width: 34, flexShrink: 0 }}
|
||||
>
|
||||
HTTP
|
||||
{entry.name || entry.protocol}
|
||||
</span>
|
||||
<Button
|
||||
type="link"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
const useViewEvents = () => {
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
|
||||
|
||||
const [openModalStatus, setOpenModalStatus] = useState<{
|
||||
open: boolean;
|
||||
name: string;
|
||||
namespace: string;
|
||||
clusterID?: number;
|
||||
}>({
|
||||
open: false,
|
||||
name: '',
|
||||
namespace: '',
|
||||
clusterID: undefined
|
||||
});
|
||||
|
||||
const openModal = (row?: ListItem) => {
|
||||
const name = row?.metadata?.name || '';
|
||||
const rowNamespace = row?.metadata?.namespace || namespace;
|
||||
setOpenModalStatus({
|
||||
open: true,
|
||||
name,
|
||||
namespace: rowNamespace,
|
||||
clusterID
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setOpenModalStatus({
|
||||
open: false,
|
||||
name: '',
|
||||
namespace: '',
|
||||
clusterID: undefined
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
openViewEventsModalStatus: openModalStatus,
|
||||
openViewEventsModal: openModal,
|
||||
closeViewEventsModal: closeModal
|
||||
};
|
||||
};
|
||||
|
||||
export default useViewEvents;
|
||||
@@ -2,7 +2,7 @@ import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
import { GPU_SERVICE_INSTANCES_API } from '../apis';
|
||||
import { GPU_SERVICE_INSTANCES_LOG_API } from '../apis';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
const useViewLogs = () => {
|
||||
@@ -29,10 +29,11 @@ const useViewLogs = () => {
|
||||
open: true,
|
||||
url:
|
||||
name && clusterID
|
||||
? `${GPU_SERVICE_INSTANCES_API({
|
||||
? `${GPU_SERVICE_INSTANCES_LOG_API({
|
||||
namespace: rowNamespace,
|
||||
clusterID
|
||||
})}/${name}/log`
|
||||
clusterID,
|
||||
name
|
||||
})}`
|
||||
: '',
|
||||
tail: 1000,
|
||||
status: row?.status?.phase || undefined
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { ConfigProvider, Divider, Flex, message, Table } from 'antd';
|
||||
import { ConfigProvider, Divider, Flex, message, Modal, Table } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import _ from 'lodash';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
@@ -26,15 +26,18 @@ import {
|
||||
queryGPUServiceInstances
|
||||
} from './apis';
|
||||
import AddModal from './components/add-modal';
|
||||
import ViewEventsModal from './components/view-events-modal';
|
||||
import ViewLogsModal from './components/view-logs-modal';
|
||||
import { FormData, ListItem } from './config/types';
|
||||
import useInstancesColumns from './hooks/use-instances-columns';
|
||||
import useViewEvents from './hooks/use-view-events';
|
||||
import useViewLogs from './hooks/use-view-logs';
|
||||
import useCreateInstance from './services/use-create-instance';
|
||||
import useUpdateInstance from './services/use-update-instance';
|
||||
|
||||
const GPUService: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
|
||||
const clusterID = currentCluster?.id;
|
||||
// In admin "All" view there's no Org context, so the helper falls
|
||||
@@ -67,10 +70,10 @@ const GPUService: React.FC = () => {
|
||||
{ ...params, namespace, clusterID: effectiveClusterID },
|
||||
options
|
||||
);
|
||||
const total = res.items?.length ?? 0;
|
||||
const total = res?.items?.length ?? 0;
|
||||
const perPage = params.perPage || 10;
|
||||
return {
|
||||
items: res.items ?? [],
|
||||
items: res?.items ?? [],
|
||||
pagination: {
|
||||
total,
|
||||
totalPage: Math.ceil(total / perPage),
|
||||
@@ -109,6 +112,11 @@ const GPUService: React.FC = () => {
|
||||
const { fetchData: updateInstance } = useUpdateInstance();
|
||||
const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } =
|
||||
useViewLogs();
|
||||
const {
|
||||
openViewEventsModal,
|
||||
closeViewEventsModal,
|
||||
openViewEventsModalStatus
|
||||
} = useViewEvents();
|
||||
const {
|
||||
fetchClusterList,
|
||||
cancelRequest: cancelClusterRequest,
|
||||
@@ -210,6 +218,36 @@ const GPUService: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRecreate = useMemoizedFn((row: ListItem) => {
|
||||
modalRef.current?.show({
|
||||
title: intl.formatMessage({
|
||||
id: 'gpuservice.instance.recreate.confirm.title'
|
||||
}),
|
||||
content: 'gpuservice.instance.recreate.confirm.content',
|
||||
okText: 'common.button.recreate',
|
||||
operation: 'gpuservice.instance.recreate.confirm.content',
|
||||
name: row.metadata?.name,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteInstance(row.metadata?.name as any);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
await createInstance({
|
||||
data: {
|
||||
metadata: { name: row.metadata.name, namespace },
|
||||
spec: row.spec
|
||||
} as FormData
|
||||
});
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
handleSearch();
|
||||
} catch (error) {
|
||||
message.error(intl.formatMessage({ id: 'common.message.fail' }));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
|
||||
if (val === 'edit') {
|
||||
handleEditInstance(row);
|
||||
@@ -219,8 +257,12 @@ const GPUService: React.FC = () => {
|
||||
name: row.metadata?.name,
|
||||
id: row.metadata?.name as any
|
||||
});
|
||||
} else if (val === 'recreate') {
|
||||
handleRecreate(row);
|
||||
} else if (val === 'viewlog') {
|
||||
openViewLogsModal(row);
|
||||
} else if (val === 'viewevent') {
|
||||
openViewEventsModal(row);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -316,15 +358,7 @@ const GPUService: React.FC = () => {
|
||||
showSorterTooltip={false}
|
||||
rowKey={(record) => record.metadata.name}
|
||||
onChange={handleTableChange}
|
||||
pagination={{
|
||||
size: 'middle',
|
||||
showSizeChanger: true,
|
||||
pageSize: queryParams.perPage,
|
||||
current: queryParams.page,
|
||||
total: dataSource.total,
|
||||
hideOnSinglePage: queryParams.perPage === 10,
|
||||
onChange: handlePageChange
|
||||
}}
|
||||
pagination={false}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
<AddModal
|
||||
@@ -341,7 +375,15 @@ const GPUService: React.FC = () => {
|
||||
tail={openViewLogsModalStatus.tail}
|
||||
onCancel={closeViewLogsModal}
|
||||
/>
|
||||
<ViewEventsModal
|
||||
open={openViewEventsModalStatus.open}
|
||||
name={openViewEventsModalStatus.name}
|
||||
namespace={openViewEventsModalStatus.namespace}
|
||||
clusterID={openViewEventsModalStatus.clusterID}
|
||||
onCancel={closeViewEventsModal}
|
||||
/>
|
||||
<DeleteModal ref={modalRef} />
|
||||
{modalContextHolder}
|
||||
</PageContainerInner>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { currentClusterAtom } from '@/atoms/gpuservice';
|
||||
import { getCurrentOrganizationId } from '@/atoms/user';
|
||||
import { getCurrentOrgNamespace } from '@/atoms/user';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -11,8 +11,8 @@ interface QueryInstanceLogParams extends InstanceLogQueryParams {
|
||||
}
|
||||
|
||||
export default function useQueryInstanceLog() {
|
||||
const namespace = getCurrentOrganizationId();
|
||||
const currentCluster = useAtomValue(currentClusterAtom);
|
||||
const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id);
|
||||
const clusterID = currentCluster?.id;
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user