feat: community backends page

This commit is contained in:
jialin
2026-01-30 20:48:55 +08:00
committed by Lawrence Li
parent 2dab5093dc
commit b6bd9cd7dd
22 changed files with 783 additions and 124 deletions
+1
View File
@@ -72,6 +72,7 @@ interface Configuration {
checked: boolean; checked: boolean;
} }
// default need to pass content and operation
const DeleteModal = forwardRef((props, ref) => { const DeleteModal = forwardRef((props, ref) => {
const intl = useIntl(); const intl = useIntl();
const { styles } = useStyles(); const { styles } = useStyles();
+1 -1
View File
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
// import './iconfont/iconfont.js'; // import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({ 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; export default IconFont;
+5
View File
@@ -156,6 +156,11 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
{handleClickPrimary ? ( {handleClickPrimary ? (
actionType === 'dropdown' ? ( actionType === 'dropdown' ? (
<DropDownActions <DropDownActions
styles={{
root: {
minWidth: 140
}
}}
menu={{ menu={{
items: actionItems, items: actionItems,
onClick: handleClickPrimary onClick: handleClickPrimary
+22 -13
View File
@@ -1,7 +1,14 @@
import { Skeleton } from 'antd'; import { Skeleton } from 'antd';
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import ResizeContainer from '../resize-container'; import ResizeContainer from '../resize-container';
const SkeletonWrapper = styled.div`
.ant-skeleton-paragraph {
margin-bottom: 0;
}
`;
interface CatalogSkeltonProps { interface CatalogSkeltonProps {
skeletonProps?: any; skeletonProps?: any;
skeletonStyle?: React.CSSProperties; skeletonStyle?: React.CSSProperties;
@@ -12,19 +19,21 @@ const CardSkelton: React.FC<CatalogSkeltonProps> = (props) => {
<ResizeContainer <ResizeContainer
dataList={Array(6).fill({ label: 'skeleton' })} dataList={Array(6).fill({ label: 'skeleton' })}
renderItem={() => ( renderItem={() => (
<Skeleton <SkeletonWrapper>
avatar={{ <Skeleton
size: 32 avatar={{
}} size: 32
paragraph={{ rows: 2 }} }}
style={{ paragraph={{ rows: 2 }}
border: '1px solid var(--ant-color-border)', style={{
borderRadius: 'var(--border-radius-base)', border: '1px solid var(--ant-color-border)',
padding: '16px 16px', borderRadius: 'var(--border-radius-base)',
...props.skeletonStyle padding: '16px 16px',
}} ...props.skeletonStyle
{...(props.skeletonProps || {})} }}
></Skeleton> {...(props.skeletonProps || {})}
></Skeleton>
</SkeletonWrapper>
)} )}
></ResizeContainer> ></ResizeContainer>
); );
+4 -1
View File
@@ -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.', '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.form.defaultEnvironment': 'Default Environment Variables',
'backend.recommendModels': 'Recommended Models', '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'
}; };
+4 -1
View File
@@ -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.', '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.form.defaultEnvironment': 'Default Environment Variables',
'backend.recommendModels': 'Recommended Models', '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'
}; };
+8 -2
View File
@@ -41,11 +41,17 @@ export default {
'Если указано, ENTRYPOINT, заданный в образе, будет проигнорирован, а вместо него для запуска контейнера будет использована команда ниже.', 'Если указано, ENTRYPOINT, заданный в образе, будет проигнорирован, а вместо него для запуска контейнера будет использована команда ниже.',
'backend.form.defaultEnvironment': 'Default Environment Variables', 'backend.form.defaultEnvironment': 'Default Environment Variables',
'backend.recommendModels': 'Recommended Models', '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) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'backend.form.defaultEnvironment': 'Default Environment Variables', // 1. 'backend.form.defaultEnvironment': 'Default Environment Variables',
// 2. 'backend.recommendModels': 'Recommended Models', // 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 ========== // ========== End of To-Do List ==========
+4 -1
View File
@@ -38,5 +38,8 @@ export default {
'如指定,镜像中定义的 ENTRYPOINT 将被忽略,下面的命令将作为容器启动入口命令使用。', '如指定,镜像中定义的 ENTRYPOINT 将被忽略,下面的命令将作为容器启动入口命令使用。',
'backend.form.defaultEnvironment': '默认环境变量', 'backend.form.defaultEnvironment': '默认环境变量',
'backend.recommendModels': '推荐模型', 'backend.recommendModels': '推荐模型',
'backend.filter.source': '按来源过滤' 'backend.filter.source': '按来源过滤',
'backend.add.custom': '自定义',
'backend.add.community': '社区仓库',
'backend.community.title': '社区后端'
}; };
+3 -1
View File
@@ -5,7 +5,9 @@ export const INFERENCE_BACKEND_API = '/inference-backends';
export const FROM_YAML_API = '/from-yaml'; 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}`, { return request<Global.PageResponse<ListItem>>(`${INFERENCE_BACKEND_API}`, {
method: 'GET', method: 'GET',
params 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;
+65 -38
View File
@@ -124,7 +124,7 @@ const InfoItem = styled.div`
display: grid; display: grid;
width: 100%; width: 100%;
grid-template-columns: max-content 1fr minmax(0, max-content); grid-template-columns: max-content 1fr minmax(0, max-content);
align-items: center; align-items: end;
gap: 8px; gap: 8px;
color: var(--ant-color-text-tertiary); color: var(--ant-color-text-tertiary);
.icon { .icon {
@@ -140,10 +140,37 @@ const InfoItem = styled.div`
interface BackendCardProps { interface BackendCardProps {
onClick?: (data: any) => void; onClick?: (data: any) => void;
onSelect?: (item: any) => void; onSelect?: (item: any) => void;
actionsRenderer?: (data: ListItem) => React.ReactNode;
data: ListItem; 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 intl = useIntl();
const handleOnSelect = (item: any) => { const handleOnSelect = (item: any) => {
@@ -152,23 +179,11 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
const handleClick = () => { const handleClick = () => {
onSelect?.({ action: 'view_versions', data: data }); onSelect?.({ action: 'view_versions', data: data });
onClick?.(data);
}; };
const renderIcon = () => { const renderIcon = () => {
if (builtInBackendLogos[data.backend_name]) { return generateIcon(data);
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>
);
}; };
const actions = useMemo(() => { const actions = useMemo(() => {
@@ -180,7 +195,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
}); });
}, [data]); }, [data]);
const onClick = (e: React.MouseEvent) => { const handleonClickAction = (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
}; };
@@ -278,6 +293,9 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
}; };
const renderSource = () => { const renderSource = () => {
if (layout === 'community') {
return null;
}
const source = data.is_built_in const source = data.is_built_in
? BackendSourceLabelMap[BackendSourceValueMap.BUILTIN] || '' ? BackendSourceLabelMap[BackendSourceValueMap.BUILTIN] || ''
: BackendSourceLabelMap[data.backend_source] || ''; : BackendSourceLabelMap[data.backend_source] || '';
@@ -285,7 +303,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
return null; return null;
} }
return ( return (
<ThemeTag <Tag
color={ color={
TagColorMap[ TagColorMap[
data.is_built_in data.is_built_in
@@ -294,7 +312,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
] ]
} }
className="font-400" className="font-400"
variant="outlined" variant="filled"
style={{ style={{
borderRadius: 'var(--ant-border-radius)', borderRadius: 'var(--ant-border-radius)',
margin: 0, margin: 0,
@@ -304,7 +322,7 @@ const BackendCard: React.FC<BackendCardProps> = ({ data, onSelect }) => {
{intl.formatMessage({ {intl.formatMessage({
id: source 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 ( return (
<StyledCard <StyledCard
onClick={handleClick} onClick={handleClick}
clickable={true} clickable={true}
hoverable={true} hoverable={true}
disabled={false} disabled={false}
height={164} active={active}
height={136}
ghost ghost
header={ header={
<Header> <Header>
<div className="title">{renderIcon()}</div> <div className="title">{renderIcon()}</div>
<span onClick={onClick} className="operations"> {renderActions()}
<DropDownActions
menu={{
items: actions,
onClick: handleOnSelect
}}
>
<Button
icon={<IconFont type="icon-more"></IconFont>}
size="small"
type="text"
></Button>
</DropDownActions>
</span>
</Header> </Header>
} }
> >
<Content> <Content>
<CardName> <CardName>
<span>{data.backend_name}</span> <span>{data.backend_name}</span>
{renderSource()}
</CardName> </CardName>
<SourceWrapper> {/* <SourceWrapper>
<InfoItem> <InfoItem>
<span className="label"> <span className="label">
<IconFont type="icon-source" className="icon" /> <IconFont type="icon-source" className="icon" />
<span>{intl.formatMessage({ id: 'models.form.source' })}:</span> <span>{intl.formatMessage({ id: 'models.form.source' })}:</span>
</span> </span>
{renderSource()}
{renderEnabledTag()} {renderEnabledTag()}
</InfoItem> </InfoItem>
{renderRecommendModels()} {renderRecommendModels()}
</SourceWrapper> </SourceWrapper> */}
{renderFrameworks()} {renderFrameworks()}
</Content> </Content>
</StyledCard> </StyledCard>
+11 -3
View File
@@ -44,6 +44,7 @@ interface BackendListProps {
activeId: Global.WithFalse<number>; activeId: Global.WithFalse<number>;
isFirst: boolean; isFirst: boolean;
onSelect?: (item: any) => void; onSelect?: (item: any) => void;
renderItem?: (item: any) => React.ReactNode;
} }
const ListSkeleton: React.FC<{ const ListSkeleton: React.FC<{
@@ -70,7 +71,7 @@ const ListSkeleton: React.FC<{
skeletonStyle={{ skeletonStyle={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: 26 gap: 24
}} }}
></CardSkeleton> ></CardSkeleton>
</SkeletonWrapper> </SkeletonWrapper>
@@ -89,7 +90,8 @@ const CardList: React.FC<BackendListProps> = (props) => {
isFirst, isFirst,
defaultSpan = 8, defaultSpan = 8,
resizable = true, resizable = true,
onSelect onSelect,
renderItem
} = props; } = props;
const { const {
total, total,
@@ -111,7 +113,13 @@ const CardList: React.FC<BackendListProps> = (props) => {
defaultSpan={defaultSpan} defaultSpan={defaultSpan}
resizable={resizable} resizable={resizable}
dataList={dataList} dataList={dataList}
renderItem={(item) => <BackendCard data={item} onSelect={onSelect} />} renderItem={(item) =>
renderItem ? (
renderItem(item)
) : (
<BackendCard data={item} onSelect={onSelect} />
)
}
></ResizeContainer> ></ResizeContainer>
<ListSkeleton loading={loading} isFirst={isFirst} /> <ListSkeleton loading={loading} isFirst={isFirst} />
</InfiniteScroller> </InfiniteScroller>
+20 -20
View File
@@ -82,26 +82,26 @@ export const backendActions = [
locale: true, locale: true,
icon: icons.Yaml icon: icons.Yaml
}, },
{ // {
label: 'common.button.enable', // label: 'common.button.enable',
value: 'enable', // value: 'enable',
key: 'enable', // key: 'enable',
locale: true, // locale: true,
icon: icons.Charger, // icon: icons.Charger,
show: (record: any) => // show: (record: any) =>
!record.enabled && // !record.enabled &&
record.backend_source === BackendSourceValueMap.COMMUNITY // record.backend_source === BackendSourceValueMap.COMMUNITY
}, // },
{ // {
label: 'common.button.disable', // label: 'common.button.disable',
value: 'disable', // value: 'disable',
key: 'disable', // key: 'disable',
locale: true, // locale: true,
icon: icons.Disabled, // icon: icons.Disabled,
show: (record: any) => // show: (record: any) =>
record.enabled && // record.enabled &&
record.backend_source === BackendSourceValueMap.COMMUNITY // record.backend_source === BackendSourceValueMap.COMMUNITY
}, // },
{ {
label: 'common.button.delete', label: 'common.button.delete',
value: '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;
@@ -1,11 +1,9 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useState } from 'react'; import { useState } from 'react';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
const useCreateBackend = () => { const useCustomBackend = () => {
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{ const [openModalStatus, setOpenModalStatus] = useState<{
open: boolean; open: boolean;
action: PageActionType; action: PageActionType;
@@ -26,7 +24,6 @@ const useCreateBackend = () => {
action, action,
currentData: row currentData: row
}); });
saveScrollHeight();
}; };
const closeModal = () => { const closeModal = () => {
@@ -36,15 +33,14 @@ const useCreateBackend = () => {
currentData: undefined, currentData: undefined,
title: '' title: ''
}); });
restoreScrollHeight();
}; };
return { return {
openBackendModalStatus: openModalStatus, openCustomModalStatus: openModalStatus,
setOpenBackendModalStatus: setOpenModalStatus, setOpenCustomModalStatus: setOpenModalStatus,
openBackendModal: openModal, openCustomModal: openModal,
closeBackendModal: closeModal closeCustomModal: closeModal
}; };
}; };
export default useCreateBackend; export default useCustomBackend;
+74 -30
View File
@@ -19,6 +19,7 @@ import {
updateBackend, updateBackend,
updateBackendFromYAML updateBackendFromYAML
} from './apis'; } from './apis';
import AddCommunityModal from './community/add-community-modal';
import AddModal from './components/add-modal'; import AddModal from './components/add-modal';
import BackendCardList from './components/backend-list'; import BackendCardList from './components/backend-list';
import VersionInfoModal from './components/version-info-modal'; import VersionInfoModal from './components/version-info-modal';
@@ -63,8 +64,14 @@ const BackendList = () => {
currentData?: ListItem; currentData?: ListItem;
}>({ open: false }); }>({ open: false });
const { handleEnableBackend } = useEnableBackend(); const { handleEnableBackend } = useEnableBackend();
const { openBackendModal, closeBackendModal, openBackendModalStatus } = const {
useCreateBackend(); addBackend,
editBackend,
closeBackendModal,
openCommunityModalStatus,
openBackendModalStatus,
addActions
} = useCreateBackend();
// built_in_version_configs is read-only, but needs to be included when updating // built_in_version_configs is read-only, but needs to be included when updating
const handleOnSubmit = async (values: FormData) => { const handleOnSubmit = async (values: FormData) => {
@@ -91,7 +98,7 @@ const BackendList = () => {
} }
}); });
} }
closeBackendModal(); closeBackendModal('custom');
handleSearch(); handleSearch();
} catch (error) {} } catch (error) {}
}; };
@@ -116,7 +123,7 @@ const BackendList = () => {
} }
}); });
} }
closeBackendModal(); closeBackendModal('custom');
handleSearch(); handleSearch();
} catch (error) {} } catch (error) {}
}; };
@@ -128,18 +135,54 @@ const BackendList = () => {
}); });
}; };
const handleAddBackend = () => { const handleAddBackend = (item: { key: 'community' | 'custom' }) => {
openBackendModal(PageAction.CREATE, ''); 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) => { const handleOnSelect = async (item: any) => {
// ================ Edit ================
if (item.action === 'edit') { if (item.action === 'edit') {
openBackendModal(PageAction.EDIT, '', item.data); editBackend(PageAction.EDIT, '', item.data);
} else if (item.action === 'delete') { return;
handleDelete(item.data, { }
name: item.data.backend_name // ================ Delete ================
}); if (item.action === 'delete') {
} else if (item.action === 'export') { 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); const currentData = structuredClone(item.data);
currentData.version_configs = _.mapValues( currentData.version_configs = _.mapValues(
@@ -150,24 +193,15 @@ const BackendList = () => {
); );
exportYAML(_.omit(currentData, ['id', 'created_at', 'updated_at'])); exportYAML(_.omit(currentData, ['id', 'created_at', 'updated_at']));
} else if (item.action === 'view_versions') { return;
}
// ================ View Versions ================
if (item.action === 'view_versions') {
setOpenVersionInfoModal({ setOpenVersionInfoModal({
open: true, open: true,
currentData: item.data 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, open: false,
currentData: undefined currentData: undefined
}); });
openBackendModal(PageAction.EDIT, '', openVersionInfoModal.currentData); editBackend(
PageAction.EDIT,
'',
openVersionInfoModal.currentData as ListItem
);
}; };
const loadMore = useMemoizedFn((nextPage: number) => { const loadMore = useMemoizedFn((nextPage: number) => {
@@ -197,6 +235,8 @@ const BackendList = () => {
widths={{ widths={{
input: 300 input: 300
}} }}
actionItems={addActions}
actionType="dropdown"
inputHolder={intl.formatMessage({ id: 'common.filter.name' })} inputHolder={intl.formatMessage({ id: 'common.filter.name' })}
selectHolder={intl.formatMessage({ id: 'backend.filter.source' })} selectHolder={intl.formatMessage({ id: 'backend.filter.source' })}
buttonText={intl.formatMessage({ id: 'backend.button.add' })} buttonText={intl.formatMessage({ id: 'backend.button.add' })}
@@ -211,7 +251,6 @@ const BackendList = () => {
value: item.value value: item.value
}))} }))}
></FilterBar> ></FilterBar>
<ScrollerContext.Provider <ScrollerContext.Provider
value={{ value={{
total: dataSource.totalPage, total: dataSource.totalPage,
@@ -239,13 +278,13 @@ const BackendList = () => {
})} })}
title={intl.formatMessage({ id: 'noresult.backend.title' })} title={intl.formatMessage({ id: 'noresult.backend.title' })}
subTitle={intl.formatMessage({ id: 'noresult.backend.subTitle' })} subTitle={intl.formatMessage({ id: 'noresult.backend.subTitle' })}
onClick={handleAddBackend} onClick={() => handleAddBackend({ key: 'community' })}
buttonText={intl.formatMessage({ id: 'noresult.button.add' })} buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
></NoResult> ></NoResult>
</ScrollerContext.Provider> </ScrollerContext.Provider>
<AddModal <AddModal
action={openBackendModalStatus.action} action={openBackendModalStatus.action}
onClose={closeBackendModal} onClose={() => closeBackendModal('custom')}
onSubmit={handleOnSubmit} onSubmit={handleOnSubmit}
onSubmitYaml={handleOnSubmitYaml} onSubmitYaml={handleOnSubmitYaml}
currentData={openBackendModalStatus.currentData as ListItem} currentData={openBackendModalStatus.currentData as ListItem}
@@ -256,6 +295,11 @@ const BackendList = () => {
: intl.formatMessage({ id: 'backend.button.edit' }) : intl.formatMessage({ id: 'backend.button.edit' })
} }
></AddModal> ></AddModal>
<AddCommunityModal
open={openCommunityModalStatus.open}
onRefresh={handleSearch}
onClose={() => closeBackendModal('community')}
></AddCommunityModal>
<VersionInfoModal <VersionInfoModal
addVersion={handleAddVersion} addVersion={handleAddVersion}
open={openVersionInfoModal.open} open={openVersionInfoModal.open}
@@ -8,11 +8,15 @@ const useEnableBackend = () => {
const handleEnableBackend = async (params: { const handleEnableBackend = async (params: {
id: number; id: number;
data: FormData; data: FormData;
showMessage?: boolean;
}) => { }) => {
const { showMessage = true } = params;
await updateBackend(params.id, { await updateBackend(params.id, {
data: params.data data: params.data
}); });
message.success(intl.formatMessage({ id: 'common.message.success' })); if (showMessage) {
message.success(intl.formatMessage({ id: 'common.message.success' }));
}
}; };
return { handleEnableBackend }; 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 -2
View File
@@ -2,14 +2,14 @@ import { Divider } from 'antd';
import React from 'react'; import React from 'react';
import '../style/separator.less'; import '../style/separator.less';
const Separator: React.FC = () => { const Separator: React.FC<{ showArrow?: boolean }> = ({ showArrow = true }) => {
return ( return (
<div className="separator"> <div className="separator">
<Divider <Divider
orientation="vertical" orientation="vertical"
style={{ height: 'calc(100vh - 89px)', marginInline: '0px' }} style={{ height: 'calc(100vh - 89px)', marginInline: '0px' }}
></Divider> ></Divider>
<span className="shape"></span> {showArrow && <span className="shape"></span>}
</div> </div>
); );
}; };