import useSetChunkRequest from '@/hooks/use-chunk-request'; import { CloseOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { BaseSelect, LogsViewer } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Button, Checkbox, Flex, Modal, Tooltip } from 'antd'; import dayjs from 'dayjs'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { MODELS_API } from '../apis'; import { InstanceRealtimeLogStatus, InstanceStatusMap } from '../config'; import useQueryModelInstanceRestartCount from '../services/use-query-instance-restart-count'; type ViewModalProps = { open: boolean; url: string; id?: number; modelId?: number | string; tail?: number; status?: string; onCancel: () => void; }; const ViewLogsModal: React.FC = (props) => { const intl = useIntl(); const { setChunkRequest } = useSetChunkRequest(); const { open, url, onCancel, tail, status } = props || {}; const [enableScorllLoad, setEnableScorllLoad] = useState(true); const [isDownloading, setIsDownloading] = useState( status === InstanceStatusMap.Downloading ); const [selectedContainer, setSelectedContainer] = useState( null ); const [params, setParams] = useState({ follow: true }); const [showPrevious, setShowPrevious] = useState(false); const logsViewerRef = React.useRef(null); const { countOptions, fetchData, cancelRequest } = useQueryModelInstanceRestartCount(); const requestRef = React.useRef(null); const contentRef = React.useRef(null); const currentWorkerCountList = useMemo(() => { return ( countOptions?.find((option) => option.value === selectedContainer) ?.children || [] ); }, [countOptions, selectedContainer]); const filteredCountOptions = useMemo(() => { if (!showPrevious) return countOptions; return countOptions?.filter((option) => option.children?.some((child) => child.previous) ); }, [countOptions, showPrevious]); const handleCancel = useCallback(() => { logsViewerRef.current?.abort(); onCancel(); }, [onCancel]); const updateHandler = (list: any) => { const data = list?.find((item: any) => item.data?.id === props.id); // state in InstanceRealtimeLogStatus will not enable scorll load, because it is in the trasisition state if (data) { setIsDownloading(data?.data?.state === InstanceStatusMap.Downloading); setEnableScorllLoad( () => !InstanceRealtimeLogStatus.includes(data?.data?.state) ); } }; 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]); const cancelOnClose = () => { logsViewerRef.current?.abort(); requestRef.current?.current?.cancel?.(); setSelectedContainer(null); setShowPrevious(false); cancelRequest(); }; const handleOnChange = (option: any) => { if (!option) { setParams({ follow: true }); } else { setParams({ follow: true, watch: !option.previous, previous: option.previous, worker_id: option.worker_id, container: option.container }); } setSelectedContainer(option?.parentValue || null); }; const handleContainerChange = (value: string, option: any) => { setSelectedContainer(value); setShowPrevious(false); const counts = option?.children || []; const lastItem = counts.find((item: any) => !item.previous); if (lastItem) { handleOnChange(lastItem); } }; const showCascader = countOptions?.some((option) => (option.children?.length ?? 0) >= 2) || countOptions?.length > 1; const handleOnChecked = (e: any) => { const checked = e.target.checked; setShowPrevious(checked); if (checked) { const currentPrev = currentWorkerCountList.find((item) => item.previous); if (currentPrev) { handleOnChange(currentPrev); return; } const mainOptionWithPrev = countOptions?.find( (option) => option.isMain && option.children?.some((child) => child.previous) ); const prevChild = mainOptionWithPrev?.children?.find( (child) => child.previous ); if (prevChild) { handleOnChange(prevChild); } return; } const currentItem = currentWorkerCountList.find((item) => !item.previous); handleOnChange(currentItem); }; const optionRender = (option: any) => { const optionData = option?.data || option; const [container, worker] = optionData.label.split(':'); const startAt = optionData.start_at ? dayjs(optionData.start_at).format('YYYY-MM-DD HH:mm:ss') : ''; return ( {container} [{worker}] {startAt && {startAt}} ); }; const renderTitle = () => { return ( {intl.formatMessage({ id: 'common.button.viewlog' })} {showCascader && (
{intl.formatMessage({ id: 'models.instance.previousRun' })} : {intl.formatMessage({ id: 'models.instance.startHistory.tips' })} } > {intl.formatMessage({ id: 'models.instance.previousRun' })} {intl.formatMessage({ id: 'resources.container' })} } options={filteredCountOptions} value={selectedContainer || undefined} labelRender={optionRender} optionRender={optionRender} onChange={handleContainerChange} style={{ width: 360 }} >
)}
); }; useEffect(() => { if (open && props.id) { requestRef.current?.current?.cancel?.(); requestRef.current = setChunkRequest({ url: `${MODELS_API}/${props.modelId}/instances`, handler: updateHandler }); fetchData(props.id).then((list) => { const lastItem = list.find((item) => item.isMain); if (lastItem) { handleOnChange(lastItem.children?.[0]); } }); } else { cancelOnClose(); } return () => { cancelOnClose(); }; }, [props.id, open]); return (
); }; export default ViewLogsModal;