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:
specifier: ^1.4.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:
specifier: ^3.14.8
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}
deprecated: This is a stub types definition. history provides its own type definitions, so you do not need this installed.
dependencies:
history: 4.10.1
history: 5.3.0
dev: false
/@types/hoist-non-react-statics@3.3.5:
@@ -5354,7 +5357,7 @@ packages:
'@types/history': 4.7.11
'@types/react': 18.3.16
'@types/react-router': 5.1.20
redux: 3.7.2
redux: 4.2.1
dev: false
/@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}
engines: {node: '>=0.11'}
dependencies:
'@babel/runtime': 7.25.7
'@babel/runtime': 7.26.0
dev: false
/dayjs@1.11.11:
@@ -15567,6 +15570,22 @@ packages:
react-dom: 18.2.0(react@18.2.0)
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):
resolution: {integrity: sha512-lmw2j8I2fhdIzHmC9ajfImfckt0WDb2KVJJBBRIsxPEw2kGkEfjLMUoB1NgiNT/Q5cC8PdjGOGQjHJIJMwyNMw==, tarball: https://registry.npmjs.org/rc-menu/-/rc-menu-9.8.4.tgz}
peerDependencies:
@@ -16050,6 +16069,24 @@ packages:
react-dom: 18.2.0(react@18.2.0)
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):
resolution: {integrity: sha512-IQPd1CDI3mnMlkFyzt2O4gQ2lxUsnBAeJEoZGJnkkXgORNqyM9qovdrCj9NzcRfpHgLdzaEbU3AmobNFGUznwQ==, tarball: https://registry.npmjs.org/rc-textarea/-/rc-textarea-0.4.7.tgz}
peerDependencies:
@@ -16804,7 +16841,7 @@ packages:
/regenerator-transform@0.15.2:
resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==, tarball: https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz}
dependencies:
'@babel/runtime': 7.25.7
'@babel/runtime': 7.26.0
dev: false
/regexp-tree@0.1.27:
+3 -3
View File
@@ -11,13 +11,13 @@
:global {
.copy-button-wrapper {
display: none;
background-color: #383838;
background-color: rgba(255, 255, 255, 20%);
position: absolute;
z-index: 10;
right: -28px;
right: 0;
top: 0;
padding: 2px;
border-radius: 4px;
border-radius: 0 4px;
}
.simplebar-content {
+4 -2
View File
@@ -28,6 +28,7 @@ type StatusTagProps = {
download?: {
percent: number;
};
extra?: React.ReactNode;
actions?: {
label: string;
icon?: React.ReactNode;
@@ -40,6 +41,7 @@ type StatusTagProps = {
const StatusTag: React.FC<StatusTagProps> = ({
statusValue,
download,
extra,
actions = [],
type = 'tag'
}) => {
@@ -78,7 +80,6 @@ const StatusTag: React.FC<StatusTagProps> = ({
style={{ color: 'rgba(255,255,255,.8)' }}
text={statusValue.message || ''}
size="small"
placement="right"
></CopyButton>
{actions?.map((item) => {
return (
@@ -104,7 +105,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
})}
</div>
<SimpleBar style={{ maxHeight: 200 }}>
<SimpleBar style={{ maxHeight: 200 }} autoHide={false}>
<div
style={{
width: 'max-content',
@@ -115,6 +116,7 @@ const StatusTag: React.FC<StatusTagProps> = ({
}}
>
{statusValue.message}
{extra}
</div>
</SimpleBar>
</div>
+4
View File
@@ -13,4 +13,8 @@
height: 22px;
opacity: 0.7;
}
.tags-content {
display: flex;
}
}
+104 -77
View File
@@ -1,99 +1,126 @@
import { MoreOutlined } from '@ant-design/icons';
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';
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 observer = useRef<IntersectionObserver | null>(null);
const resizeObserver = useRef<ResizeObserver | null>(null);
const [hiddenIndex, setHiddenIndex] = useState<number>(4);
const uid = useRef(0);
const moreBtnRef = useRef<HTMLDivElement>(null);
const moreButtonWidth = useRef(0);
const nodeSizeList = useRef<number[]>([]);
const updateUid = () => {
uid.current += 1;
return uid.current;
};
const calculateHiddenIndices = () => {
const wrapperWidth = wrapperRef.current?.offsetWidth || 0;
const childNodes = tagsContentRef.current?.childNodes;
let sizeList = nodeSizeList.current;
const dropItems = useMemo(() => {
return React.Children.toArray(children)
.slice(hiddenIndex)
.map((child) => ({
label: child,
key: updateUid()
}));
}, [children, hiddenIndex]);
if (!sizeList.length && childNodes?.length) {
sizeList = _.map(childNodes, (node: HTMLDivElement, index: number) => {
if (index === childNodes.length - 1) {
return node.offsetWidth;
}
return node.offsetWidth + gap;
});
nodeSizeList.current = sizeList;
}
const updateHiddenIndex = () => {
if (!wrapperRef.current || !observer.current) return;
if (wrapperWidth === 0 || !sizeList.length) return;
const childrenList = Array.from(wrapperRef.current.children);
let newHiddenIndex = 0;
// cache more button width
if (moreBtnRef.current?.offsetWidth) {
moreButtonWidth.current = moreBtnRef.current?.offsetWidth + gap;
}
childrenList.forEach((child, index) => {
const rect = child.getBoundingClientRect();
// visible in wrapperRef
const issivible =
rect.top < wrapperRef.current!.clientHeight &&
rect.bottom > 0 &&
rect.left < wrapperRef.current!.clientWidth &&
rect.right > 0;
let totalWidth = 0;
let start = 0;
let end = dataList.length;
if (!issivible) {
newHiddenIndex = Math.max(newHiddenIndex, index + 1);
for (let i = 0; i < sizeList.length; i++) {
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(() => {
if (!wrapperRef.current) return;
if (tagsContentRef.current) {
calculateHiddenIndices();
}
}, [dataList, gap]);
// observer.current = new IntersectionObserver(
// (entries) => {
// const lastEntry = entries[entries.length - 1];
// 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]);
const handleContentResize = _.throttle(() => {
calculateHiddenIndices();
}, 200);
return (
<div className="tags-wrapper" ref={wrapperRef}>
{React.Children.toArray(children).slice(0, hiddenIndex)}
{React.Children.toArray(children).length > 4 && (
<Dropdown
trigger={['hover']}
menu={{
items: dropItems
}}
>
<Tag className="more">
<MoreOutlined rotate={90} />
</Tag>
</Dropdown>
)}
</div>
<ResizeObserver onResize={handleContentResize}>
<div className="tags-wrapper" ref={wrapperRef}>
<ResizeObserver onResize={handleContentResize}>
<div className="tags-content" ref={tagsContentRef} style={{ gap }}>
{_.map(
_.slice(dataList, hiddenIndices.start, hiddenIndices.end),
(item: any, index: number) => {
return <span key={index}>{renderTag?.(item)}</span>;
}
)}
</div>
</ResizeObserver>
{hiddenIndices.end < dataList.length && (
<Dropdown
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':
'If using macOS or Windows as a worker, check GGUF (uncheck for audio models).',
'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':
'当 macOS 或 Windows 作 worker 时勾选 GGUF(搜索音频模型时取消勾选)',
'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;
};
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 (
<div
onClick={handleOnClick}
@@ -81,7 +103,7 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
<span className="flex-center">
{_.map(data.licenses, (license: string, index: number) => {
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>
<span>{license}</span>
</span>
@@ -113,15 +135,11 @@ const CatalogItem: React.FC<CatalogItemProps> = (props) => {
<>
<span className="dot"></span>
<div className="box">
<TagWrapper>
{data.sizes.map((sItem, i) => {
return (
<Tag key={sItem} className="tag-item">
{sItem}B
</Tag>
);
})}
</TagWrapper>
<TagWrapper
gap={8}
dataList={data.sizes}
renderTag={renderTag}
></TagWrapper>
</div>
</>
)}
@@ -15,7 +15,7 @@ import {
ThunderboltFilled
} from '@ant-design/icons';
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 _ from 'lodash';
import React, { useCallback } from 'react';
@@ -282,6 +282,21 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
? { percent: item.download_progress }
: 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={{
status:
item.state === InstanceStatusMap.Downloading &&
+2 -1
View File
@@ -31,7 +31,8 @@ const PlaygroundEmbedding: React.FC = () => {
const getModelListByEmbedding = async () => {
try {
const params = {
categories: modelCategoriesMap.embedding
categories: modelCategoriesMap.embedding,
with_meta: true
};
const res = await queryModelsList(params);
const list = _.map(res.data || [], (item: any) => {
+2 -1
View File
@@ -33,7 +33,8 @@ const PlaygroundRerank: React.FC = () => {
const getModelListByReranker = async () => {
try {
const params = {
categories: modelCategoriesMap.reranker
categories: modelCategoriesMap.reranker,
with_meta: true
};
const res = await queryModelsList(params);
const list = _.map(res.data || [], (item: any) => {
@@ -59,7 +59,6 @@ const AddWorker: React.FC<ViewModalProps> = (props) => {
{ version: versionInfo.version }
)}
</li>
<li>{intl.formatMessage({ id: 'resources.worker.select.command' })}</li>
<li>
<span>
{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}`;
}
},
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: {
getToken:
'docker run -it ${gpustack_container_id} cat /var/lib/gpustack/token'
@@ -65,6 +70,7 @@ export const addWorkerGuide: Record<string, any> = {
export const containerInstallOptions = [
{ label: 'CUDA', value: 'cuda' },
{ label: 'AMD', value: 'rocm' },
{ label: 'CANN', value: 'npu' },
{ label: 'MUSA', value: 'musa' },
{ label: 'CPU', value: 'cpu' }