diff --git a/src/hooks/use-download-logs.tsx b/src/hooks/use-download-logs.tsx new file mode 100644 index 00000000..7b5cb90a --- /dev/null +++ b/src/hooks/use-download-logs.tsx @@ -0,0 +1,71 @@ +import { HandlerOptions } from '@/hooks/use-chunk-fetch'; +import useDownloadStream from '@/hooks/use-download-stream'; +import { useIntl } from '@umijs/max'; +import { Progress, notification } from 'antd'; +import dayjs from 'dayjs'; + +const renderMessage = (title: string) => { + return ( +
+ {title} +
+ ); +}; + +const createFileName = (name: string) => { + const timestamp = dayjs().format('YYYY-MM-DD_HH-mm-ss'); + const fileName = `${name}_${timestamp}.txt`; + return fileName; +}; + +const useDownloadLogs = () => { + const { downloadStream } = useDownloadStream(); + const intl = useIntl(); + const [api, contextHolder] = notification.useNotification({ + stack: { threshold: 1 } + }); + + const downloadNotification = ( + data: HandlerOptions & { + filename: string; + duration?: number; + chunkRequestRef: any; + } + ) => { + api.open({ + duration: data.duration, + message: renderMessage(data.filename), + key: data.filename, + closeIcon: ( + {intl.formatMessage({ id: 'common.button.cancel' })} + ), + description: , + onClose() { + data.chunkRequestRef?.current?.abort(); + notification.destroy?.(data.filename); + } + }); + }; + + const handleDownloadLog = async (params: { url: string; name: string }) => { + downloadStream({ + url: params.url, + filename: createFileName(params.name), + downloadNotification + }); + }; + + return { + onDownloadLog: handleDownloadLog, + contextHolder + }; +}; + +export default useDownloadLogs; diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index a600da2f..d1bd399f 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -229,7 +229,7 @@ export default { 'common.options.none': 'None', 'common.options.auto': 'Auto', 'common.search.empty': 'No matching results found.', - 'common.button.downloadLog': 'Download Log', + 'common.button.downloadLog': 'Download Logs', 'common.button.faq': 'FAQ', 'common.button.moreInfo': 'More Info', 'common.text.warning': 'Warning', diff --git a/src/pages/benchmark/components/compare-conditions.tsx b/src/pages/benchmark/components/compare-conditions.tsx deleted file mode 100644 index cf7e98c0..00000000 --- a/src/pages/benchmark/components/compare-conditions.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import Wrapper from '@/components/label-selector/wrapper'; -import { DeleteOutlined } from '@ant-design/icons'; -import { useHover } from 'ahooks'; -import { Button, Divider, Flex, Input, Popover } from 'antd'; -import React, { useRef, useState } from 'react'; -import styled from 'styled-components'; - -const ItemContainer = styled.div` - display: flex; - margin-bottom: 12px; - align-items: center; - justify-content: flex-start; - - .seprator { - display: flex; - flex: none; - width: 12px; - align-items: center; - justify-content: center; - color: var(--ant-color-text-tertiary); - } - - .btn { - width: 24px; - margin-left: 10px; - flex: none; - } -`; - -interface MetadataListProps { - dataList: any[]; - label: React.ReactNode; - description?: React.ReactNode; - disabled?: boolean; - btnText?: string; - onAdd?: () => void; - onDelete?: (index: number, item: any) => void; - children?: (item: any, index: number) => React.ReactNode; -} - -const MetadataList: React.FC = ({ - dataList, - label, - description, - disabled, - btnText, - children, - onDelete, - onAdd -}) => { - return ( - - {dataList.map((item, index) => ( - {children?.(item, index)} - ))} - - ); -}; - -const DeleteWrapper = styled.span` - cursor: pointer; - color: var(--ant-color-text-tertiary); - &:hover { - color: var(--ant-color-error-text-hover); - } -`; - -const Box = styled.div` - width: 100%; - .ant-input-suffix { - opacity: 0; - transition: opacity 0.2s ease-in-out; - } - &:hover { - .ant-input-suffix { - opacity: 1; - transition: opacity 0.2s ease-in-out; - } - } -`; - -const Container = styled.div` - display: flex; - align-items: center; - border: 1px solid var(--ant-color-border); - border-radius: 4px; - padding: 8.5px 11px; - height: 40px; - width: 240px; - cursor: pointer; - &:hover { - border-color: var(--ant-color-primary-hover); - } - &:active { - box-shadow: var(--ant-input-active-shadow); - } - .holder { - color: var(--ant-color-text-placeholder); - } -`; -const CompareConditions: React.FC = () => { - const [open, setOpen] = useState(false); - const [filters, setFilters] = useState([]); - const containerRef = useRef(null); - const isHovering = useHover(containerRef); - - const onAdd = () => { - setFilters((prev) => [...prev, '']); - }; - - const onDelete = (index: number, item: any) => { - setFilters((prev) => prev.filter((_, i) => i !== index)); - }; - - const handleOnChange = (value: string, index: number) => { - const newFilters = [...filters]; - newFilters[index] = value; - setFilters(newFilters); - }; - - const FiltersDrop = () => { - return ( - <> - - {(item, index) => ( - - 1 ? ( - - onDelete?.(index, item)} /> - - ) : null - } - onChange={(e) => handleOnChange(e.target.value, index)} - /> - - )} - - - - - - - - ); - }; - - return ( - - - Export filter - - - ); -}; - -export default CompareConditions; diff --git a/src/pages/benchmark/components/detail-content.tsx b/src/pages/benchmark/components/detail-content.tsx index 4c050980..160be161 100644 --- a/src/pages/benchmark/components/detail-content.tsx +++ b/src/pages/benchmark/components/detail-content.tsx @@ -8,7 +8,6 @@ import { BenchmarkListItem } from '../config/types'; import useQueryDetail from '../services/use-query-detail'; import Configure from './configure'; import Environment from './environment'; -import Logs from './logs'; import Summary from './summary'; const Details: React.FC<{ currentData?: BenchmarkListItem }> = ({ @@ -43,13 +42,13 @@ const Details: React.FC<{ currentData?: BenchmarkListItem }> = ({ label: intl.formatMessage({ id: 'benchmark.detail.environment.title' }), children: , icon: - }, - { - key: 'logs', - label: intl.formatMessage({ id: 'benchmark.detail.logs.title' }), - children: , - icon: } + // { + // key: 'logs', + // label: intl.formatMessage({ id: 'benchmark.detail.logs.title' }), + // children: , + // icon: + // } ]; const handleChangeTab = (key: string) => { diff --git a/src/pages/benchmark/components/row-actions.tsx b/src/pages/benchmark/components/row-actions.tsx new file mode 100644 index 00000000..d0846160 --- /dev/null +++ b/src/pages/benchmark/components/row-actions.tsx @@ -0,0 +1,88 @@ +import DropdownButtons from '@/components/drop-down-buttons'; +import IconFont from '@/components/icon-font'; +import icons from '@/components/icon-font/icons'; +import useDownloadLogs from '@/hooks/use-download-logs'; +import { DownloadOutlined } from '@ant-design/icons'; +import { BENCHMARKS_API } from '../apis'; +import { BenchmarkStatusValueMap } from '../config'; +import { BenchmarkListItem as ListItem } from '../config/types'; + +const actionList = [ + { + key: 'edit', + label: 'common.button.edit', + icon: icons.EditOutlined + }, + { + label: 'common.button.viewlog', + key: 'viewlog', + status: [ + BenchmarkStatusValueMap.Claimed, + BenchmarkStatusValueMap.Running, + BenchmarkStatusValueMap.Error, + BenchmarkStatusValueMap.Completed + ], + icon: + }, + { + label: 'common.button.downloadLog', + key: 'download', + status: [ + BenchmarkStatusValueMap.Claimed, + BenchmarkStatusValueMap.Running, + BenchmarkStatusValueMap.Error, + BenchmarkStatusValueMap.Completed + ], + icon: + }, + { + key: 'delete', + label: 'common.button.delete', + icon: icons.DeleteOutlined, + props: { + danger: true + } + } +]; + +interface RowActionsProps { + record: ListItem; + handleSelect: (key: string, record: ListItem) => void; +} + +const RowActions: React.FC = (props) => { + const { record, handleSelect } = props; + const { onDownloadLog, contextHolder } = useDownloadLogs(); + + const actions = actionList.filter((action) => { + if (action.key === 'viewlog' || action.key === 'download') { + return action.status?.includes(record.state); + } + + return true; + }); + + const handleDownloadLog = async () => { + onDownloadLog({ + url: `${BENCHMARKS_API}/${record.id}/logs`, + name: record.name + }); + }; + + const onSelect = (val: string) => { + if (val === 'download') { + handleDownloadLog(); + return; + } + handleSelect(val, record); + }; + + return ( + <> + {contextHolder} + + + ); +}; + +export default RowActions; diff --git a/src/pages/benchmark/components/view-logs-modal.tsx b/src/pages/benchmark/components/view-logs-modal.tsx new file mode 100644 index 00000000..90b960d4 --- /dev/null +++ b/src/pages/benchmark/components/view-logs-modal.tsx @@ -0,0 +1,104 @@ +import LogsViewer from '@/components/logs-viewer/virtual-log-list'; +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, tail, url, status } = 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} + maskClosable={false} + keyboard={true} + styles={{ + wrapper: { + borderRadius: 0 + } + }} + width="100%" + footer={null} + > +
+ +
+
+ ); +}; + +export default ViewLogsModal; diff --git a/src/pages/benchmark/hooks/use-benchmark-columns.tsx b/src/pages/benchmark/hooks/use-benchmark-columns.tsx index de3b34b7..64fb5eb4 100644 --- a/src/pages/benchmark/hooks/use-benchmark-columns.tsx +++ b/src/pages/benchmark/hooks/use-benchmark-columns.tsx @@ -1,7 +1,5 @@ // columns.ts import AutoTooltip from '@/components/auto-tooltip'; -import DropdownButtons from '@/components/drop-down-buttons'; -import icons from '@/components/icon-font/icons'; import StatusTag from '@/components/status-tag'; import { tableSorter } from '@/config/settings'; import { useIntl } from '@umijs/max'; @@ -10,25 +8,10 @@ import { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; import _ from 'lodash'; import { useMemo } from 'react'; +import RowActions from '../components/row-actions'; import { BenchmarkStatus, BenchmarkStatusLabelMap } from '../config'; import { BenchmarkListItem as ListItem } from '../config/types'; -const actionList = [ - { - key: 'edit', - label: 'common.button.edit', - icon: icons.EditOutlined - }, - { - key: 'delete', - label: 'common.button.delete', - icon: icons.DeleteOutlined, - props: { - danger: true - } - } -]; - const useBenchmarkColumns = ( sortOrder: string[], handleSelect: (val: string, record: ListItem) => void, @@ -174,10 +157,7 @@ const useBenchmarkColumns = ( showTitle: false }, render: (value: string, record: ListItem) => ( - handleSelect(val, record)} - > + ) } ]; diff --git a/src/pages/benchmark/hooks/use-view-logs.ts b/src/pages/benchmark/hooks/use-view-logs.ts new file mode 100644 index 00000000..91e02ae4 --- /dev/null +++ b/src/pages/benchmark/hooks/use-view-logs.ts @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import { BENCHMARKS_API } from '../apis'; +import { BenchmarkListItem as ListItem } from '../config/types'; + +const useViewLogs = () => { + const [openModalStatus, setOpenModalStatus] = useState<{ + open: boolean; + url: string; + tail?: number; + status?: string; + }>({ + open: false, + url: '', + tail: 1000, + status: undefined + }); + + const openModal = (row?: ListItem) => { + setOpenModalStatus({ + open: true, + url: `${BENCHMARKS_API}/${row?.id}/logs`, + tail: 1000, + status: row?.state || 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/benchmark/index.tsx b/src/pages/benchmark/index.tsx index e3316e84..715187ad 100644 --- a/src/pages/benchmark/index.tsx +++ b/src/pages/benchmark/index.tsx @@ -21,11 +21,13 @@ import { import AddBenchmarkModal from './components/add-benchmark-modal'; import LeftActions from './components/left-actions'; import RightActions from './components/right-actions'; +import ViewLogsModal from './components/view-logs-modal'; import { FormData, BenchmarkListItem as ListItem } from './config/types'; import useBenchmarkColumns from './hooks/use-benchmark-columns'; import useColumnSettings from './hooks/use-column-settings'; import useCreateBenchmark from './hooks/use-create-benchmark'; import useExportData from './hooks/use-export-data'; +import useViewLogs from './hooks/use-view-logs'; import useQueryDataset from './services/use-query-dataset'; const Benchmark: React.FC = () => { @@ -55,6 +57,8 @@ const Benchmark: React.FC = () => { const { dataList: modelList, fetchData: fetchModelList } = useQueryModelList({ getValue: (item: any) => item.name }); + const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } = + useViewLogs(); const { SettingsButton, selectedColumns } = useColumnSettings(); const { datasetList, fetchDatasetData } = useQueryDataset(); @@ -104,6 +108,8 @@ const Benchmark: React.FC = () => { handleEditUser(row); } else if (val === 'delete') { handleDelete({ ...row, name: row.name }); + } else if (val === 'viewlog') { + openViewLogsModal(row); } }); @@ -218,6 +224,12 @@ const Benchmark: React.FC = () => { onCancel={handleModalCancel} onOk={handleModalOk} > + );