refactor(components): migrate shared components to core-ui and remove unused ones

This commit is contained in:
jialin
2026-06-24 14:02:41 +08:00
committed by jialin
parent 28976dea4b
commit ee42a9d0ed
29 changed files with 13 additions and 1639 deletions
@@ -1,68 +0,0 @@
import { IconFont } from '@gpustack/core-ui';
import { Collapse, CollapseProps } from 'antd';
import React from 'react';
import styled from 'styled-components';
const CollapseInner = styled(Collapse)`
.ant-collapse-header {
display: flex;
align-items: center;
margin-bottom: 10px !important;
padding-inline: 5px !important;
padding-block: 8px !important;
border-radius: var(--border-radius-base) !important;
font-size: 14px !important;
font-weight: 600 !important;
&:hover {
background-color: var(--ant-color-fill-tertiary) !important;
}
}
.ant-collapse-body {
padding-inline: 0 !important;
padding-block: 0 !important;
}
.ant-collapse-header-text {
display: flex;
align-items: center;
height: 24px;
}
`;
const CollapsePanel: React.FC<{
items: CollapseProps['items'];
activeKey: string | string[];
accordion?: boolean;
defaultActiveKey?: string | string[];
onChange?: (key: string | string[]) => void;
styles?: Record<string, React.CSSProperties>;
}> = ({ items, activeKey, accordion, defaultActiveKey, onChange, styles }) => {
return (
<CollapseInner
expandIconPlacement="start"
bordered={false}
ghost
accordion={accordion}
activeKey={activeKey}
defaultActiveKey={defaultActiveKey}
onChange={onChange}
destroyOnHidden={false}
styles={{
...styles,
header: {
backgroundColor: 'var(--ant-collapse-header-bg)'
}
}}
expandIcon={({ isActive }) => (
<IconFont
type="icon-down"
rotate={isActive ? 0 : -90}
style={{ fontSize: '14px' }}
></IconFont>
)}
items={items}
></CollapseInner>
);
};
export default CollapsePanel;
-250
View File
@@ -1,250 +0,0 @@
import {
readColumnSettings,
writeColumnSettings
} from '@/utils/localstore/index';
import { SettingOutlined } from '@ant-design/icons';
import { OverlayScroller } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Checkbox, Col, Popover, Row, Tooltip } from 'antd';
import React, { useEffect } from 'react';
import styled from 'styled-components';
const Container = styled.div`
padding: 8px 12px;
padding-right: 4px;
.title {
font-weight: 500;
margin-bottom: 12px;
}
.btn-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
}
.buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
}
`;
const Title = styled.div`
font-weight: 500;
margin-bottom: 8px;
margin-top: 4px;
`;
const LabelWrapper = styled.span`
color: var(--ant-color-text-secondary);
> span {
display: flex;
align-items: center;
gap: 4px;
color: var(--ant-color-text-secondary);
.sub-title {
display: none;
}
}
`;
const ColumnSettings: React.FC<{
width?: number;
fixedColumns?: string[];
tableName: string;
contentHeight: number;
columns: {
title: React.ReactNode;
dataIndex?: string;
children?: { title: React.ReactNode; dataIndex?: string }[];
}[];
selectedColumns?: string[];
defaultSelectedColumns?: string[];
grouped?: boolean;
onReset?: () => void;
onChange?: (selectedColumns: string[]) => void;
}> = (props) => {
const intl = useIntl();
const {
tableName,
contentHeight,
width = 420,
columns,
selectedColumns,
defaultSelectedColumns,
grouped,
onReset,
onChange,
fixedColumns
} = props;
const [open, setOpen] = React.useState(false);
const [innerSelectedColumns, setInnerSelectedColumns] = React.useState<
string[]
>(defaultSelectedColumns || []);
React.useEffect(() => {
if (open) {
setInnerSelectedColumns(selectedColumns ?? defaultSelectedColumns ?? []);
}
}, [open, selectedColumns, defaultSelectedColumns]);
const handleToggle = () => {
setOpen(!open);
};
const handleDraftChange = (columns: string[]) => {
setInnerSelectedColumns(columns);
};
const handleSelectAll = () => {
if (grouped) {
const allCols: string[] = [];
columns.forEach((group) => {
group.children?.forEach((col) => {
if (col.dataIndex) {
allCols.push(col.dataIndex);
}
});
});
handleDraftChange(allCols);
} else {
const allCols = columns
.map((col) => col.dataIndex)
.filter((dataIndex): dataIndex is string => Boolean(dataIndex));
handleDraftChange(allCols);
}
};
const handleConfirm = () => {
setOpen(false);
writeColumnSettings(tableName, innerSelectedColumns);
onChange?.(innerSelectedColumns);
};
const handleReset = () => {
const resetCols = defaultSelectedColumns ?? [];
writeColumnSettings(tableName, resetCols);
onChange?.(resetCols);
setInnerSelectedColumns(resetCols);
onReset?.();
};
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen);
};
useEffect(() => {
const initColumns = async () => {
const stored = await readColumnSettings(tableName);
if (stored && stored.length > 0) {
setInnerSelectedColumns(stored);
onChange?.(stored);
} else {
setInnerSelectedColumns(defaultSelectedColumns || []);
}
};
initColumns();
}, []);
const contentRender = () => {
return (
<Container>
{!grouped && (
<div className="title">
{intl.formatMessage({ id: 'benchmark.table.columnSettings' })}
</div>
)}
<OverlayScroller
maxHeight={contentHeight}
styles={{
wrapper: {
paddingInlineStart: 0
}
}}
>
<Checkbox.Group
value={innerSelectedColumns}
onChange={handleDraftChange}
>
<>
{grouped ? (
columns.map((row, index) => (
<div key={index}>
<Title>{row.title}</Title>
<Row>
{row.children?.map((col) => (
<Col key={col.dataIndex} span={12}>
<Checkbox
disabled={fixedColumns?.includes(
col.dataIndex || ''
)}
value={col.dataIndex}
style={{ marginBottom: 8 }}
>
<LabelWrapper>{col.title}</LabelWrapper>
</Checkbox>
</Col>
))}
</Row>
</div>
))
) : (
<Row>
{columns.map((col) => (
<Col key={col.dataIndex} span={12}>
<Checkbox
disabled={fixedColumns?.includes(col.dataIndex || '')}
value={col.dataIndex}
style={{ marginBottom: 8 }}
>
<LabelWrapper>{col.title}</LabelWrapper>
</Checkbox>
</Col>
))}
</Row>
)}
</>
</Checkbox.Group>
</OverlayScroller>
<div className="btn-wrapper">
<Button size="middle" onClick={handleReset}>
{intl.formatMessage({ id: 'common.button.resetdefault' })}
</Button>
<div className="buttons">
<Button size="middle" type="primary" onClick={handleSelectAll}>
{intl.formatMessage({ id: 'common.checkbox.all' })}
</Button>
<Button size="middle" type="primary" onClick={handleConfirm}>
{intl.formatMessage({ id: 'common.button.save' })}
</Button>
</div>
</div>
</Container>
);
};
return (
<Popover
open={open}
onOpenChange={handleOpenChange}
trigger={'click'}
arrow={false}
placement="bottomRight"
content={contentRender()}
styles={{
root: {
width: width
}
}}
>
<Tooltip
title={intl.formatMessage({ id: 'benchmark.table.columnSettings' })}
>
<Button onClick={handleToggle} icon={<SettingOutlined />}></Button>
</Tooltip>
</Popover>
);
};
export default ColumnSettings;
@@ -1,99 +0,0 @@
import { useOverlayScroller } from '@gpustack/core-ui';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import { WrapperContext } from './use-wrapper-context';
const Wrapper = styled.div`
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
width: 100%;
`;
const ContentWrapper = styled.div`
flex: 1;
position: relative;
overflow-y: auto;
`;
const Footer = styled.div`
padding-block: 0;
background-color: var(--ant-color-bg-elevated);
`;
interface ColumnWrapperProps {
children: React.ReactNode;
footer?: React.ReactNode;
maxHeight?: string | number;
paddingBottom?: number;
styles?: {
wrapper?: React.CSSProperties;
container?: React.CSSProperties;
};
}
const ColumnWrapper: React.FC<ColumnWrapperProps> = ({
children,
footer,
maxHeight,
styles = {}
}) => {
const scroller = React.useRef<any>(null);
const footerRef = React.useRef<HTMLDivElement>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
const {
initialize,
instance,
scrollEventElement,
scrollToBottom,
scrollToTarget,
getScrollElementScrollableHeight
} = useOverlayScroller({
options: {
scrollbars: {
autoHide: 'move'
}
}
});
React.useEffect(() => {
if (scroller.current) {
initialize(scroller.current);
}
}, []);
const setContentPaddingBottom = useCallback((padding: number) => {
contentRef.current!.style.paddingBottom = `${padding}px`;
}, []);
return (
<WrapperContext.Provider
value={{
scroller: scroller,
osInstance: instance,
scrollEventElement,
getScrollElementScrollableHeight,
scrollToBottom,
scrollToTarget,
setSScrollContentPaddingBottom: setContentPaddingBottom
}}
>
<Wrapper style={{ height: maxHeight || '100%', ...styles.wrapper }}>
<ContentWrapper
ref={scroller}
style={{
padding: '16px 24px',
...styles.container
}}
>
<div ref={contentRef}>{children}</div>
</ContentWrapper>
{footer && <Footer ref={footerRef}>{footer}</Footer>}
</Wrapper>
</WrapperContext.Provider>
);
};
export default ColumnWrapper;
@@ -1,28 +0,0 @@
import { createContext, useContext } from 'react';
interface WrapperContextProps {
osInstance?: any;
scroller?: any;
scrollEventElement?: any;
scrollToBottom?: () => void;
scrollToTop?: () => void;
getScrollElementScrollableHeight?: () => {
scrollHeight: number;
scrollTop: number;
};
scrollToTarget?: (target: any, offset?: number) => void;
getScrollElement?: () => HTMLElement | null;
setSScrollContentPaddingBottom?: (padding: number) => void;
}
export const WrapperContext = createContext<WrapperContextProps>(
{} as WrapperContextProps
);
export const useWrapperContext = () => {
const context = useContext(WrapperContext);
if (!context) {
throw new Error('useWrapperContext must be used within a WrapperProvider');
}
return context;
};
-155
View File
@@ -1,155 +0,0 @@
import { CloseOutlined } from '@ant-design/icons';
import { OverlayScroller } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd';
import classNames from 'classnames';
import React, { forwardRef, useImperativeHandle } from 'react';
import filterFormCss from '../styles/filter-form.less';
const FilterForm: React.FC<
React.PropsWithChildren & {
ref?: any;
width?: number;
contentHeight?: number | string;
initialValues?: any;
hasFilters?: boolean;
open?: boolean;
onClear?: () => void;
onClose?: () => void;
onValuesChange?: (ChangeValues: any, allValues: any) => void;
styles?: {
container?: React.CSSProperties;
wrapper?: React.CSSProperties;
};
}
> = forwardRef(
(
{
children,
open,
width = 300,
contentHeight = 400,
initialValues = {},
styles,
onClose,
onClear,
onValuesChange
},
ref
) => {
const intl = useIntl();
const [form] = Form.useForm();
const handleOnReset = () => {
form.resetFields(Object.keys(initialValues));
form.resetFields();
onValuesChange?.({}, form.getFieldsValue());
};
const handleOnClose = () => {
onClose?.();
};
const handleOnValuesChange = (changedValues: any, allValues: any) => {
onValuesChange?.(changedValues, allValues);
};
const handleOnClear = () => {
handleOnReset();
};
const filtersCount = Object.values(form.getFieldsValue()).filter(
(value) => value !== undefined && value !== null && value !== ''
).length;
const renderFooter = () => {
return (
<div className={filterFormCss['btn-wrapper']}>
<Button size="middle" onClick={handleOnReset}>
{intl.formatMessage({ id: 'common.button.reset' })}
</Button>
<div className={filterFormCss.buttons}>
<Button size="middle" type="primary" onClick={handleOnClose}>
{intl.formatMessage({ id: 'common.button.close' })}
</Button>
</div>
</div>
);
};
useImperativeHandle(ref, () => ({
form,
reset: handleOnReset,
getValues: () => form.getFieldsValue(),
setValues: (values: any) => form.setFieldsValue(values)
}));
return (
<div
className={classNames(filterFormCss.wrapper, {
[filterFormCss.show]: open
})}
style={{
width: open ? width : 0,
height: contentHeight,
...styles?.wrapper
}}
>
<div
style={{ width: width, ...styles?.container }}
className={filterFormCss.container}
>
<div className={filterFormCss.title}>
<span
style={{
fontWeight: 500,
color: 'var(--ant-color-text-tertiary)'
}}
>
{intl.formatMessage({ id: 'common.filter.label' })}
</span>
<Button
size="small"
icon={<CloseOutlined />}
onClick={handleOnClose}
type="text"
style={{
color: 'var(--ant-color-text-secondary)'
}}
></Button>
</div>
<OverlayScroller
maxHeight={contentHeight}
styles={{
wrapper: {
paddingInline: 8
}
}}
>
<Form
onValuesChange={handleOnValuesChange}
initialValues={initialValues}
form={form}
layout="vertical"
styles={{
label: {
lineHeight: 1,
height: 'auto',
marginBottom: 8,
fontWeight: 500
},
content: {
minHeight: 0
}
}}
>
{children}
</Form>
</OverlayScroller>
</div>
</div>
);
}
);
export default FilterForm;
-90
View File
@@ -1,90 +0,0 @@
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import type { DrawerProps } from 'antd';
import { Tag } from 'antd';
import React from 'react';
const ModalFooterStyle = {
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
};
type AddModalProps = {
title: React.ReactNode;
open: boolean;
onCancel?: () => void;
children?: React.ReactNode;
onSubmit?: () => void;
width?: number | string;
footer?: React.ReactNode;
subTitle?: React.ReactNode;
push?: DrawerProps['push'];
};
const FormDrawer: React.FC<AddModalProps> = ({
title,
open,
onCancel,
onSubmit,
children,
width = 600,
subTitle,
footer,
push
}) => {
return (
<GSDrawer
title={
<>
{title}
{subTitle && (
<Tag
variant="outlined"
style={{
fontSize: 12,
fontWeight: 400,
marginLeft: 8,
borderRadius: 4,
borderColor: 'var(--ant-color-border-secondary)',
color: 'var(--ant-color-text-secondary)'
}}
>
{subTitle}
</Tag>
)}
</>
}
open={open}
onClose={onCancel}
destroyOnHidden={true}
closeIcon={false}
mask={{
closable: false
}}
keyboard={false}
push={push}
styles={{
wrapper: { width }
}}
footer={false}
>
<ColumnWrapper
styles={{
container: { paddingBlock: 0 }
}}
footer={
footer ?? (
<ModalFooter
onOk={onSubmit}
onCancel={onCancel}
style={ModalFooterStyle}
></ModalFooter>
)
}
>
{children}
</ColumnWrapper>
</GSDrawer>
);
};
export default FormDrawer;
@@ -1,81 +0,0 @@
.mask {
position: fixed;
inset: 0;
z-index: 1004;
height: 100%;
background-color: var(--ant-color-bg-mask);
opacity: 0;
transition: opacity var(--ant-motion-duration-slow)
var(--ant-motion-ease-in-out);
}
.maskOpen {
opacity: 1;
}
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: auto;
left: auto;
z-index: 1005;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
background: var(--ant-color-bg-elevated);
border-radius: var(--ant-border-radius-lg) 0 0 var(--ant-border-radius-lg);
box-shadow:
-6px 0 16px 0 rgb(0 0 0 / 8%),
-3px 0 6px -4px rgb(0 0 0 / 12%),
-9px 0 28px 8px rgb(0 0 0 / 5%);
transform: translateX(100%);
transition: transform var(--ant-motion-duration-slow)
var(--ant-motion-ease-in-out);
}
.overlayOpen {
transform: translateX(0);
}
.header {
display: flex;
align-items: center;
gap: 12px;
min-height: 56px;
padding: var(--ant-padding) var(--ant-padding-lg);
border-bottom: 1px solid var(--ant-color-split);
color: var(--ant-color-text);
font-size: 14px;
font-weight: 600;
line-height: 24px;
}
.title {
display: flex;
min-width: 0;
align-items: center;
}
.subTitle {
margin-left: 8px;
border-color: var(--ant-color-border-secondary);
border-radius: 4px;
color: var(--ant-color-text-secondary);
font-size: 12px;
font-weight: 400;
}
.content {
padding-block: 16px;
flex: 1;
height: calc(100vh - 57px);
overflow-y: auto;
}
.footer {
display: flex;
justify-content: flex-end;
padding: 16px 24px 8px;
}
-122
View File
@@ -1,122 +0,0 @@
import { ColumnWrapper, IconFont } from '@gpustack/core-ui';
import { Button, Tag } from 'antd';
import classNames from 'classnames';
import React from 'react';
import { createPortal } from 'react-dom';
import styles from './form-overlay-view.module.less';
type FormOverlayViewProps = {
title: React.ReactNode;
open: boolean;
onCancel?: () => void;
children?: React.ReactNode;
onSubmit?: () => void;
footer?: React.ReactNode;
subTitle?: React.ReactNode;
width?: number | string;
className?: string;
style?: React.CSSProperties;
maskClosable?: boolean;
getContainer?: () => HTMLElement | null | undefined;
};
const FormOverlayView: React.FC<FormOverlayViewProps> = ({
title,
open,
onCancel,
onSubmit,
children,
subTitle,
footer,
width = 600,
className,
style,
maskClosable = false,
getContainer
}) => {
const [container, setContainer] = React.useState<HTMLElement | null>(null);
const [mounted, setMounted] = React.useState(false);
const [active, setActive] = React.useState(false);
React.useEffect(() => {
if (open) {
setContainer(getContainer?.() ?? null);
setMounted(true);
return;
}
setActive(false);
}, [getContainer, open]);
React.useEffect(() => {
if (!mounted || !container) {
return;
}
const id = requestAnimationFrame(() => setActive(true));
return () => cancelAnimationFrame(id);
}, [mounted, container]);
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
if (e.target !== e.currentTarget || e.propertyName !== 'transform') {
return;
}
if (!open && !active) {
setMounted(false);
setContainer(null);
}
};
if (!mounted || !container) {
return null;
}
return createPortal(
<>
<div
className={classNames(styles.mask, { [styles.maskOpen]: active })}
onClick={maskClosable ? onCancel : undefined}
/>
<div
className={classNames(
styles.overlay,
{ [styles.overlayOpen]: active },
className
)}
style={{ width, ...style }}
role="dialog"
aria-modal="true"
onTransitionEnd={handleTransitionEnd}
>
<div className={styles.header}>
<Button
type="text"
size="small"
style={{ fontWeight: 600, fontSize: 16 }}
icon={<IconFont type="icon-down2" rotate={90} />}
onClick={onCancel}
/>
<div className={styles.title}>
{title}
{subTitle && (
<Tag variant="outlined" className={styles.subTitle}>
{subTitle}
</Tag>
)}
</div>
</div>
<ColumnWrapper
styles={{
container: { paddingBlock: 16 }
}}
footer={footer}
>
{children}
</ColumnWrapper>
</div>
</>,
container
);
};
export default FormOverlayView;
File diff suppressed because one or more lines are too long
-26
View File
@@ -1,26 +0,0 @@
import { update } from 'jdenticon';
import React, { useEffect, useRef } from 'react';
interface IdenticonProps {
value: string;
size: number;
}
const Identicon: React.FC<IdenticonProps> = ({
value = 'test',
size = 28
}: IdenticonProps) => {
const icon = useRef<any>(null);
useEffect(() => {
update(icon.current, value);
}, [value]);
return (
<>
<svg data-jdenticon-value={value} height={size} ref={icon} width={size} />
</>
);
};
export default Identicon;
@@ -1,68 +0,0 @@
import {
digitReg,
lowercaseReg,
specialCharacterReg,
uppercaseReg
} from '@/config';
import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Space } from 'antd';
const PasswordValidate: React.FC<{ value: string }> = ({ value = '' }) => {
const intl = useIntl();
const renderIcon = ({ valid, text }: { valid: boolean; text: string }) => {
return (
<>
{valid ? (
<CheckCircleFilled style={{ color: 'green' }} />
) : (
<CloseCircleFilled style={{ color: 'red' }} />
)}
<span
className="m-l-5"
style={{ color: 'var(--ant-color-text-description)' }}
>
{text}
</span>
</>
);
};
return (
<Space direction="vertical" style={{ paddingTop: '10px' }}>
<span>
{renderIcon({
valid: uppercaseReg.test(value),
text: intl.formatMessage({ id: 'users.password.uppcase' })
})}
</span>
<span>
{renderIcon({
valid: lowercaseReg.test(value),
text: intl.formatMessage({ id: 'users.password.lowercase' })
})}
</span>
<span>
{renderIcon({
valid: digitReg.test(value),
text: intl.formatMessage({ id: 'users.password.number' })
})}
</span>
<span>
{renderIcon({
valid: value.length >= 6 && value.length <= 12,
text: intl.formatMessage({ id: 'users.password.length' })
})}
</span>
<span>
{renderIcon({
valid: specialCharacterReg.test(value),
text: intl.formatMessage({ id: 'users.password.special' })
})}
</span>
</Space>
);
};
export default PasswordValidate;
@@ -1,60 +0,0 @@
import { AutoTooltip } from '@gpustack/core-ui';
import React from 'react';
import pillButtonCss from './styles.less';
type Option = {
label: string;
value: string | number | null;
icon?: React.ReactNode;
};
type Props = {
value?: string | number;
onChange?: (value: string | number | undefined | null) => void;
options: Option[];
disabled?: boolean;
variant?: 'filled' | 'outlined' | 'solid';
};
const PillButtonGroup: React.FC<Props> = ({
value,
onChange,
options,
disabled,
variant = 'outlined'
}) => {
return (
<div className={pillButtonCss['wrapper']}>
{options.map((item) => {
const active = value === item.value;
return (
<AutoTooltip
ghost
title={item.label}
key={item.value}
maxWidth={'100%'}
>
<span
className={`${pillButtonCss['pill-item']} ${active ? pillButtonCss['active'] : ''}`}
key={item.value}
color="default"
onClick={() => {
if (item.value !== value) {
onChange?.(item.value);
}
if (item.value === value) {
onChange?.(undefined);
}
}}
>
{item.icon}
{item.label}
</span>
</AutoTooltip>
);
})}
</div>
);
};
export default PillButtonGroup;
@@ -1,31 +0,0 @@
.pill-item {
height: 28px;
line-height: 28px;
display: flex;
align-items: center;
gap: 8px;
justify-content: center;
border-radius: var(--ant-border-radius);
background-color: var(--ant-color-fill-tertiary);
color: var(--ant-color-text-secondary);
padding: 0 8px;
cursor: pointer;
transition: all 0.3s;
&.active {
background-color: var(--ant-color-fill);
color: var(--ant-color-text);
font-weight: 500;
}
&:not(.active):hover {
background-color: var(--ant-color-fill-secondary);
}
}
.wrapper {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
align-items: center;
}
@@ -1,115 +0,0 @@
import { SegmentLine } from '@gpustack/core-ui';
import { useMemoizedFn } from 'ahooks';
import _ from 'lodash';
import React, { forwardRef, useImperativeHandle } from 'react';
import styled from 'styled-components';
import useFieldScroll from './use-field-scroll';
const SegmentedHeader = styled.div<{ $top?: number }>`
position: sticky;
top: ${(props) => props.$top || 0}px;
z-index: 10;
margin-bottom: 16px;
border-bottom: 1px solid var(--ant-color-split);
background-color: var(--ant-color-bg-elevated);
`;
/**
* ScrollSpyTabs component
* defaultTarget: The default active target tab
* segmentedTop: { // Mostlty, It's always a constants.
* top: number; // The top offset for the sticky header
* offsetTop: number; // The offset top for the target
* }
* getScrollElementScrollableHeight: function to get the scrollable height of the scroll element
* segmentOptions.field: The target data-field={segmentOptions.field} to scroll to
* activeKey: The current active keys for collapsible sections
* setActiveKey: The function to set active keys for collapsible sections
*/
interface ScrollSpyTabsProps {
ref?: any;
children?: React.ReactNode;
defaultTarget?: string;
segmentedTop: {
top: number;
offsetTop: number;
};
activeKey: string[];
setActiveKey: (keys: string[]) => void;
getScrollElementScrollableHeight?: () => {
scrollHeight: number;
scrollTop: number;
};
segmentOptions: {
label: string;
value: string;
icon?: React.ReactNode;
field: string;
}[];
}
const ScrollSpyTabs: React.FC<ScrollSpyTabsProps> = forwardRef(
(
{
getScrollElementScrollableHeight,
segmentedTop,
segmentOptions,
defaultTarget,
activeKey,
setActiveKey,
children
},
ref
) => {
const [target, setTarget] = React.useState<string>(
defaultTarget || segmentOptions[0]?.value || ''
);
const { scrollToSegment, holderHeight } = useFieldScroll({
activeKey,
setActiveKey,
segmentOptions,
segmentedTop: segmentedTop,
getScrollElementScrollableHeight: getScrollElementScrollableHeight
});
const throttleScrollToSegment = useMemoizedFn(
_.throttle(
async (val: string) => {
setTarget(val);
scrollToSegment(val, { offsetTop: segmentedTop.offsetTop });
},
500,
{ trailing: true }
)
);
const handleTargetChange = async (val: any) => {
throttleScrollToSegment(val);
};
useImperativeHandle(ref, () => ({
handleTargetChange
}));
return (
<div>
{segmentOptions.length > 0 && (
<SegmentedHeader $top={segmentedTop.top}>
<SegmentLine
theme={'light'}
defaultValue={target}
value={target}
onChange={handleTargetChange}
options={segmentOptions}
/>
</SegmentedHeader>
)}
{children}
<div className="holder" style={{ height: holderHeight }}></div>
</div>
);
}
);
export default ScrollSpyTabs;
@@ -1,118 +0,0 @@
import { useMemoizedFn } from 'ahooks';
import { useCallback, useRef, useState } from 'react';
interface ScrollOptions {
wait?: number;
behavior?: 'smooth' | 'auto';
block?: 'start' | 'end' | 'center';
offsetTop?: number;
}
export default function useScrollAfterExpand({
activeKey,
setActiveKey,
segmentOptions,
defaultWait = 300,
segmentedTop = { top: 0, offsetTop: 96 },
getScrollElementScrollableHeight
}: {
activeKey: string[];
setActiveKey: (keys: string[]) => void;
segmentOptions: { value: string; field: string }[];
getScrollElementScrollableHeight?: () => {
scrollHeight: number;
scrollTop: number;
};
defaultWait?: number;
segmentedTop: {
top: number; // The top offset for the sticky header
offsetTop: number; // The offset top for the target
};
}) {
const [holderHeight, setHolderHeight] = useState<number>(0);
const boxHeightRef = useRef<number>(0);
const scrollToElement = useCallback(
(
el: HTMLElement,
{ behavior = 'smooth', offsetTop = 0 }: ScrollOptions = {}
) => {
// find the nearest scrollable parent
const scrollParent = (() => {
let node: HTMLElement | null = el;
while (node) {
const { overflowY } = getComputedStyle(node);
if (overflowY === 'auto' || overflowY === 'scroll') return node;
node = node.parentElement;
}
return document.scrollingElement || document.documentElement;
})();
const parentRect = scrollParent.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const top =
elRect.top - parentRect.top + scrollParent.scrollTop - offsetTop;
scrollParent.scrollTo({ top, behavior });
},
[]
);
/**
* due to the scrollheight changes after expanding the segment and including the holder height.
*
*/
const scrollToSegment = useMemoizedFn(
async (val: string, options?: ScrollOptions) => {
if (!activeKey.includes(val)) {
setActiveKey([...activeKey, val]);
await new Promise((r) => {
setTimeout(r, options?.wait ?? defaultWait);
});
}
const current = segmentOptions.find((item) => item.value === val);
if (!current?.field) return;
await new Promise(requestAnimationFrame);
const el: HTMLElement | null = document.querySelector(
`[data-field="${current.field}"]`
) as HTMLElement | null;
const targetRectTop = el?.getBoundingClientRect().top || 0;
const scroller = getScrollElementScrollableHeight?.() || {
scrollHeight: 0,
scrollTop: 0
};
// remaining scroll height
const remainingScrollHeight = scroller.scrollHeight - scroller.scrollTop;
// total distance from the top of the scroller to the target element
const offsetDistance =
targetRectTop - segmentedTop.offsetTop - segmentedTop.top;
let boxHeight = 0;
// verify boxHeight is correct, if setting the boxHeight causes the element to be hidden, use the previous boxHeight
if (offsetDistance <= 0) {
boxHeight = boxHeightRef.current;
} else {
boxHeight =
offsetDistance - remainingScrollHeight + boxHeightRef.current;
}
// update boxHeightRef
boxHeightRef.current = boxHeight;
setHolderHeight(boxHeight);
await new Promise(requestAnimationFrame);
if (el) scrollToElement(el, options);
}
);
return { scrollToSegment, holderHeight };
}
@@ -1,44 +0,0 @@
interface FinishFailedOptions {
requiredFields: {
[tab: string]: {
sort: number;
fields: string[];
};
};
onTargetChange: (key: string) => void;
updateActiveKey: (key: string[]) => void;
}
const useFinishFailed = (options: FinishFailedOptions) => {
const { requiredFields, onTargetChange, updateActiveKey } = options;
const handleOnFinishFailed = (errorInfo: any) => {
const { errorFields } = errorInfo;
console.log('Finish failed:', errorInfo);
if (errorFields && errorFields.length > 0) {
const collapseKeys: { sort: number; key: string }[] = [];
const names = errorFields.map((item: any) => item.name[0]);
Object.entries(requiredFields).forEach(([tab, { fields, sort }]) => {
const hasError = fields.some((field: string) => names.includes(field));
if (hasError) {
collapseKeys.push({ sort, key: tab });
}
});
if (collapseKeys.length > 0) {
const keys = collapseKeys
.sort((a, b) => a.sort - b.sort)
.map((item) => item.key);
updateActiveKey(keys);
onTargetChange(collapseKeys[0].key);
}
}
};
return {
handleOnFinishFailed
};
};
export default useFinishFailed;
@@ -1,40 +0,0 @@
import { useState } from 'react';
const useScrollActiveChange = (options: {
initalActiveKeys: string[];
initialCollapseKeys?: string[];
}) => {
const [activeKey, setActiveKey] = useState<string[]>(
options.initalActiveKeys
);
const [collapseKeys, setCollapseKeys] = useState<string[]>(
options.initialCollapseKeys || options.initalActiveKeys
);
const handleActiveChange = (key: string[]) => {
setActiveKey(key);
setCollapseKeys(key);
};
const handleOnCollapseChange = (keys: string | string[]) => {
const keysArray = Array.isArray(keys) ? keys : [keys];
setActiveKey(keysArray);
setCollapseKeys(keysArray);
};
const updateActiveKey = (keys: string[]) => {
setActiveKey((prev: string[]) => [...new Set([...prev, ...keys])]);
setCollapseKeys((prev) => [...new Set([...prev, ...keys])]);
};
return {
activeKey,
collapseKeys,
setCollapseKeys,
handleActiveChange,
handleOnCollapseChange,
updateActiveKey
};
};
export default useScrollActiveChange;
@@ -1,36 +0,0 @@
import { CloseCircleFilled } from '@ant-design/icons';
import { Button } from 'antd';
import styled from 'styled-components';
const StyledButton = styled(Button)`
background-color: transparent !important;
padding: 0;
.anticon {
color: var(--ant-color-text-quaternary);
font-size: 12px !important;
transition: color 0.3s ease;
}
&:hover {
.anticon {
color: var(--ant-color-text-tertiary);
}
}
`;
interface SmallCloseButtonProps {
onClick?: () => void;
}
const SmallCloseButton: React.FC<SmallCloseButtonProps> = ({ onClick }) => {
return (
<StyledButton
icon={<CloseCircleFilled />}
shape="circle"
type="text"
onClick={onClick}
size="small"
></StyledButton>
);
};
export default SmallCloseButton;
@@ -1,41 +0,0 @@
.wrapper {
flex-shrink: 0;
overflow: hidden;
width: 0;
transition: all var(--ant-motion-duration-slow) var(--ant-motion-ease-in-out);
border-color: var(--ant-color-split);
&.show {
width: 232px;
border-right: 1px solid var(--ant-color-split);
}
}
.container {
padding: 0 16px;
.title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
margin-top: 8px;
padding-inline: 8px;
font-weight: 500;
color: var(--ant-color-text-tertiary);
}
.btn-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 24px;
padding-right: 8px;
}
.buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
}
}
-109
View File
@@ -1,109 +0,0 @@
import { MoreOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Transfer, TransferProps } from 'antd';
import styled from 'styled-components';
type TransferKey = string | number | bigint;
const TransferWrap = styled.div`
.ant-transfer-section {
width: 100%;
height: 300px;
.anticon-more {
display: none;
}
.ant-input-outlined {
height: 32px;
padding-block: 4px;
border-radius: 4px;
}
}
.ant-transfer-actions {
margin: 0 16px;
gap: 12px;
.ant-btn-icon-only {
width: 32px;
height: 32px;
border-radius: 50%;
}
}
.ant-transfer-list-content {
&::-webkit-scrollbar {
width: var(--scrollbar-size);
}
&::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
border-radius: 4px;
}
}
.ant-transfer-list-content-item {
&:hover {
background-color: var(--ant-control-item-bg-hover);
}
&.ant-transfer-list-content-item-checked {
background-color: unset;
&:hover {
background-color: var(--ant-control-item-bg-hover);
}
}
}
}
.ant-pagination {
justify-content: center;
}
`;
interface TransferInnerProps extends TransferProps {
total?: number;
perPage?: number;
onPageChange?: (page: number, perPage?: number) => void;
dataSource?: Array<{ key: TransferKey; title: string }>;
targetKeys?: TransferKey[];
}
const TransferInner: React.FC<TransferInnerProps> = (props) => {
const intl = useIntl();
const renderAllLabels = (info: {
selectedCount: number;
totalCount: number;
}) => {
if (info.selectedCount) {
return (
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
{intl.formatMessage(
{ id: 'common.select.count' },
{ count: info.selectedCount }
)}
</span>
);
}
return null;
};
return (
<TransferWrap>
<Transfer
{...props}
selectAllLabels={
props.selectAllLabels || [renderAllLabels, renderAllLabels]
}
selectionsIcon={
<MoreOutlined style={{ fontSize: 14, marginBottom: 3 }} />
}
></Transfer>
</TransferWrap>
);
};
export default TransferInner;
+1 -1
View File
@@ -3,6 +3,7 @@ import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import {
AlertBlockInfo,
ColumnWrapper,
GSDrawer,
IconFont,
ModalFooter,
@@ -13,7 +14,6 @@ import { Tabs } from 'antd';
import _ from 'lodash';
import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import ColumnWrapper from '../../_components/column-wrapper';
import {
BackendSourceValueMap,
builtInBackendFields,
@@ -1,6 +1,5 @@
import { PageAction } from '@/config';
import FormOverlayView from '@/pages/_components/form-overlay-view';
import { ModalFooter } from '@gpustack/core-ui';
import { ModalFooter, SubDrawer } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useRef, useState } from 'react';
import { FormData as PublicKeyFormData } from '../../public-keys/config/types';
@@ -45,7 +44,7 @@ const PublicKeyOverlay: React.FC<PublicKeyOverlayProps> = ({
};
return (
<FormOverlayView
<SubDrawer
title={intl.formatMessage({ id: 'gpuservice.publicKey.add' })}
open={open}
width={drawerWidth}
@@ -70,7 +69,7 @@ const PublicKeyOverlay: React.FC<PublicKeyOverlayProps> = ({
open={open}
onFinish={handleFinish}
/>
</FormOverlayView>
</SubDrawer>
);
};
@@ -1,8 +1,7 @@
import { PageAction } from '@/config';
import FormOverlayView from '@/pages/_components/form-overlay-view';
import { FormContext } from '@/pages/gpu-service/storage/config/form-context';
import useQueryStorageClass from '@/pages/gpu-service/storage/services/use-query-storage-class';
import { ModalFooter } from '@gpustack/core-ui';
import { ModalFooter, SubDrawer } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useEffect, useRef, useState } from 'react';
import { FormData as StorageFormData } from '../../storage/config/types';
@@ -52,7 +51,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
};
return (
<FormOverlayView
<SubDrawer
title={intl.formatMessage({ id: 'gpuservice.storage.add' })}
open={open}
width={drawerWidth}
@@ -79,7 +78,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
onFinish={handleFinish}
/>
</FormContext.Provider>
</FormOverlayView>
</SubDrawer>
);
};
@@ -1,8 +1,7 @@
import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui';
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormData, ListItem } from '../config/types';
import GPUServicePublicKeyForm from '../forms';
@@ -1,8 +1,7 @@
import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui';
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormData, ListItem } from '../config/types';
import GPUServiceStorageTypeForm from '../forms';
@@ -1,8 +1,7 @@
import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui';
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormContext } from '../config/form-context';
import { FormData, ListItem } from '../config/types';
import GPUServiceStorageForm from '../forms';
@@ -1,8 +1,7 @@
import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui';
import { FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormData, ListItem } from '../config/types';
import GPUServiceTemplateForm from '../forms';
@@ -2,14 +2,13 @@ import { getRequestId } from '@/atoms/models';
import { PageActionType } from '@/config/types';
import useDeferredRequest from '@/hooks/use-deferred-request';
import { ClusterStatusValueMap } from '@/pages/cluster-management/config';
import { GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { Button } from 'antd';
import _ from 'lodash';
import { FC, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import ColumnWrapper from '../../../_components/column-wrapper';
import {
defaultFormValues,
DeployFormKeyMap,
@@ -1,10 +1,9 @@
import { ProviderValueMap } from '@/pages/cluster-management/config';
import { GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { debounce } from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import ColumnWrapper from '../../../_components/column-wrapper';
import { modelSourceMap } from '../../config';
import { FormData } from '../../config/types';
import HFModelFile from '../model-source/hf-model-file';