fix(style): catalog size tags overflow

This commit is contained in:
jialin
2025-01-09 09:26:42 +08:00
parent 39b6c00c6d
commit fb4666195c
14 changed files with 243 additions and 102 deletions
+41 -4
View File
@@ -128,6 +128,9 @@ dependencies:
rc-resize-observer: rc-resize-observer:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0(react-dom@18.2.0)(react@18.2.0) version: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-tabs:
specifier: ^15.5.0
version: 15.5.0(react-dom@18.2.0)(react@18.2.0)
rc-virtual-list: rc-virtual-list:
specifier: ^3.14.8 specifier: ^3.14.8
version: 3.14.8(react-dom@18.2.0)(react@18.2.0) version: 3.14.8(react-dom@18.2.0)(react@18.2.0)
@@ -5208,7 +5211,7 @@ packages:
resolution: {integrity: sha512-hy8b7Y1J8OGe6LbAjj3xniQrj3v6lsivCcrmf4TzSgPzLkhIeKgc5IZnT7ReIqmEuodjfO8EYAuoFvIrHi/+jQ==, tarball: https://registry.npmjs.org/@types/history/-/history-5.0.0.tgz} resolution: {integrity: sha512-hy8b7Y1J8OGe6LbAjj3xniQrj3v6lsivCcrmf4TzSgPzLkhIeKgc5IZnT7ReIqmEuodjfO8EYAuoFvIrHi/+jQ==, tarball: https://registry.npmjs.org/@types/history/-/history-5.0.0.tgz}
deprecated: This is a stub types definition. history provides its own type definitions, so you do not need this installed. deprecated: This is a stub types definition. history provides its own type definitions, so you do not need this installed.
dependencies: dependencies:
history: 4.10.1 history: 5.3.0
dev: false dev: false
/@types/hoist-non-react-statics@3.3.5: /@types/hoist-non-react-statics@3.3.5:
@@ -5354,7 +5357,7 @@ packages:
'@types/history': 4.7.11 '@types/history': 4.7.11
'@types/react': 18.3.16 '@types/react': 18.3.16
'@types/react-router': 5.1.20 '@types/react-router': 5.1.20
redux: 3.7.2 redux: 4.2.1
dev: false dev: false
/@types/react-router@5.1.20: /@types/react-router@5.1.20:
@@ -9076,7 +9079,7 @@ packages:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==, tarball: https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz} resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==, tarball: https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz}
engines: {node: '>=0.11'} engines: {node: '>=0.11'}
dependencies: dependencies:
'@babel/runtime': 7.25.7 '@babel/runtime': 7.26.0
dev: false dev: false
/dayjs@1.11.11: /dayjs@1.11.11:
@@ -15567,6 +15570,22 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
/rc-menu@9.16.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-vAL0yqPkmXWk3+YKRkmIR8TYj3RVdEt3ptG2jCJXWNAvQbT0VJJdRyHZ7kG/l1JsZlB+VJq/VcYOo69VR4oD+w==, tarball: https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.0.tgz}
peerDependencies:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.26.0
'@rc-component/trigger': 2.2.3(react-dom@18.2.0)(react@18.2.0)
classnames: 2.5.1
rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0)
rc-overflow: 1.3.2(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.44.2(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/rc-menu@9.8.4(react-dom@18.2.0)(react@18.2.0): /rc-menu@9.8.4(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-lmw2j8I2fhdIzHmC9ajfImfckt0WDb2KVJJBBRIsxPEw2kGkEfjLMUoB1NgiNT/Q5cC8PdjGOGQjHJIJMwyNMw==, tarball: https://registry.npmjs.org/rc-menu/-/rc-menu-9.8.4.tgz} resolution: {integrity: sha512-lmw2j8I2fhdIzHmC9ajfImfckt0WDb2KVJJBBRIsxPEw2kGkEfjLMUoB1NgiNT/Q5cC8PdjGOGQjHJIJMwyNMw==, tarball: https://registry.npmjs.org/rc-menu/-/rc-menu-9.8.4.tgz}
peerDependencies: peerDependencies:
@@ -16050,6 +16069,24 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
/rc-tabs@15.5.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-NrDcTaUJLh9UuDdMBkjKTn97U9iXG44s9D03V5NHkhEDWO5/nC6PwC3RhkCWFMKB9hh+ryqgZ+TIr1b9Jd/hnQ==, tarball: https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.0.tgz}
engines: {node: '>=8.x'}
peerDependencies:
react: '>=16.9.0'
react-dom: '>=16.9.0'
dependencies:
'@babel/runtime': 7.26.0
classnames: 2.5.1
rc-dropdown: 4.2.0(react-dom@18.2.0)(react@18.2.0)
rc-menu: 9.16.0(react-dom@18.2.0)(react@18.2.0)
rc-motion: 2.9.3(react-dom@18.2.0)(react@18.2.0)
rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0)
rc-util: 5.44.2(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/rc-textarea@0.4.7(react-dom@18.2.0)(react@18.2.0): /rc-textarea@0.4.7(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-IQPd1CDI3mnMlkFyzt2O4gQ2lxUsnBAeJEoZGJnkkXgORNqyM9qovdrCj9NzcRfpHgLdzaEbU3AmobNFGUznwQ==, tarball: https://registry.npmjs.org/rc-textarea/-/rc-textarea-0.4.7.tgz} resolution: {integrity: sha512-IQPd1CDI3mnMlkFyzt2O4gQ2lxUsnBAeJEoZGJnkkXgORNqyM9qovdrCj9NzcRfpHgLdzaEbU3AmobNFGUznwQ==, tarball: https://registry.npmjs.org/rc-textarea/-/rc-textarea-0.4.7.tgz}
peerDependencies: peerDependencies:
@@ -16804,7 +16841,7 @@ packages:
/regenerator-transform@0.15.2: /regenerator-transform@0.15.2:
resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==, tarball: https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz} resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==, tarball: https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz}
dependencies: dependencies:
'@babel/runtime': 7.25.7 '@babel/runtime': 7.26.0
dev: false dev: false
/regexp-tree@0.1.27: /regexp-tree@0.1.27:
+3 -3
View File
@@ -11,13 +11,13 @@
:global { :global {
.copy-button-wrapper { .copy-button-wrapper {
display: none; display: none;
background-color: #383838; background-color: rgba(255, 255, 255, 20%);
position: absolute; position: absolute;
z-index: 10; z-index: 10;
right: -28px; right: 0;
top: 0; top: 0;
padding: 2px; padding: 2px;
border-radius: 4px; border-radius: 0 4px;
} }
.simplebar-content { .simplebar-content {
+4 -2
View File
@@ -28,6 +28,7 @@ type StatusTagProps = {
download?: { download?: {
percent: number; percent: number;
}; };
extra?: React.ReactNode;
actions?: { actions?: {
label: string; label: string;
icon?: React.ReactNode; icon?: React.ReactNode;
@@ -40,6 +41,7 @@ type StatusTagProps = {
const StatusTag: React.FC<StatusTagProps> = ({ const StatusTag: React.FC<StatusTagProps> = ({
statusValue, statusValue,
download, download,
extra,
actions = [], actions = [],
type = 'tag' type = 'tag'
}) => { }) => {
@@ -78,7 +80,6 @@ const StatusTag: React.FC<StatusTagProps> = ({
style={{ color: 'rgba(255,255,255,.8)' }} style={{ color: 'rgba(255,255,255,.8)' }}
text={statusValue.message || ''} text={statusValue.message || ''}
size="small" size="small"
placement="right"
></CopyButton> ></CopyButton>
{actions?.map((item) => { {actions?.map((item) => {
return ( return (
@@ -104,7 +105,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
})} })}
</div> </div>
<SimpleBar style={{ maxHeight: 200 }}> <SimpleBar style={{ maxHeight: 200 }} autoHide={false}>
<div <div
style={{ style={{
width: 'max-content', width: 'max-content',
@@ -115,6 +116,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
}} }}
> >
{statusValue.message} {statusValue.message}
{extra}
</div> </div>
</SimpleBar> </SimpleBar>
</div> </div>
+4
View File
@@ -13,4 +13,8 @@
height: 22px; height: 22px;
opacity: 0.7; opacity: 0.7;
} }
.tags-content {
display: flex;
}
} }
+104 -77
View File
@@ -1,99 +1,126 @@
import { MoreOutlined } from '@ant-design/icons'; import { MoreOutlined } from '@ant-design/icons';
import { Dropdown, Tag } from 'antd'; import { Dropdown, Tag } from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import _ from 'lodash';
import ResizeObserver from 'rc-resize-observer';
import React, { useEffect, useRef, useState } from 'react';
import './index.less'; import './index.less';
const TagsWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { interface TagsWrapperProps {
gap?: number;
dataList: any[];
renderTag: (item: any) => React.ReactNode;
}
const TagsWrapper: React.FC<TagsWrapperProps> = (props) => {
const { gap = 0, dataList, renderTag } = props;
const tagsContentRef = useRef<HTMLDivElement>(null);
const [hiddenIndices, setHiddenIndices] = useState({
start: 0,
end: dataList.length
});
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const observer = useRef<IntersectionObserver | null>(null); const moreBtnRef = useRef<HTMLDivElement>(null);
const resizeObserver = useRef<ResizeObserver | null>(null); const moreButtonWidth = useRef(0);
const [hiddenIndex, setHiddenIndex] = useState<number>(4); const nodeSizeList = useRef<number[]>([]);
const uid = useRef(0);
const updateUid = () => { const calculateHiddenIndices = () => {
uid.current += 1; const wrapperWidth = wrapperRef.current?.offsetWidth || 0;
return uid.current; const childNodes = tagsContentRef.current?.childNodes;
}; let sizeList = nodeSizeList.current;
const dropItems = useMemo(() => { if (!sizeList.length && childNodes?.length) {
return React.Children.toArray(children) sizeList = _.map(childNodes, (node: HTMLDivElement, index: number) => {
.slice(hiddenIndex) if (index === childNodes.length - 1) {
.map((child) => ({ return node.offsetWidth;
label: child, }
key: updateUid() return node.offsetWidth + gap;
})); });
}, [children, hiddenIndex]); nodeSizeList.current = sizeList;
}
const updateHiddenIndex = () => { if (wrapperWidth === 0 || !sizeList.length) return;
if (!wrapperRef.current || !observer.current) return;
const childrenList = Array.from(wrapperRef.current.children); // cache more button width
let newHiddenIndex = 0; if (moreBtnRef.current?.offsetWidth) {
moreButtonWidth.current = moreBtnRef.current?.offsetWidth + gap;
}
childrenList.forEach((child, index) => { let totalWidth = 0;
const rect = child.getBoundingClientRect(); let start = 0;
// visible in wrapperRef let end = dataList.length;
const issivible =
rect.top < wrapperRef.current!.clientHeight &&
rect.bottom > 0 &&
rect.left < wrapperRef.current!.clientWidth &&
rect.right > 0;
if (!issivible) { for (let i = 0; i < sizeList.length; i++) {
newHiddenIndex = Math.max(newHiddenIndex, index + 1); const nodeWidth = sizeList[i];
if (totalWidth + moreButtonWidth.current >= wrapperWidth) {
end = i - 1;
break;
} }
}); totalWidth += nodeWidth;
setHiddenIndex(newHiddenIndex); if (totalWidth >= wrapperWidth) {
end = i;
break;
}
}
if (hiddenIndices.start !== start || hiddenIndices.end !== end) {
setHiddenIndices({
start,
end
});
}
}; };
useEffect(() => { useEffect(() => {
if (!wrapperRef.current) return; if (tagsContentRef.current) {
calculateHiddenIndices();
}
}, [dataList, gap]);
// observer.current = new IntersectionObserver( const handleContentResize = _.throttle(() => {
// (entries) => { calculateHiddenIndices();
// const lastEntry = entries[entries.length - 1]; }, 200);
// if (lastEntry && lastEntry.intersectionRatio < 1) {
// setHiddenIndex((prev) =>
// Math.max(prev, React.Children.count(children))
// );
// }
// },
// { root: wrapperRef.current, threshold: 1 }
// );
// const childrenList = wrapperRef.current.children;
// if (childrenList.length > 0) {
// observer.current.observe(childrenList[childrenList.length - 1]);
// }
resizeObserver.current = new ResizeObserver(() => {
// updateHiddenIndex();
});
resizeObserver.current.observe(wrapperRef.current);
return () => {
observer.current?.disconnect();
resizeObserver.current?.disconnect();
};
}, [children]);
return ( return (
<div className="tags-wrapper" ref={wrapperRef}> <ResizeObserver onResize={handleContentResize}>
{React.Children.toArray(children).slice(0, hiddenIndex)} <div className="tags-wrapper" ref={wrapperRef}>
{React.Children.toArray(children).length > 4 && ( <ResizeObserver onResize={handleContentResize}>
<Dropdown <div className="tags-content" ref={tagsContentRef} style={{ gap }}>
trigger={['hover']} {_.map(
menu={{ _.slice(dataList, hiddenIndices.start, hiddenIndices.end),
items: dropItems (item: any, index: number) => {
}} return <span key={index}>{renderTag?.(item)}</span>;
> }
<Tag className="more"> )}
<MoreOutlined rotate={90} /> </div>
</Tag> </ResizeObserver>
</Dropdown> {hiddenIndices.end < dataList.length && (
)} <Dropdown
</div> trigger={['hover']}
menu={{
items: _.map(
_.slice(dataList, hiddenIndices.end),
(item: any, index: number) => {
return {
label: renderTag?.(item),
key: index
};
}
)
}}
>
<Tag
className="more"
style={{ marginInline: `${gap}px 0` }}
ref={moreBtnRef}
>
<MoreOutlined rotate={90} />
</Tag>
</Dropdown>
)}
</div>
</ResizeObserver>
); );
}; };
@@ -0,0 +1,29 @@
import { MoreOutlined } from '@ant-design/icons';
import { Dropdown, Tag } from 'antd';
import React from 'react';
interface MoreDropdownProps {
items: any[];
}
const MoreDropdown: React.FC<MoreDropdownProps> = (props) => {
const { items } = props;
return (
<>
{items && items.length > 0 ? (
<Dropdown
trigger={['hover']}
menu={{
items: items
}}
>
<Tag className="more">
<MoreOutlined rotate={90} />
</Tag>
</Dropdown>
) : null}
</>
);
};
export default React.memo(MoreDropdown);
+2 -1
View File
@@ -89,5 +89,6 @@ export default {
'models.form.search.gguftips': 'models.form.search.gguftips':
'If using macOS or Windows as a worker, check GGUF (uncheck for audio models).', 'If using macOS or Windows as a worker, check GGUF (uncheck for audio models).',
'models.form.button.addlabel': 'Add Label', 'models.form.button.addlabel': 'Add Label',
'models.filter.category': 'Filter by category' 'models.filter.category': 'Filter by category',
'models.list.more.logs': 'View More'
}; };
+2 -1
View File
@@ -85,5 +85,6 @@ export default {
'models.form.search.gguftips': 'models.form.search.gguftips':
'当 macOS 或 Windows 作 worker 时勾选 GGUF(搜索音频模型时取消勾选)', '当 macOS 或 Windows 作 worker 时勾选 GGUF(搜索音频模型时取消勾选)',
'models.form.button.addlabel': '添加标签', 'models.form.button.addlabel': '添加标签',
'models.filter.category': '按类别筛选' 'models.filter.category': '按类别筛选',
'models.list.more.logs': '查看更多'
}; };
+28 -10
View File
@@ -27,6 +27,28 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
e.target.src = fallbackImg; e.target.src = fallbackImg;
}; };
const renderTag = (sItem: any) => {
return (
<Tag
key={sItem}
className="tag-item"
style={{
marginRight: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '2px 6px',
borderRadius: 4,
fontSize: 12,
opacity: 0.7,
height: 22
}}
>
{sItem}B
</Tag>
);
};
return ( return (
<div <div
onClick={handleOnClick} onClick={handleOnClick}
@@ -81,7 +103,7 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
<span className="flex-center"> <span className="flex-center">
{_.map(data.licenses, (license: string, index: number) => { {_.map(data.licenses, (license: string, index: number) => {
return ( return (
<span key={license} className="flex-center m-r-16"> <span key={license} className="flex-center m-r-8">
<IconFont type="icon-justice1" className="m-r-5"></IconFont> <IconFont type="icon-justice1" className="m-r-5"></IconFont>
<span>{license}</span> <span>{license}</span>
</span> </span>
@@ -113,15 +135,11 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
<> <>
<span className="dot"></span> <span className="dot"></span>
<div className="box"> <div className="box">
<TagWrapper> <TagWrapper
{data.sizes.map((sItem, i) => { gap={8}
return ( dataList={data.sizes}
<Tag key={sItem} className="tag-item"> renderTag={renderTag}
{sItem}B ></TagWrapper>
</Tag>
);
})}
</TagWrapper>
</div> </div>
</> </>
)} )}
@@ -15,7 +15,7 @@ import {
ThunderboltFilled ThunderboltFilled
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Col, Divider, Row, Space, Tag, Tooltip } from 'antd'; import { Button, Col, Divider, Row, Space, Tag, Tooltip } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
@@ -282,6 +282,21 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
? { percent: item.download_progress } ? { percent: item.download_progress }
: undefined : undefined
} }
extra={
item.state === InstanceStatusMap.Error ? (
<Button
type="link"
size="small"
onClick={() =>
handleChildSelect('viewlog', item, list)
}
>
{intl.formatMessage({
id: 'models.list.more.logs'
})}
</Button>
) : null
}
statusValue={{ statusValue={{
status: status:
item.state === InstanceStatusMap.Downloading && item.state === InstanceStatusMap.Downloading &&
+2 -1
View File
@@ -31,7 +31,8 @@ const PlaygroundEmbedding: React.FC = () => {
const getModelListByEmbedding = async () => { const getModelListByEmbedding = async () => {
try { try {
const params = { const params = {
categories: modelCategoriesMap.embedding categories: modelCategoriesMap.embedding,
with_meta: true
}; };
const res = await queryModelsList(params); const res = await queryModelsList(params);
const list = _.map(res.data || [], (item: any) => { const list = _.map(res.data || [], (item: any) => {
+2 -1
View File
@@ -33,7 +33,8 @@ const PlaygroundRerank: React.FC = () => {
const getModelListByReranker = async () => { const getModelListByReranker = async () => {
try { try {
const params = { const params = {
categories: modelCategoriesMap.reranker categories: modelCategoriesMap.reranker,
with_meta: true
}; };
const res = await queryModelsList(params); const res = await queryModelsList(params);
const list = _.map(res.data || [], (item: any) => { const list = _.map(res.data || [], (item: any) => {
@@ -59,7 +59,6 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
{ version: versionInfo.version } { version: versionInfo.version }
)} )}
</li> </li>
<li>{intl.formatMessage({ id: 'resources.worker.select.command' })}</li>
<li> <li>
<span> <span>
{intl.formatMessage({ id: 'resources.worker.driver.install' })} {intl.formatMessage({ id: 'resources.worker.driver.install' })}
+6
View File
@@ -57,6 +57,11 @@ export const addWorkerGuide: Record<string, any> = {
return `docker run -d --ipc=host --network=host gpustack/gpustack:${params.tag} --server-url ${params.server} --token ${params.token}`; return `docker run -d --ipc=host --network=host gpustack/gpustack:${params.tag} --server-url ${params.server} --token ${params.token}`;
} }
}, },
rocm: {
registerWorker(params: { server: string; tag: string; token: string }) {
return `docker run -d --network=host --ipc=host --group-add=video --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --device /dev/kfd --device /dev/dri gpustack/gpustack:${params.tag} --server-url ${params.server} --token ${params.token}`;
}
},
container: { container: {
getToken: getToken:
'docker run -it ${gpustack_container_id} cat /var/lib/gpustack/token' 'docker run -it ${gpustack_container_id} cat /var/lib/gpustack/token'
@@ -65,6 +70,7 @@ export const addWorkerGuide: Record<string, any> = {
export const containerInstallOptions = [ export const containerInstallOptions = [
{ label: 'CUDA', value: 'cuda' }, { label: 'CUDA', value: 'cuda' },
{ label: 'AMD', value: 'rocm' },
{ label: 'CANN', value: 'npu' }, { label: 'CANN', value: 'npu' },
{ label: 'MUSA', value: 'musa' }, { label: 'MUSA', value: 'musa' },
{ label: 'CPU', value: 'cpu' } { label: 'CPU', value: 'cpu' }