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;
}
// default need to pass content and operation
const DeleteModal = forwardRef((props, ref) => {
const intl = useIntl();
const { styles } = useStyles();
+1 -1
View File
@@ -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;
+5
View File
@@ -156,6 +156,11 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
{handleClickPrimary ? (
actionType === 'dropdown' ? (
<DropDownActions
styles={{
root: {
minWidth: 140
}
}}
menu={{
items: actionItems,
onClick: handleClickPrimary
+22 -13
View File
@@ -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>
);
+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.',
'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'
};
+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.',
'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'
};
+8 -2
View File
@@ -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 ==========
+4 -1
View File
@@ -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': '社区后端'
};
+3 -1
View File
@@ -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;
+65 -38
View File
@@ -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>
+11 -3
View File
@@ -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>
+20 -20
View File
@@ -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;
@@ -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;
+74 -30
View File
@@ -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 -2
View File
@@ -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>
);
};