chore: update model list polling

main
jialin 2 years ago
parent 94e8638d28
commit 28088b270d

@ -2,11 +2,11 @@
"private": true,
"author": "jialin <jialinkuang@126.com>",
"scripts": {
"dev": "max dev",
"build": "max build",
"dev": "max dev",
"format": "prettier --cache --write .",
"prepare": "husky",
"postinstall": "max setup",
"prepare": "husky",
"setup": "max setup",
"start": "npm run dev"
},
@ -19,6 +19,7 @@
"@umijs/max": "^4.2.1",
"antd": "^5.17.0",
"antd-style": "^3.6.2",
"axios": "^1.7.2",
"classnames": "^2.5.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.11",

File diff suppressed because it is too large Load Diff

@ -2,7 +2,7 @@ import { DownOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Checkbox, Col, Empty, Row, Spin } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import RowContext from '../row-context';
import { RowContextProps, SealTableProps } from '../types';
@ -17,6 +17,7 @@ const TableRow: React.FC<
rowSelection,
rowKey,
columns,
pollingChildren,
onExpand,
renderChildren,
loadChildren
@ -26,6 +27,7 @@ const TableRow: React.FC<
const [checked, setChecked] = useState(false);
const [childrenData, setChildrenData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const pollTimer = useRef<any>(null);
useEffect(() => {
if (rowSelection) {
@ -38,12 +40,26 @@ const TableRow: React.FC<
}
}, [rowSelection]);
const handleRowExpand = async () => {
setExpanded(!expanded);
onExpand?.(!expanded, record);
if (expanded) {
return;
useEffect(() => {
return () => {
if (pollTimer.current) {
clearInterval(pollTimer.current);
}
};
}, []);
const handlePolling = async () => {
if (pollingChildren) {
try {
const data = await loadChildren?.(record);
setChildrenData(data || []);
} catch (error) {
setChildrenData([]);
}
}
};
const handleLoadChildren = async () => {
try {
setLoading(true);
const data = await loadChildren?.(record);
@ -55,6 +71,28 @@ const TableRow: React.FC<
}
};
const handleRowExpand = async () => {
setExpanded(!expanded);
onExpand?.(!expanded, record);
if (pollTimer.current) {
clearInterval(pollTimer.current);
}
if (expanded) {
return;
}
if (pollingChildren) {
await handleLoadChildren();
pollTimer.current = setInterval(() => {
handlePolling();
}, 1000);
} else {
handleLoadChildren();
}
};
const handleSelectChange = (e: any) => {
if (e.target.checked) {
// update selectedRowKeys

@ -14,6 +14,7 @@ const SealTable: React.FC<SealTableProps> = (props) => {
onExpand,
loading,
expandable,
pollingChildren,
rowSelection,
renderChildren,
loadChildren
@ -129,6 +130,7 @@ const SealTable: React.FC<SealTableProps> = (props) => {
rowSelection={rowSelection}
expandable={expandable}
rowKey={rowKey}
pollingChildren={pollingChildren}
renderChildren={renderChildren}
loadChildren={loadChildren}
onExpand={onExpand}

@ -1,4 +1,5 @@
.row-children {
position: relative;
display: flex;
align-items: center;
height: 54px;
@ -6,6 +7,7 @@
border-radius: var(--ant-table-header-border-radius);
background-color: var(--color-fill-1);
transition: all 0.2s ease;
&:hover {
background-color: var(--ant-table-row-hover-bg);
transition: all 0.2s ease;

@ -29,6 +29,7 @@ export interface SealTableProps {
empty?: React.ReactNode;
expandable?: React.ReactNode;
dataSource: any[];
pollingChildren?: boolean;
loading?: boolean;
onExpand?: (expanded: boolean, record: any) => void;
renderChildren?: (data: any) => React.ReactNode;
@ -39,5 +40,6 @@ export interface SealTableProps {
export interface RowContextProps {
record: Record<string, any>;
columns: React.ReactNode[];
pollingChildren?: boolean;
rowIndex: number;
}

@ -37,3 +37,9 @@ export const StatusMaps = {
success: 'success',
inactive: 'inactive'
};
export const WatchEventType = {
CREATE: 'ADDED',
UPDATE: 'MODIFIED',
DELETE: 'DELETED'
};

@ -1,8 +1,10 @@
@import url('src/assets/styles/common.less');
@import url('src/assets/styles/menu.less');
html {
--color-fill-1: rgba(0, 0, 0, 0.04);
--color-fill-1: rgba(0, 0, 0, 4%);
// --color-fill-1: #f3f6fa;
--color-fill-2: #fff;
--color-fill-2: #fff;
--color-fill-3: #f3f6fa;
--menu-border-radius-base: 32px;
--border-radius-base: 16px;
@ -12,7 +14,7 @@ html {
--color-text-1: var(--ant-color-text);
--color-bg-light-1: #f0fff6;
--font-size-base: 12px;
--ant-color-fill-secondary: rgba(0, 0, 0, 0.06);
--ant-color-fill-secondary: rgba(0, 0, 0, 6%);
--table-td-radius: 24px;
--checkbox-border-radius: 4px;
--ant-table-cell-padding-inline: 16px;
@ -26,17 +28,19 @@ html {
--seal-transition-func: cubic-bezier(0, 0, 1, 1);
// ======== input ============
--ant-input-active-shadow: 0 0 0 2px rgba(5, 255, 105, 0.06);
--ant-input-active-shadow: 0 0 0 2px rgba(5, 255, 105, 6%);
--ant-input-active-border-color: #2fbf85;
--ant-input-hover-border-color: #54cc98;
.css-var-rf {
--ant-font-size: var(--font-size-base);
}
.css-var-rg,
.css-var-ri,
.css-var-rh {
--ant-font-size: var(--font-size-base);
&.ant-menu-css-var {
--ant-menu-item-height: 46px;
--ant-menu-item-selected-bg: var(--color-white-1);
@ -47,6 +51,7 @@ html {
--ant-menu-item-active-bg: var(--color-white-1);
}
}
.css-var-r0 {
// --ant-control-height: 36px;
--ant-font-size-sm: var(--font-size-base);
@ -56,7 +61,7 @@ html {
--ant-padding-sm: 14px;
--ant-border-radius-lg: 16px;
--ant-color-error: #ff4d4f;
--ant-color-bg-mask: rgba(0, 0, 0, 0.35);
--ant-color-bg-mask: rgba(0, 0, 0, 35%);
&.ant-popover {
--ant-popover-inner-padding: 26px;
@ -72,6 +77,7 @@ html {
&.ant-table-css-var {
--ant-table-row-hover-bg: #e6e6e6;
}
&.ant-modal-css-var {
--ant-modal-title-font-size: 14px;
--ant-modal-header-margin-bottom: 26px;
@ -90,12 +96,14 @@ html {
--ant-menu-item-color: var(--color-text-1);
--ant-menu-item-active-bg: var(--color-white-1);
}
.css-var-r0.ant-input-css-var {
--ant-input-input-font-size: var(--font-size-base);
--ant-input-input-font-size-lg: var(--font-size-base);
--ant-input-input-font-size-sm: var(--font-size-base);
--ant-input-padding-inline-lg: 14px;
}
.css-var-r0.ant-select-css-var {
--ant-select-option-active-bg: var(--color-fill-1);
--ant-select-option-font-size: var(--font-size-base);
@ -117,35 +125,44 @@ body {
.ant-table-wrapper .ant-table-selection-column {
padding-inline-start: 16px !important;
}
.ant-table-container .ant-table-content table {
border-spacing: 0 20px;
.ant-table-thead th.ant-table-column-sort {
background-color: transparent;
&::before {
background-color: var(--ant-table-header-split-color) !important;
}
}
.ant-checkbox {
.ant-checkbox-inner::after {
display: flex !important;
}
}
.ant-checkbox-indeterminate .ant-checkbox-inner:after {
.ant-checkbox-indeterminate .ant-checkbox-inner::after {
display: flex !important;
}
tr > th {
background-color: var(--color-white-1);
border: none;
padding-block: 0px;
padding-block: 0;
}
tr > td {
background-color: var(--color-fill-1);
border-bottom: none;
height: 70px;
&:first-child {
border-top-left-radius: var(--table-td-radius);
border-bottom-left-radius: var(--table-td-radius);
}
&:last-child {
border-top-right-radius: var(--table-td-radius);
border-bottom-right-radius: var(--table-td-radius);
@ -156,6 +173,7 @@ body {
.ant-input-css-var.ant-input {
height: 40px;
}
.ant-input-outlined.ant-input-status-error:not(.ant-input-disabled) {
border: none;
box-shadow: none;
@ -200,36 +218,40 @@ body {
}
// ======== basic layout style start============
// ======== basic layout style end ============
// ======== menu style start ============
.ant-pro-sider-collapsed-button {
display: none;
}
.ant-pro-layout {
height: 100vh;
.ant-pro-sider-logo {
padding-left: 22px;
border-block-end: none;
}
.ant-layout {
min-height: 100vh;
}
.ant-pro-layout-container {
background-color: var(--color-fill-2);
}
}
// ======== basic layout style end ============
// ======== menu style start ============
.ant-pro-sider-collapsed-button {
display: none;
}
.ant-pro-layout {
.ant-pro-sider {
.ant-menu {
.ant-menu-item:not(.ant-menu-item-selected) {
color: var(--ant-color-text);
}
.ant-menu-item:hover {
color: var(--ant-color-text);
}
.ant-menu-item.ant-menu-item-selected:hover {
color: var(--ant-color-primary);
}
@ -243,5 +265,23 @@ body {
border-radius: 16px;
}
@import url('src/assets/styles/common.less');
@import url('src/assets/styles/menu.less');
@keyframes skeleton-loading {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
.skeleton-loading {
animation: skeleton-loading 2.5s ease infinite;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 4%) 25%,
rgba(0, 0, 0, 10%) 37%,
rgba(255, 255, 255, 4%) 63%
);
background-size: 400% 100%;
}

@ -0,0 +1,239 @@
import { WatchEventType } from '@/config';
import { request } from '@umijs/max';
import axios from 'axios';
import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';
interface RequestConfig {
url: string;
handler: (data: any) => any;
beforeReconnect?: () => void;
params?: object;
contentType?: 'json' | 'text';
}
export const sliceJsonStr = (text: string) => {
if (!text) return [];
const result: string[] = [];
for (let i = 0, j = 0, start = 0; i < text.length; i += 1) {
if (text.charAt(i) === '{') {
j += 1;
}
if (text.charAt(i) === '}') {
j -= 1;
}
if (j === 0 && i !== start) {
result.push(text.slice(start, i + 1));
start = i + 1;
}
}
return result;
};
export const parseJsonStr = (list: string[]) => {
return _.map(list, (str: string) => {
return JSON.parse(str);
});
};
const findMatchingClosingBracket = (inputStr: string, startIndex: number) => {
let count = 0;
for (let i = startIndex; i < inputStr.length; i += 1) {
if (inputStr[i] === '{') {
count += 1;
} else if (inputStr[i] === '}') {
count -= 1;
}
if (count === 0) {
return i;
}
}
return -1;
};
const findValidJSONStrings = (inputStr: string) => {
const validJSONStrings: any[] = [];
let startIndex = 0;
while (startIndex < inputStr.length) {
const openingBraceIndex = inputStr.indexOf('{', startIndex);
if (openingBraceIndex === -1) {
break; // No more opening braces
}
const closingBraceIndex = findMatchingClosingBracket(
inputStr,
openingBraceIndex
);
if (closingBraceIndex === -1) {
// If no matching closing brace, set startIndex to next character
startIndex = openingBraceIndex + 1;
} else {
const jsonString = inputStr.substring(
openingBraceIndex,
closingBraceIndex + 1
);
try {
const data = JSON.parse(jsonString);
validJSONStrings.push(data);
} catch (error) {
// Ignore invalid JSON
}
startIndex = closingBraceIndex + 1;
}
}
return validJSONStrings;
};
const parseData = (data: string) => {
const res = findValidJSONStrings(data);
return res;
};
export const createAxiosToken = () => {
const { CancelToken } = axios;
const source = CancelToken.source();
return source;
};
export const sliceData = (data: string, loaded: number, loadedSize: any) => {
const result = data.slice(loadedSize.current);
loadedSize.current = loaded;
return result;
};
const useSetChunkRequest = () => {
const [requestReadyState, setRequestReadyState] = useState(3);
const axiosToken = useRef<any>(null);
const requestConfig = useRef<any>({});
const loaded = useRef(0);
const total = useRef(0);
const totalCount = 5;
const retryCount = useRef(totalCount);
const particalConfig = { params: {}, contentType: 'json' };
const timer = useRef<any>(null);
const loadedSize = useRef(0);
const reset = () => {
loaded.current = 0;
total.current = 0;
loadedSize.current = 0;
setRequestReadyState(3);
};
const resetResultSchema = (result: any[]) => {
return _.map(result, (data: any) => {
if (data.type === WatchEventType.DELETE) {
data.ids = _.map(data.items, (item: any) => item.id);
}
data.collection = data.items || [];
return data;
});
};
const axiosChunkRequest = async ({
url,
handler,
beforeReconnect,
params = {},
contentType = 'json'
}: RequestConfig) => {
reset();
axiosToken.current?.cancel?.();
axiosToken.current = createAxiosToken();
try {
const { request: requestData } = await request(url, {
params: {
...params,
watch: true
},
skipErrorHandler: true,
getResponse: true,
cancelToken: axiosToken.current.token,
async onDownloadProgress(e) {
const { response, readyState } = e.currentTarget;
setRequestReadyState(readyState);
loaded.current = e.loaded || 0;
total.current = e.total || 0;
let result = response;
let cres = '';
if (contentType === 'json') {
const currentRes = sliceData(response, e.loaded, loadedSize);
result = parseData(currentRes);
result = resetResultSchema(result);
cres = currentRes;
}
handler(result);
console.log('chunkrequest===', {
result,
url,
params,
raw: cres
});
}
});
setRequestReadyState(requestData?.readyState);
if (retryCount.current > 0) {
retryCount.current -= 1;
}
} catch (error) {
if (!axios.isCancel(error)) {
setRequestReadyState(4);
if (retryCount.current > 0) {
retryCount.current -= 1;
}
}
}
return axiosToken.current;
};
const setChunkRequest = (config: RequestConfig) => {
requestConfig.current = { ...particalConfig, ...config };
retryCount.current = totalCount;
clearTimeout(timer.current);
axiosChunkRequest(requestConfig.current);
return axiosToken.current;
};
useEffect(() => {
const handleUnload = () => {
axiosToken.current?.cancel?.();
};
window.addEventListener('beforeunload', handleUnload);
return () => {
reset();
requestConfig.current.beforeReconnect = null;
axiosToken.current?.cancel?.();
clearTimeout(timer.current);
window.removeEventListener('beforeunload', handleUnload);
};
}, []);
useEffect(() => {
if (requestReadyState === 4 && retryCount.current > 0) {
requestConfig.current.beforeReconnect?.();
clearTimeout(timer.current);
timer.current = setTimeout(
() => {
axiosChunkRequest(requestConfig.current);
},
2 ** (totalCount - retryCount.current) * 1000
);
}
}, [requestReadyState]);
return {
setChunkRequest
};
};
export default useSetChunkRequest;

@ -0,0 +1,6 @@
import axiso from 'axios';
export default function useRequestToken() {
const { source } = axiso.CancelToken;
return source;
}

@ -0,0 +1,79 @@
import { WatchEventType } from '@/config';
import _ from 'lodash';
interface ChunkedCollection {
ids: string[];
collection: any[];
type: string;
}
// Only used to update lists without nested state
export function useUpdateChunkedList(
dataList: { id: string | number }[],
options: {
setDataList: (args: any) => void;
callback?: (args: any) => void;
filterFun?: (args: any) => boolean;
mapFun?: (args: any) => any;
computedID?: (d: object) => string;
}
) {
const updateChunkedList = (data: ChunkedCollection) => {
let collections = data?.collection || [];
if (options?.computedID) {
collections = _.map(collections, (item: any) => {
item.id = options?.computedID?.(item);
return item;
});
}
if (options?.filterFun) {
collections = _.filter(data?.collection, options?.filterFun);
}
if (options?.mapFun) {
collections = _.map(data?.collection, options?.mapFun);
}
const ids = data?.ids || [];
// CREATE
if (data?.type === WatchEventType.CREATE) {
_.each(collections, (item: any) => {
const updateIndex = _.findIndex(
dataList,
(sItem: any) => sItem.id === item.id
);
if (updateIndex === -1) {
const updateItem = _.cloneDeep(item);
const list = _.concat(updateItem, dataList);
options.setDataList(list);
}
});
}
// DELETE
if (data?.type === WatchEventType.DELETE) {
const list = _.filter(dataList, (item: any) => {
return !_.find(ids, (id: any) => id === item.id);
});
options.setDataList(list);
}
// UPDATE
if (data?.type === WatchEventType.UPDATE) {
_.each(collections, (item: any) => {
const updateIndex = _.findIndex(
dataList,
(sItem: any) => sItem.id === item.id
);
if (updateIndex > -1) {
const updateItem = _.cloneDeep(item);
dataList[updateIndex] = updateItem;
}
options.setDataList(dataList);
});
}
if (options?.callback) {
options?.callback(dataList);
}
};
return {
updateChunkedList
};
}
export default useUpdateChunkedList;

@ -12,11 +12,13 @@ export const MODEL_INSTANCE_API = '/model_instances';
// ===================== Models =====================
export async function queryModelsList(
params: Global.Pagination & { query?: string }
params: Global.Pagination & { query?: string },
options?: any
) {
return request<Global.PageResponse<ListItem>>(`${MODELS_API}`, {
methos: 'GET',
params
params,
...options
});
}
@ -112,3 +114,12 @@ export async function callHuggingfaceQuickSearch(params: any) {
params
});
}
export async function queryHuggingfaceModelFiles(params: any) {
return request(
`https://huggingface.co/openbmb/MiniCPM-Llama3-V-2_5-gguf/tree/main`,
{
method: 'GET'
}
);
}

@ -5,8 +5,12 @@ import SealColumn from '@/components/seal-table/components/seal-column';
import StatusTag from '@/components/status-tag';
import { PageAction } from '@/config';
import type { PageActionType } from '@/config/types';
import useSetChunkRequest, {
createAxiosToken
} from '@/hooks/use-chunk-request';
import useTableRowSelection from '@/hooks/use-table-row-selection';
import useTableSort from '@/hooks/use-table-sort';
import useUpdateChunkedList from '@/hooks/use-update-chunk-list';
import {
DeleteOutlined,
FieldTimeOutlined,
@ -30,8 +34,9 @@ import {
} from 'antd';
import dayjs from 'dayjs';
import _ from 'lodash';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import {
MODELS_API,
createModel,
createModelInstance,
deleteModel,
@ -50,6 +55,7 @@ const Models: React.FC = () => {
const access = useAccess();
const intl = useIntl();
const navigate = useNavigate();
const { setChunkRequest } = useSetChunkRequest();
const rowSelection = useTableRowSelection();
const { sortOrder, setSortOrder } = useTableSort({
defaultSortOrder: 'descend'
@ -63,19 +69,30 @@ const Models: React.FC = () => {
const [action, setAction] = useState<PageActionType>(PageAction.CREATE);
const [title, setTitle] = useState<string>('');
const [dataSource, setDataSource] = useState<ListItem[]>([]);
const timer = useRef<any>();
let axiosToken = createAxiosToken();
const [queryParams, setQueryParams] = useState({
page: 1,
perPage: 10,
query: ''
});
// request data
const { updateChunkedList } = useUpdateChunkedList(dataSource, {
setDataList: setDataSource
});
const fetchData = async () => {
setLoading(true);
const fetchData = async (polling?: boolean) => {
axiosToken?.cancel?.();
axiosToken = createAxiosToken();
setLoading(!polling);
try {
const params = {
..._.pickBy(queryParams, (val: any) => !!val)
};
const res = await queryModelsList(params);
const res = await queryModelsList(params, {
cancelToken: axiosToken.token
});
console.log('res=======', res);
setDataSource(res.items);
setTotal(res.pagination.total);
@ -85,6 +102,15 @@ const Models: React.FC = () => {
setLoading(false);
}
};
// update data by polling
const fetchDataByPolling = () => {
clearInterval(timer.current);
timer.current = setInterval(() => {
fetchData(true);
}, 3000);
};
const handleShowSizeChange = (page: number, size: number) => {
console.log(page, size);
setQueryParams({
@ -106,7 +132,38 @@ const Models: React.FC = () => {
setSortOrder(sorter.order);
};
const handleFilter = () => {
fetchData();
};
const updateHandler = (list: any) => {
_.each(list, (data: any) => {
updateChunkedList(data);
});
if (!dataSource.length) {
handleFilter();
}
};
const createModelsChunkRequest = () => {
try {
setChunkRequest({
url: MODELS_API,
params: {
..._.pickBy(
_.omit(queryParams, ['page', 'perPage']),
(val: any) => !!val
)
},
handler: updateHandler
});
} catch (error) {
// ignore
}
};
const handleSearch = (e: any) => {
console.log('request==========');
fetchData();
};
@ -230,6 +287,17 @@ const Models: React.FC = () => {
return data.items || [];
};
useEffect(() => {
fetchData();
}, [queryParams]);
useEffect(() => {
fetchDataByPolling();
return () => {
clearInterval(timer.current);
};
}, []);
const renderChildren = (list: any) => {
return (
<Space size={16} direction="vertical" style={{ width: '100%' }}>
@ -239,6 +307,10 @@ const Models: React.FC = () => {
key={`${item.id}`}
onMouseEnter={() => handleOnMouseEnter(item.id, index)}
onMouseLeave={handleOnMouseLeave}
style={{ borderRadius: 'var(--ant-table-header-border-radius)' }}
className={
item.download_progress !== 100 ? 'skeleton-loading' : ''
}
>
<RowChildren>
<Row style={{ width: '100%' }} align="middle">
@ -296,12 +368,6 @@ const Models: React.FC = () => {
);
};
// request data
useEffect(() => {
fetchData();
}, [queryParams]);
return (
<>
<PageContainer
@ -358,6 +424,7 @@ const Models: React.FC = () => {
rowKey="id"
expandable={true}
onChange={handleTableChange}
pollingChildren={true}
loadChildren={getModelInstances}
renderChildren={renderChildren}
pagination={{

Loading…
Cancel
Save