feat: community backends page
This commit is contained in:
@@ -72,6 +72,7 @@ interface Configuration {
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
// default need to pass content and operation
|
||||
const DeleteModal = forwardRef((props, ref) => {
|
||||
const intl = useIntl();
|
||||
const { styles } = useStyles();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
|
||||
// import './iconfont/iconfont.js';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_339f1qeidmn.js'
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_mis8x88kojd.js'
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -156,6 +156,11 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
|
||||
{handleClickPrimary ? (
|
||||
actionType === 'dropdown' ? (
|
||||
<DropDownActions
|
||||
styles={{
|
||||
root: {
|
||||
minWidth: 140
|
||||
}
|
||||
}}
|
||||
menu={{
|
||||
items: actionItems,
|
||||
onClick: handleClickPrimary
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Skeleton } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ResizeContainer from '../resize-container';
|
||||
|
||||
const SkeletonWrapper = styled.div`
|
||||
.ant-skeleton-paragraph {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
interface CatalogSkeltonProps {
|
||||
skeletonProps?: any;
|
||||
skeletonStyle?: React.CSSProperties;
|
||||
@@ -12,19 +19,21 @@ const CardSkelton: React.FC<CatalogSkeltonProps> = (props) => {
|
||||
<ResizeContainer
|
||||
dataList={Array(6).fill({ label: 'skeleton' })}
|
||||
renderItem={() => (
|
||||
<Skeleton
|
||||
avatar={{
|
||||
size: 32
|
||||
}}
|
||||
paragraph={{ rows: 2 }}
|
||||
style={{
|
||||
border: '1px solid var(--ant-color-border)',
|
||||
borderRadius: 'var(--border-radius-base)',
|
||||
padding: '16px 16px',
|
||||
...props.skeletonStyle
|
||||
}}
|
||||
{...(props.skeletonProps || {})}
|
||||
></Skeleton>
|
||||
<SkeletonWrapper>
|
||||
<Skeleton
|
||||
avatar={{
|
||||
size: 32
|
||||
}}
|
||||
paragraph={{ rows: 2 }}
|
||||
style={{
|
||||
border: '1px solid var(--ant-color-border)',
|
||||
borderRadius: 'var(--border-radius-base)',
|
||||
padding: '16px 16px',
|
||||
...props.skeletonStyle
|
||||
}}
|
||||
{...(props.skeletonProps || {})}
|
||||
></Skeleton>
|
||||
</SkeletonWrapper>
|
||||
)}
|
||||
></ResizeContainer>
|
||||
);
|
||||
|
||||
@@ -41,5 +41,8 @@ export default {
|
||||
'If specified, the ENTRYPOINT defined in the image will be ignored, and the command below will be used as the container startup entrypoint.',
|
||||
'backend.form.defaultEnvironment': 'Default Environment Variables',
|
||||
'backend.recommendModels': 'Recommended Models',
|
||||
'backend.filter.source': 'Filter by source'
|
||||
'backend.filter.source': 'Filter by source',
|
||||
'backend.add.custom': 'Custom',
|
||||
'backend.add.community': 'Community Repository',
|
||||
'backend.community.title': 'Community Backends'
|
||||
};
|
||||
|
||||
@@ -41,5 +41,8 @@ export default {
|
||||
'If specified, the ENTRYPOINT defined in the image will be ignored, and the command below will be used as the container startup entrypoint.',
|
||||
'backend.form.defaultEnvironment': 'Default Environment Variables',
|
||||
'backend.recommendModels': 'Recommended Models',
|
||||
'backend.filter.source': 'Filter by source'
|
||||
'backend.filter.source': 'Filter by source',
|
||||
'backend.add.custom': 'Custom',
|
||||
'backend.add.community': 'Community Repository',
|
||||
'backend.community.title': 'Community Backends'
|
||||
};
|
||||
|
||||
@@ -41,11 +41,17 @@ export default {
|
||||
'Если указано, ENTRYPOINT, заданный в образе, будет проигнорирован, а вместо него для запуска контейнера будет использована команда ниже.',
|
||||
'backend.form.defaultEnvironment': 'Default Environment Variables',
|
||||
'backend.recommendModels': 'Recommended Models',
|
||||
'backend.filter.source': 'Filter by source'
|
||||
'backend.filter.source': 'Filter by source',
|
||||
'backend.add.custom': 'Custom',
|
||||
'backend.add.community': 'Community Repository',
|
||||
'backend.community.title': 'Community Backends'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
// 1. 'backend.form.defaultEnvironment': 'Default Environment Variables',
|
||||
// 2. 'backend.recommendModels': 'Recommended Models',
|
||||
// 3. 'backend.filter.source': 'Filter by source'
|
||||
// 3. 'backend.filter.source': 'Filter by source',
|
||||
// 4. 'backend.add.custom': 'Custom',
|
||||
// 5. 'backend.add.community': 'Community Repository',
|
||||
// 6. 'backend.community.title': 'Community Backends'
|
||||
// ========== End of To-Do List ==========
|
||||
|
||||
@@ -38,5 +38,8 @@ export default {
|
||||
'如指定,镜像中定义的 ENTRYPOINT 将被忽略,下面的命令将作为容器启动入口命令使用。',
|
||||
'backend.form.defaultEnvironment': '默认环境变量',
|
||||
'backend.recommendModels': '推荐模型',
|
||||
'backend.filter.source': '按来源过滤'
|
||||
'backend.filter.source': '按来源过滤',
|
||||
'backend.add.custom': '自定义',
|
||||
'backend.add.community': '社区仓库',
|
||||
'backend.community.title': '社区后端'
|
||||
};
|
||||
|
||||
@@ -5,7 +5,9 @@ export const INFERENCE_BACKEND_API = '/inference-backends';
|
||||
|
||||
export const FROM_YAML_API = '/from-yaml';
|
||||
|
||||
export async function queryBackendsList(params: Global.SearchParams) {
|
||||
export async function queryBackendsList(
|
||||
params: Global.SearchParams & { community?: number }
|
||||
) {
|
||||
return request<Global.PageResponse<ListItem>>(`${INFERENCE_BACKEND_API}`, {
|
||||
method: 'GET',
|
||||
params
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import GSDrawer from '@/components/scroller-modal/gs-drawer';
|
||||
import ColumnWrapper from '@/pages/_components/column-wrapper';
|
||||
import Separator from '@/pages/llmodels/components/separator';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ListItem } from '../config/types';
|
||||
import useEnableBackend from '../services/use-enable-backend';
|
||||
import BackendDetail from './backend-detail';
|
||||
import CommunityBackends from './commnity-backends';
|
||||
|
||||
const Container = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const AddCommunityModal: React.FC<{
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onRefresh?: () => void;
|
||||
}> = ({ open, onClose, onRefresh }) => {
|
||||
const intl = useIntl();
|
||||
const { handleEnableBackend } = useEnableBackend();
|
||||
const [currentData, setCurrentData] = React.useState<any>(null);
|
||||
|
||||
const handleOnEnable = async (checked: boolean, data: ListItem) => {
|
||||
await handleEnableBackend({
|
||||
id: data.id,
|
||||
data: {
|
||||
...data,
|
||||
enabled: checked
|
||||
}
|
||||
});
|
||||
onRefresh?.();
|
||||
};
|
||||
|
||||
const handleOnClick = (data: ListItem) => {
|
||||
setCurrentData(data);
|
||||
};
|
||||
return (
|
||||
<GSDrawer
|
||||
open={open}
|
||||
title={intl.formatMessage({ id: 'backend.community.title' })}
|
||||
footer={false}
|
||||
onClose={onClose}
|
||||
destroyOnHidden={true}
|
||||
closeIcon={false}
|
||||
maskClosable={false}
|
||||
keyboard={false}
|
||||
styles={{
|
||||
wrapper: { width: 'max(50vw, 900px)' }
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
<ColumnWrapper
|
||||
maxHeight={'calc(100vh - 90px)'}
|
||||
styles={{
|
||||
container: {
|
||||
paddingTop: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CommunityBackends
|
||||
onClick={handleOnClick}
|
||||
onEnable={handleOnEnable}
|
||||
></CommunityBackends>
|
||||
</ColumnWrapper>
|
||||
<Separator showArrow={false}></Separator>
|
||||
</div>
|
||||
<ColumnWrapper
|
||||
maxHeight={'calc(100vh - 90px)'}
|
||||
styles={{
|
||||
container: {
|
||||
paddingTop: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<BackendDetail
|
||||
onEnable={handleOnEnable}
|
||||
currentData={currentData}
|
||||
></BackendDetail>
|
||||
</ColumnWrapper>
|
||||
</Container>
|
||||
</GSDrawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCommunityModal;
|
||||
@@ -0,0 +1,163 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import { BulbOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Tag, Typography } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { generateIcon } from '../components/backend-card';
|
||||
|
||||
const Title = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
.text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
`;
|
||||
|
||||
const Subtitle = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--ant-color-text);
|
||||
font-weight: 500;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--ant-color-split);
|
||||
padding-bottom: 8px;
|
||||
.icon {
|
||||
color: var(--ant-color-text-tertiary);
|
||||
}
|
||||
`;
|
||||
|
||||
const Section = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
padding: 0px 8px;
|
||||
border-radius: var(--ant-border-radius);
|
||||
`;
|
||||
|
||||
const UL = styled.ul`
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
list-style-type: disc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
.label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
const BackendDetail: React.FC<{
|
||||
onEnable: (checked: boolean, data: any) => void;
|
||||
onDisable?: () => void;
|
||||
currentData?: any;
|
||||
}> = ({ onEnable, onDisable, currentData }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const renderSupportedFrameworks = () => {
|
||||
const frameworks = currentData?.framework_index_map || {};
|
||||
if (Object.keys(frameworks).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<UL>
|
||||
{Object.entries(frameworks as Record<string, string[]>).map(
|
||||
([key, value]) => {
|
||||
return (
|
||||
<li key={key}>
|
||||
<div className="label">{_.upperCase(key)}</div>
|
||||
{/* <Flex gap={8} wrap="wrap" style={{ marginBlock: 8 }}>
|
||||
{value?.map?.((v: string, index: number) => (
|
||||
<Tag key={index} style={{ margin: 0 }}>
|
||||
{v}
|
||||
</Tag>
|
||||
))}
|
||||
</Flex> */}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</UL>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Title>
|
||||
<span className="text">
|
||||
{generateIcon(currentData || {}, 32)}
|
||||
<AutoTooltip ghost>{currentData?.backend_name}</AutoTooltip>
|
||||
</span>
|
||||
{currentData && (
|
||||
<span>
|
||||
{currentData?.enabled ? (
|
||||
<Tag color="geekblue" variant="filled">
|
||||
{intl.formatMessage({ id: 'common.status.enabled' })}
|
||||
</Tag>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
size="middle"
|
||||
onClick={() => onEnable(true, currentData)}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.enable' })}
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</Title>
|
||||
{Object.keys(currentData?.framework_index_map || {}).length > 0 && (
|
||||
<Section>
|
||||
<Subtitle>
|
||||
<BulbOutlined className="icon" />
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'backend.availableFrameworks' })}
|
||||
</span>
|
||||
</Subtitle>
|
||||
<Content>{renderSupportedFrameworks()}</Content>
|
||||
</Section>
|
||||
)}
|
||||
{/* <Section>
|
||||
<Subtitle>
|
||||
<BulbOutlined className="icon" />
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: 'backend.form.defaultExecuteCommand'
|
||||
})}
|
||||
</span>
|
||||
</Subtitle>
|
||||
<Content style={{ padding: 0, border: 'none' }}>
|
||||
<HighlightCode
|
||||
code={currentData?.default_run_command}
|
||||
></HighlightCode>
|
||||
</Content>
|
||||
</Section> */}
|
||||
{currentData?.description && (
|
||||
<Section>
|
||||
<Subtitle>
|
||||
<BulbOutlined className="icon" />
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'common.table.description' })}
|
||||
</span>
|
||||
</Subtitle>
|
||||
<Content>
|
||||
<Typography.Text>{currentData?.description}</Typography.Text>
|
||||
</Content>
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackendDetail;
|
||||
@@ -0,0 +1,153 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import ThemeTag from '@/components/tags-wrapper/theme-tag';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Input, Switch, Tooltip } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ScrollerContext } from '../../_components/infinite-scroller/use-scroller-context';
|
||||
import NoResult from '../../_components/no-result';
|
||||
import { INFERENCE_BACKEND_API, queryBackendsList } from '../apis';
|
||||
import BackendCard from '../components/backend-card';
|
||||
import BackendCardList from '../components/backend-list';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
const FilterBox = styled.div`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--ant-color-bg-container);
|
||||
`;
|
||||
|
||||
const CommunityBackends: React.FC<{
|
||||
onEnable: (checked: boolean, data: ListItem) => void;
|
||||
onClick: (data: ListItem) => void;
|
||||
}> = ({ onEnable, onClick }) => {
|
||||
const intl = useIntl();
|
||||
const [currentData, setCurrentData] = React.useState<ListItem | null>(null);
|
||||
const { dataSource, loadMore, queryParams, handleSearch, handleNameChange } =
|
||||
useTableFetch<ListItem>({
|
||||
fetchAPI: queryBackendsList,
|
||||
API: INFERENCE_BACKEND_API,
|
||||
watch: false,
|
||||
isInfiniteScroll: true,
|
||||
contentForDelete: 'backends.title',
|
||||
defaultQueryParams: {
|
||||
perPage: 24,
|
||||
community: 1
|
||||
}
|
||||
});
|
||||
|
||||
const handleOnClickItem = (data: ListItem) => {
|
||||
onClick(data);
|
||||
setCurrentData(data);
|
||||
};
|
||||
|
||||
const handleOnEnable = async (checked: boolean, data: ListItem) => {
|
||||
await onEnable(checked, data);
|
||||
await handleSearch();
|
||||
if (data.id === currentData?.id) {
|
||||
handleOnClickItem({
|
||||
...data,
|
||||
enabled: checked
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const actionsRenderer = (data: ListItem) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
data.enabled
|
||||
? false
|
||||
: intl.formatMessage({ id: 'common.button.enable' })
|
||||
}
|
||||
>
|
||||
<>
|
||||
{data.enabled ? (
|
||||
<ThemeTag style={{ margin: 0 }} color="geekblue" variant="filled">
|
||||
{intl.formatMessage({ id: 'common.status.enabled' })}
|
||||
</ThemeTag>
|
||||
) : (
|
||||
<span onClick={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
onChange={(checked) => handleOnEnable(checked, data)}
|
||||
size="small"
|
||||
></Switch>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (dataSource.dataList.length > 0 && dataSource.loadend && !currentData) {
|
||||
handleOnClickItem(dataSource.dataList[0]);
|
||||
}
|
||||
}, [dataSource.dataList, dataSource.loadend]);
|
||||
|
||||
const renderItem = (item: ListItem) => {
|
||||
return (
|
||||
<BackendCard
|
||||
active={currentData?.id === item.id}
|
||||
data={item}
|
||||
layout="community"
|
||||
onClick={handleOnClickItem}
|
||||
actionsRenderer={actionsRenderer}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FilterBox>
|
||||
<Input
|
||||
prefix={
|
||||
<SearchOutlined
|
||||
style={{ color: 'var(--ant-color-text-placeholder)' }}
|
||||
></SearchOutlined>
|
||||
}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'common.filter.name'
|
||||
})}
|
||||
style={{ width: '100%' }}
|
||||
allowClear
|
||||
onChange={handleNameChange}
|
||||
></Input>
|
||||
</FilterBox>
|
||||
<ScrollerContext.Provider
|
||||
value={{
|
||||
total: dataSource.totalPage,
|
||||
current: queryParams.page!,
|
||||
loading: dataSource.loading,
|
||||
refresh: loadMore,
|
||||
throttleDelay: 300
|
||||
}}
|
||||
>
|
||||
<BackendCardList
|
||||
dataList={dataSource.dataList}
|
||||
loading={dataSource.loading}
|
||||
activeId={false}
|
||||
isFirst={!dataSource.loadend}
|
||||
renderItem={renderItem}
|
||||
></BackendCardList>
|
||||
<NoResult
|
||||
loading={dataSource.loading}
|
||||
loadend={dataSource.loadend}
|
||||
dataSource={dataSource.dataList}
|
||||
image={<IconFont type="icon-models" />}
|
||||
filters={_.omit(queryParams, ['sort_by'])}
|
||||
noFoundText={intl.formatMessage({
|
||||
id: 'noresult.backend.nofound'
|
||||
})}
|
||||
></NoResult>
|
||||
</ScrollerContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunityBackends;
|
||||
@@ -124,7 +124,7 @@ const InfoItem = styled.div`
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: max-content 1fr minmax(0, max-content);
|
||||
align-items: center;
|
||||
align-items: end;
|
||||
gap: 8px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
.icon {
|
||||
@@ -140,10 +140,37 @@ const InfoItem = styled.div`
|
||||
interface BackendCardProps {
|
||||
onClick?: (data: any) => void;
|
||||
onSelect?: (item: any) => void;
|
||||
actionsRenderer?: (data: ListItem) => React.ReactNode;
|
||||
data: ListItem;
|
||||
layout?: 'default' | 'community';
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
||||
export const generateIcon = (data: ListItem, height = 20) => {
|
||||
if (builtInBackendLogos[data.backend_name]) {
|
||||
return <img src={builtInBackendLogos[data.backend_name]} height={height} />;
|
||||
}
|
||||
if (data.icon) {
|
||||
return <img src={data.icon} height={height} />;
|
||||
}
|
||||
const color = customColors[data.id % customColors.length];
|
||||
const icon = customIcons[data.id % customIcons.length];
|
||||
|
||||
return (
|
||||
<TagInner color={color} variant="filled">
|
||||
{icon}
|
||||
</TagInner>
|
||||
);
|
||||
};
|
||||
|
||||
const BackendCard: React.FC<BackendCardProps> = ({
|
||||
data,
|
||||
onClick,
|
||||
onSelect,
|
||||
actionsRenderer,
|
||||
layout = 'default',
|
||||
active
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleOnSelect = (item: any) => {
|
||||
@@ -152,23 +179,11 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
||||
|
||||
const handleClick = () => {
|
||||
onSelect?.({ action: 'view_versions', data: data });
|
||||
onClick?.(data);
|
||||
};
|
||||
|
||||
const renderIcon = () => {
|
||||
if (builtInBackendLogos[data.backend_name]) {
|
||||
return <img src={builtInBackendLogos[data.backend_name]} height={20} />;
|
||||
}
|
||||
if (data.icon) {
|
||||
return <img src={data.icon} height={20} />;
|
||||
}
|
||||
const color = customColors[data.id % customColors.length];
|
||||
const icon = customIcons[data.id % customIcons.length];
|
||||
|
||||
return (
|
||||
<TagInner color={color} variant="filled">
|
||||
{icon}
|
||||
</TagInner>
|
||||
);
|
||||
return generateIcon(data);
|
||||
};
|
||||
|
||||
const actions = useMemo(() => {
|
||||
@@ -180,7 +195,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
const handleonClickAction = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
@@ -278,6 +293,9 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
||||
};
|
||||
|
||||
const renderSource = () => {
|
||||
if (layout === 'community') {
|
||||
return null;
|
||||
}
|
||||
const source = data.is_built_in
|
||||
? BackendSourceLabelMap[BackendSourceValueMap.BUILTIN] || ''
|
||||
: BackendSourceLabelMap[data.backend_source] || '';
|
||||
@@ -285,7 +303,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ThemeTag
|
||||
<Tag
|
||||
color={
|
||||
TagColorMap[
|
||||
data.is_built_in
|
||||
@@ -294,7 +312,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
||||
]
|
||||
}
|
||||
className="font-400"
|
||||
variant="outlined"
|
||||
variant="filled"
|
||||
style={{
|
||||
borderRadius: 'var(--ant-border-radius)',
|
||||
margin: 0,
|
||||
@@ -304,7 +322,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
||||
{intl.formatMessage({
|
||||
id: source
|
||||
})}
|
||||
</ThemeTag>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -330,49 +348,58 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderActions = () => {
|
||||
return actionsRenderer ? (
|
||||
actionsRenderer(data)
|
||||
) : (
|
||||
<span onClick={handleonClickAction} className="operations">
|
||||
<DropDownActions
|
||||
menu={{
|
||||
items: actions,
|
||||
onClick: handleOnSelect
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<IconFont type="icon-more"></IconFont>}
|
||||
size="small"
|
||||
type="text"
|
||||
></Button>
|
||||
</DropDownActions>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledCard
|
||||
onClick={handleClick}
|
||||
clickable={true}
|
||||
hoverable={true}
|
||||
disabled={false}
|
||||
height={164}
|
||||
active={active}
|
||||
height={136}
|
||||
ghost
|
||||
header={
|
||||
<Header>
|
||||
<div className="title">{renderIcon()}</div>
|
||||
<span onClick={onClick} className="operations">
|
||||
<DropDownActions
|
||||
menu={{
|
||||
items: actions,
|
||||
onClick: handleOnSelect
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<IconFont type="icon-more"></IconFont>}
|
||||
size="small"
|
||||
type="text"
|
||||
></Button>
|
||||
</DropDownActions>
|
||||
</span>
|
||||
{renderActions()}
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
<Content>
|
||||
<CardName>
|
||||
<span>{data.backend_name}</span>
|
||||
{renderSource()}
|
||||
</CardName>
|
||||
<SourceWrapper>
|
||||
{/* <SourceWrapper>
|
||||
<InfoItem>
|
||||
<span className="label">
|
||||
<IconFont type="icon-source" className="icon" />
|
||||
<span>{intl.formatMessage({ id: 'models.form.source' })}:</span>
|
||||
</span>
|
||||
{renderSource()}
|
||||
{renderEnabledTag()}
|
||||
</InfoItem>
|
||||
{renderRecommendModels()}
|
||||
</SourceWrapper>
|
||||
</SourceWrapper> */}
|
||||
{renderFrameworks()}
|
||||
</Content>
|
||||
</StyledCard>
|
||||
|
||||
@@ -44,6 +44,7 @@ interface BackendListProps {
|
||||
activeId: Global.WithFalse<number>;
|
||||
isFirst: boolean;
|
||||
onSelect?: (item: any) => void;
|
||||
renderItem?: (item: any) => React.ReactNode;
|
||||
}
|
||||
|
||||
const ListSkeleton: React.FC<{
|
||||
@@ -70,7 +71,7 @@ const ListSkeleton: React.FC<{
|
||||
skeletonStyle={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 26
|
||||
gap: 24
|
||||
}}
|
||||
></CardSkeleton>
|
||||
</SkeletonWrapper>
|
||||
@@ -89,7 +90,8 @@ const CardList: React.FC<BackendListProps> = (props) => {
|
||||
isFirst,
|
||||
defaultSpan = 8,
|
||||
resizable = true,
|
||||
onSelect
|
||||
onSelect,
|
||||
renderItem
|
||||
} = props;
|
||||
const {
|
||||
total,
|
||||
@@ -111,7 +113,13 @@ const CardList: React.FC<BackendListProps> = (props) => {
|
||||
defaultSpan={defaultSpan}
|
||||
resizable={resizable}
|
||||
dataList={dataList}
|
||||
renderItem={(item) => <BackendCard data={item} onSelect={onSelect} />}
|
||||
renderItem={(item) =>
|
||||
renderItem ? (
|
||||
renderItem(item)
|
||||
) : (
|
||||
<BackendCard data={item} onSelect={onSelect} />
|
||||
)
|
||||
}
|
||||
></ResizeContainer>
|
||||
<ListSkeleton loading={loading} isFirst={isFirst} />
|
||||
</InfiniteScroller>
|
||||
|
||||
@@ -82,26 +82,26 @@ export const backendActions = [
|
||||
locale: true,
|
||||
icon: icons.Yaml
|
||||
},
|
||||
{
|
||||
label: 'common.button.enable',
|
||||
value: 'enable',
|
||||
key: 'enable',
|
||||
locale: true,
|
||||
icon: icons.Charger,
|
||||
show: (record: any) =>
|
||||
!record.enabled &&
|
||||
record.backend_source === BackendSourceValueMap.COMMUNITY
|
||||
},
|
||||
{
|
||||
label: 'common.button.disable',
|
||||
value: 'disable',
|
||||
key: 'disable',
|
||||
locale: true,
|
||||
icon: icons.Disabled,
|
||||
show: (record: any) =>
|
||||
record.enabled &&
|
||||
record.backend_source === BackendSourceValueMap.COMMUNITY
|
||||
},
|
||||
// {
|
||||
// label: 'common.button.enable',
|
||||
// value: 'enable',
|
||||
// key: 'enable',
|
||||
// locale: true,
|
||||
// icon: icons.Charger,
|
||||
// show: (record: any) =>
|
||||
// !record.enabled &&
|
||||
// record.backend_source === BackendSourceValueMap.COMMUNITY
|
||||
// },
|
||||
// {
|
||||
// label: 'common.button.disable',
|
||||
// value: 'disable',
|
||||
// key: 'disable',
|
||||
// locale: true,
|
||||
// icon: icons.Disabled,
|
||||
// show: (record: any) =>
|
||||
// record.enabled &&
|
||||
// record.backend_source === BackendSourceValueMap.COMMUNITY
|
||||
// },
|
||||
{
|
||||
label: 'common.button.delete',
|
||||
value: 'delete',
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { useState } from 'react';
|
||||
|
||||
const useCommunityBackend = () => {
|
||||
const [openModalStatus, setOpenModalStatus] = useState<{
|
||||
open: boolean;
|
||||
action: PageActionType;
|
||||
title: string;
|
||||
}>({
|
||||
open: false,
|
||||
action: PageAction.CREATE,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const openModal = (action: PageActionType, title: string) => {
|
||||
setOpenModalStatus({
|
||||
open: true,
|
||||
title: title,
|
||||
action
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setOpenModalStatus({
|
||||
open: false,
|
||||
action: PageAction.CREATE,
|
||||
title: ''
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
openCommunityModalStatus: openModalStatus,
|
||||
setOpenCommunityModalStatus: setOpenModalStatus,
|
||||
openCommunityModal: openModal,
|
||||
closeCommunityModal: closeModal
|
||||
};
|
||||
};
|
||||
|
||||
export default useCommunityBackend;
|
||||
@@ -0,0 +1,69 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { PageAction } from '@/config';
|
||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||
import { ListItem } from '../config/types';
|
||||
import useCommunityBackend from './use-community-backend';
|
||||
import useCustomBackend from './use-custom-backend';
|
||||
|
||||
const useCreateBackend = () => {
|
||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||
const { openCommunityModal, closeCommunityModal, openCommunityModalStatus } =
|
||||
useCommunityBackend();
|
||||
const { openCustomModal, closeCustomModal, openCustomModalStatus } =
|
||||
useCustomBackend();
|
||||
|
||||
const addItems = [
|
||||
{
|
||||
label: 'backend.add.community',
|
||||
locale: true,
|
||||
key: 'community',
|
||||
value: 'community',
|
||||
icon: <IconFont type="icon-public" />
|
||||
},
|
||||
{
|
||||
label: 'backend.add.custom',
|
||||
locale: true,
|
||||
key: 'custom',
|
||||
value: 'custom',
|
||||
icon: <IconFont type="icon-person" />
|
||||
}
|
||||
];
|
||||
|
||||
const handleEditBackend = (
|
||||
action: PageAction,
|
||||
title: string,
|
||||
row: ListItem
|
||||
) => {
|
||||
openCustomModal(action, title, row);
|
||||
saveScrollHeight();
|
||||
};
|
||||
|
||||
const handleCloseModal = (type: 'community' | 'custom') => {
|
||||
if (type === 'community') {
|
||||
closeCommunityModal();
|
||||
} else {
|
||||
closeCustomModal();
|
||||
}
|
||||
restoreScrollHeight();
|
||||
};
|
||||
|
||||
const handleAddBackend = (type: 'community' | 'custom') => {
|
||||
if (type === 'community') {
|
||||
openCommunityModal(PageAction.CREATE, 'Add Community Backend');
|
||||
} else {
|
||||
openCustomModal(PageAction.CREATE, 'Add Custom Backend');
|
||||
}
|
||||
saveScrollHeight();
|
||||
};
|
||||
|
||||
return {
|
||||
closeBackendModal: handleCloseModal,
|
||||
addBackend: handleAddBackend,
|
||||
editBackend: handleEditBackend,
|
||||
openBackendModalStatus: openCustomModalStatus, // for create custom backend and edit
|
||||
openCommunityModalStatus: openCommunityModalStatus, // for create community backend
|
||||
addActions: addItems
|
||||
};
|
||||
};
|
||||
|
||||
export default useCreateBackend;
|
||||
+6
-10
@@ -1,11 +1,9 @@
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import useBodyScroll from '@/hooks/use-body-scroll';
|
||||
import { useState } from 'react';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
const useCreateBackend = () => {
|
||||
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
|
||||
const useCustomBackend = () => {
|
||||
const [openModalStatus, setOpenModalStatus] = useState<{
|
||||
open: boolean;
|
||||
action: PageActionType;
|
||||
@@ -26,7 +24,6 @@ const useCreateBackend = () => {
|
||||
action,
|
||||
currentData: row
|
||||
});
|
||||
saveScrollHeight();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
@@ -36,15 +33,14 @@ const useCreateBackend = () => {
|
||||
currentData: undefined,
|
||||
title: ''
|
||||
});
|
||||
restoreScrollHeight();
|
||||
};
|
||||
|
||||
return {
|
||||
openBackendModalStatus: openModalStatus,
|
||||
setOpenBackendModalStatus: setOpenModalStatus,
|
||||
openBackendModal: openModal,
|
||||
closeBackendModal: closeModal
|
||||
openCustomModalStatus: openModalStatus,
|
||||
setOpenCustomModalStatus: setOpenModalStatus,
|
||||
openCustomModal: openModal,
|
||||
closeCustomModal: closeModal
|
||||
};
|
||||
};
|
||||
|
||||
export default useCreateBackend;
|
||||
export default useCustomBackend;
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
updateBackend,
|
||||
updateBackendFromYAML
|
||||
} from './apis';
|
||||
import AddCommunityModal from './community/add-community-modal';
|
||||
import AddModal from './components/add-modal';
|
||||
import BackendCardList from './components/backend-list';
|
||||
import VersionInfoModal from './components/version-info-modal';
|
||||
@@ -63,8 +64,14 @@ const BackendList = () => {
|
||||
currentData?: ListItem;
|
||||
}>({ open: false });
|
||||
const { handleEnableBackend } = useEnableBackend();
|
||||
const { openBackendModal, closeBackendModal, openBackendModalStatus } =
|
||||
useCreateBackend();
|
||||
const {
|
||||
addBackend,
|
||||
editBackend,
|
||||
closeBackendModal,
|
||||
openCommunityModalStatus,
|
||||
openBackendModalStatus,
|
||||
addActions
|
||||
} = useCreateBackend();
|
||||
|
||||
// built_in_version_configs is read-only, but needs to be included when updating
|
||||
const handleOnSubmit = async (values: FormData) => {
|
||||
@@ -91,7 +98,7 @@ const BackendList = () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
closeBackendModal();
|
||||
closeBackendModal('custom');
|
||||
handleSearch();
|
||||
} catch (error) {}
|
||||
};
|
||||
@@ -116,7 +123,7 @@ const BackendList = () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
closeBackendModal();
|
||||
closeBackendModal('custom');
|
||||
handleSearch();
|
||||
} catch (error) {}
|
||||
};
|
||||
@@ -128,18 +135,54 @@ const BackendList = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddBackend = () => {
|
||||
openBackendModal(PageAction.CREATE, '');
|
||||
const handleAddBackend = (item: { key: 'community' | 'custom' }) => {
|
||||
addBackend(item.key);
|
||||
};
|
||||
|
||||
const handleDisableCommunityBackend = async (item: any) => {
|
||||
// use disable for community backend deletion
|
||||
handleEnableBackend({
|
||||
id: item.data.id,
|
||||
data: {
|
||||
...item.data,
|
||||
enabled: item.action === 'enable'
|
||||
},
|
||||
showMessage: false
|
||||
});
|
||||
// delay 200ms to refresh list
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 300);
|
||||
});
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleOnSelect = async (item: any) => {
|
||||
// ================ Edit ================
|
||||
if (item.action === 'edit') {
|
||||
openBackendModal(PageAction.EDIT, '', item.data);
|
||||
} else if (item.action === 'delete') {
|
||||
handleDelete(item.data, {
|
||||
name: item.data.backend_name
|
||||
});
|
||||
} else if (item.action === 'export') {
|
||||
editBackend(PageAction.EDIT, '', item.data);
|
||||
return;
|
||||
}
|
||||
// ================ Delete ================
|
||||
if (item.action === 'delete') {
|
||||
if (item.data.backend_source === BackendSourceValueMap.COMMUNITY) {
|
||||
modalRef.current?.show({
|
||||
content: 'backends.title',
|
||||
operation: 'common.delete.single.confirm',
|
||||
name: item.data.backend_name,
|
||||
async onOk() {
|
||||
handleDisableCommunityBackend(item);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleDelete(item.data, {
|
||||
name: item.data.backend_name
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ================ Export YAML ================
|
||||
if (item.action === 'export') {
|
||||
const currentData = structuredClone(item.data);
|
||||
|
||||
currentData.version_configs = _.mapValues(
|
||||
@@ -150,24 +193,15 @@ const BackendList = () => {
|
||||
);
|
||||
|
||||
exportYAML(_.omit(currentData, ['id', 'created_at', 'updated_at']));
|
||||
} else if (item.action === 'view_versions') {
|
||||
return;
|
||||
}
|
||||
|
||||
// ================ View Versions ================
|
||||
if (item.action === 'view_versions') {
|
||||
setOpenVersionInfoModal({
|
||||
open: true,
|
||||
currentData: item.data
|
||||
});
|
||||
} else if (item.action === 'enable' || item.action === 'disable') {
|
||||
handleEnableBackend({
|
||||
id: item.data.id,
|
||||
data: {
|
||||
...item.data,
|
||||
enabled: item.action === 'enable'
|
||||
}
|
||||
});
|
||||
// delay 200ms to refresh list
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 300);
|
||||
});
|
||||
handleSearch();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,7 +210,11 @@ const BackendList = () => {
|
||||
open: false,
|
||||
currentData: undefined
|
||||
});
|
||||
openBackendModal(PageAction.EDIT, '', openVersionInfoModal.currentData);
|
||||
editBackend(
|
||||
PageAction.EDIT,
|
||||
'',
|
||||
openVersionInfoModal.currentData as ListItem
|
||||
);
|
||||
};
|
||||
|
||||
const loadMore = useMemoizedFn((nextPage: number) => {
|
||||
@@ -197,6 +235,8 @@ const BackendList = () => {
|
||||
widths={{
|
||||
input: 300
|
||||
}}
|
||||
actionItems={addActions}
|
||||
actionType="dropdown"
|
||||
inputHolder={intl.formatMessage({ id: 'common.filter.name' })}
|
||||
selectHolder={intl.formatMessage({ id: 'backend.filter.source' })}
|
||||
buttonText={intl.formatMessage({ id: 'backend.button.add' })}
|
||||
@@ -211,7 +251,6 @@ const BackendList = () => {
|
||||
value: item.value
|
||||
}))}
|
||||
></FilterBar>
|
||||
|
||||
<ScrollerContext.Provider
|
||||
value={{
|
||||
total: dataSource.totalPage,
|
||||
@@ -239,13 +278,13 @@ const BackendList = () => {
|
||||
})}
|
||||
title={intl.formatMessage({ id: 'noresult.backend.title' })}
|
||||
subTitle={intl.formatMessage({ id: 'noresult.backend.subTitle' })}
|
||||
onClick={handleAddBackend}
|
||||
onClick={() => handleAddBackend({ key: 'community' })}
|
||||
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
|
||||
></NoResult>
|
||||
</ScrollerContext.Provider>
|
||||
<AddModal
|
||||
action={openBackendModalStatus.action}
|
||||
onClose={closeBackendModal}
|
||||
onClose={() => closeBackendModal('custom')}
|
||||
onSubmit={handleOnSubmit}
|
||||
onSubmitYaml={handleOnSubmitYaml}
|
||||
currentData={openBackendModalStatus.currentData as ListItem}
|
||||
@@ -256,6 +295,11 @@ const BackendList = () => {
|
||||
: intl.formatMessage({ id: 'backend.button.edit' })
|
||||
}
|
||||
></AddModal>
|
||||
<AddCommunityModal
|
||||
open={openCommunityModalStatus.open}
|
||||
onRefresh={handleSearch}
|
||||
onClose={() => closeBackendModal('community')}
|
||||
></AddCommunityModal>
|
||||
<VersionInfoModal
|
||||
addVersion={handleAddVersion}
|
||||
open={openVersionInfoModal.open}
|
||||
|
||||
@@ -8,11 +8,15 @@ const useEnableBackend = () => {
|
||||
const handleEnableBackend = async (params: {
|
||||
id: number;
|
||||
data: FormData;
|
||||
showMessage?: boolean;
|
||||
}) => {
|
||||
const { showMessage = true } = params;
|
||||
await updateBackend(params.id, {
|
||||
data: params.data
|
||||
});
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
if (showMessage) {
|
||||
message.success(intl.formatMessage({ id: 'common.message.success' }));
|
||||
}
|
||||
};
|
||||
|
||||
return { handleEnableBackend };
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useState } from 'react';
|
||||
import { queryBackendsList } from '../apis';
|
||||
|
||||
const useQueryCommunityBackends = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [dataList, setDataList] = useState<any[]>([]);
|
||||
|
||||
const fetchCommunityBackends = async (params?: any) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await queryBackendsList({
|
||||
page: -1,
|
||||
community: 1,
|
||||
...params
|
||||
});
|
||||
setDataList(response.items || []);
|
||||
} catch (error) {
|
||||
setDataList([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return {
|
||||
loading,
|
||||
dataList,
|
||||
fetchCommunityBackends
|
||||
};
|
||||
};
|
||||
export default useQueryCommunityBackends;
|
||||
@@ -2,14 +2,14 @@ import { Divider } from 'antd';
|
||||
import React from 'react';
|
||||
import '../style/separator.less';
|
||||
|
||||
const Separator: React.FC = () => {
|
||||
const Separator: React.FC<{ showArrow?: boolean }> = ({ showArrow = true }) => {
|
||||
return (
|
||||
<div className="separator">
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
style={{ height: 'calc(100vh - 89px)', marginInline: '0px' }}
|
||||
></Divider>
|
||||
<span className="shape"></span>
|
||||
{showArrow && <span className="shape"></span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user