Compare commits

...

3 Commits

Author SHA1 Message Date
  liu2592603532 9ce68b8813 merge 10 months ago
  liu2592603532 67861012ee . 11 months ago
  liu2592603532 5f478f8214 merge 11 months ago
64 changed files with 8129 additions and 3 deletions
Split View
  1. +1
    -0
      react-ui/.nvmrc
  2. +8
    -2
      react-ui/config/proxy.ts
  3. +37
    -1
      react-ui/config/routes.ts
  4. +20
    -0
      react-ui/mock/knowledge.ts
  5. BIN
      react-ui/src/assets/img/computer-img.png
  6. BIN
      react-ui/src/assets/img/graph-correct.png
  7. BIN
      react-ui/src/assets/img/graph-delete.png
  8. BIN
      react-ui/src/assets/img/graph-edit.png
  9. BIN
      react-ui/src/assets/img/graph-error.png
  10. BIN
      react-ui/src/assets/img/graph-star.png
  11. BIN
      react-ui/src/assets/img/icon-graph.png
  12. BIN
      react-ui/src/assets/img/runningstatus.png
  13. BIN
      react-ui/src/assets/img/tupu/b-circle.png
  14. BIN
      react-ui/src/assets/img/tupu/c-circle.png
  15. BIN
      react-ui/src/assets/img/user-answer.png
  16. +183
    -0
      react-ui/src/pages/knowledge/GraphCounter/index.less
  17. +413
    -0
      react-ui/src/pages/knowledge/GraphCounter/index.tsx
  18. +69
    -0
      react-ui/src/pages/knowledge/GraphView/index.less
  19. +293
    -0
      react-ui/src/pages/knowledge/GraphView/index.tsx
  20. +94
    -0
      react-ui/src/pages/knowledge/GraphView/utils.tsx
  21. +54
    -0
      react-ui/src/pages/knowledge/ModelMenu/index.less
  22. +92
    -0
      react-ui/src/pages/knowledge/ModelMenu/index.tsx
  23. +115
    -0
      react-ui/src/pages/knowledge/MyGraph/components/graphItem.tsx
  24. +181
    -0
      react-ui/src/pages/knowledge/MyGraph/components/index.less
  25. +181
    -0
      react-ui/src/pages/knowledge/MyGraph/index.less
  26. +124
    -0
      react-ui/src/pages/knowledge/MyGraph/index.tsx
  27. +192
    -0
      react-ui/src/pages/knowledge/MySubject/index.less
  28. +293
    -0
      react-ui/src/pages/knowledge/MySubject/index.tsx
  29. +206
    -0
      react-ui/src/pages/knowledge/OnlineGraph/index.less
  30. +632
    -0
      react-ui/src/pages/knowledge/OnlineGraph/index.tsx
  31. +976
    -0
      react-ui/src/pages/knowledge/SubjectView/index.jsx
  32. +93
    -0
      react-ui/src/pages/knowledge/SubjectView/index.less
  33. +94
    -0
      react-ui/src/pages/knowledge/SubjectView/utils.tsx
  34. +0
    -0
      react-ui/src/pages/knowledge/components/AddCodeConfigModal/index.less
  35. +228
    -0
      react-ui/src/pages/knowledge/components/AddCodeConfigModal/index.tsx
  36. +0
    -0
      react-ui/src/pages/knowledge/components/AddSubjectConfigModal/index.less
  37. +136
    -0
      react-ui/src/pages/knowledge/components/AddSubjectConfigModal/index.tsx
  38. +0
    -0
      react-ui/src/pages/knowledge/components/AddUpdateModal/index.less
  39. +161
    -0
      react-ui/src/pages/knowledge/components/AddUpdateModal/index.tsx
  40. +108
    -0
      react-ui/src/pages/knowledge/components/CodeConfigItem/index.less
  41. +94
    -0
      react-ui/src/pages/knowledge/components/CodeConfigItem/index.tsx
  42. +50
    -0
      react-ui/src/pages/knowledge/components/CodeSelectorModal/index.less
  43. +120
    -0
      react-ui/src/pages/knowledge/components/CodeSelectorModal/index.tsx
  44. +29
    -0
      react-ui/src/pages/knowledge/components/GlobalParamsDrawer/index.less
  45. +189
    -0
      react-ui/src/pages/knowledge/components/GlobalParamsDrawer/index.tsx
  46. +61
    -0
      react-ui/src/pages/knowledge/components/GraphNodeDrawer/index.less
  47. +382
    -0
      react-ui/src/pages/knowledge/components/GraphNodeDrawer/index.tsx
  48. +54
    -0
      react-ui/src/pages/knowledge/components/ModelMenu/index.less
  49. +108
    -0
      react-ui/src/pages/knowledge/components/ModelMenu/index.tsx
  50. +6
    -0
      react-ui/src/pages/knowledge/components/PropsLabel/index.less
  51. +44
    -0
      react-ui/src/pages/knowledge/components/PropsLabel/index.tsx
  52. +251
    -0
      react-ui/src/pages/knowledge/components/ResourceSelectorModal/config.tsx
  53. +76
    -0
      react-ui/src/pages/knowledge/components/ResourceSelectorModal/index.less
  54. +314
    -0
      react-ui/src/pages/knowledge/components/ResourceSelectorModal/index.tsx
  55. +91
    -0
      react-ui/src/pages/knowledge/components/SubjectEdgeDrawer/index.less
  56. +234
    -0
      react-ui/src/pages/knowledge/components/SubjectEdgeDrawer/index.tsx
  57. +91
    -0
      react-ui/src/pages/knowledge/components/SubjectNodeDrawer/index.less
  58. +635
    -0
      react-ui/src/pages/knowledge/components/SubjectNodeDrawer/index.tsx
  59. +0
    -0
      react-ui/src/pages/knowledge/components/subjecDetailEdit/index.less
  60. +144
    -0
      react-ui/src/pages/knowledge/components/subjecDetailEdit/index.tsx
  61. +0
    -0
      react-ui/src/pages/knowledge/config.tsx
  62. +93
    -0
      react-ui/src/services/graph/index.ts
  63. +64
    -0
      react-ui/src/services/subject/index.ts
  64. +15
    -0
      react-ui/src/types.ts

+ 1
- 0
react-ui/.nvmrc View File

@@ -0,0 +1 @@
v16.11.0

+ 8
- 2
react-ui/config/proxy.ts View File

@@ -20,14 +20,20 @@ export default {
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
'/api/': {
// 要代理的地址
target: 'http://172.20.32.181:31213', // 开发环境
target: 'http://172.20.32.114:8082/', // 开发环境
// target: 'http://172.20.32.197:31213', // 开发环境
// target: 'http://172.20.32.98:8082',
// target: 'http://172.20.32.150:8082',
// 配置了这个可以从 http 代理到 https
// 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true,
// pathRewrite: { '^/api': '' },
pathRewrite: { '^/api': '' },
},
// '/cp/': {
// target: 'http://172.20.32.14:9213', // 开发环境
// changeOrigin: true,
// pathRewrite: { '^/cp': '' },
// },
'/profile/avatar/': {
target: 'http://172.20.32.185:31213',
changeOrigin: true,


+ 37
- 1
react-ui/config/routes.ts View File

@@ -44,7 +44,7 @@ export default [
{
name: 'login',
path: '/user/login',
component: './User/Login',
component: './User/Login/login',
},
],
},
@@ -147,6 +147,42 @@ export default [
},
],
},
{
name: '知识图谱',
path: '/knowledge',
routes: [
{
name: '我的图谱',
path: 'myGraph',
component: './knowledge/MyGraph/index',
},
{
name: '我的主体',
path: 'mySubject',
component: './knowledge/MySubject/index',
},
{
name: '在线版本图谱',
path: 'onlineGraph',
component: './knowledge/OnlineGraph/index',
},
{
name: '预览主体',
path: 'subjectView/:name/:id',
component: './knowledge/SubjectView/index',
},
{
name: '预览图谱',
path: 'graphView',
component: './knowledge/GraphView/index.tsx',
},
{
name: '图谱配置',
path: 'graphCounter',
component: './knowledge/GraphCounter/index',
},
],
},
{
name: 'AI资产',
path: '/dataset',


+ 20
- 0
react-ui/mock/knowledge.ts View File

@@ -0,0 +1,20 @@

import { defineMock } from 'umi';

export default defineMock({
'GET /api/kg/configuration/list': {
code: 200,
msg: '操作成功',
data: [
{
id: '000000001',
description:"是个好东西",
name: '你收到了 14 份新周报',
updateTime: '2017-08-09',
effectiveTime: '2017-08-09',
type: 'notification',
}
],
},
});


BIN
react-ui/src/assets/img/computer-img.png View File

Before After
Width: 80  |  Height: 80  |  Size: 14 kB

BIN
react-ui/src/assets/img/graph-correct.png View File

Before After
Width: 32  |  Height: 32  |  Size: 788 B

BIN
react-ui/src/assets/img/graph-delete.png View File

Before After
Width: 40  |  Height: 40  |  Size: 1.8 kB

BIN
react-ui/src/assets/img/graph-edit.png View File

Before After
Width: 40  |  Height: 40  |  Size: 1.7 kB

BIN
react-ui/src/assets/img/graph-error.png View File

Before After
Width: 32  |  Height: 32  |  Size: 739 B

BIN
react-ui/src/assets/img/graph-star.png View File

Before After
Width: 32  |  Height: 32  |  Size: 1.3 kB

BIN
react-ui/src/assets/img/icon-graph.png View File

Before After
Width: 48  |  Height: 48  |  Size: 2.4 kB

BIN
react-ui/src/assets/img/runningstatus.png View File

Before After
Width: 96  |  Height: 96  |  Size: 2.0 kB

BIN
react-ui/src/assets/img/tupu/b-circle.png View File

Before After
Width: 80  |  Height: 80  |  Size: 3.4 kB

BIN
react-ui/src/assets/img/tupu/c-circle.png View File

Before After
Width: 80  |  Height: 80  |  Size: 3.5 kB

BIN
react-ui/src/assets/img/user-answer.png View File

Before After
Width: 80  |  Height: 80  |  Size: 4.5 kB

+ 183
- 0
react-ui/src/pages/knowledge/GraphCounter/index.less View File

@@ -0,0 +1,183 @@
.graphCounter-page {
:global {
.ant-tabs .ant-tabs-tab+.ant-tabs-tab {
margin-left: 54px;
}
}
height: 100%;
padding: 0 9px;
&__container {
height: 91px;
padding:14px 30px 0 30px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
color:#1d1d20;
font-size:16px;
&__box{
display: flex;
align-items: center;
color:#1d1d20;
font-size:15px;
&__update{
padding: 7px 10px;
background:rgba(22, 100, 255, 0.06);
border:1px solid rgba(22, 100, 255, 0.11);
border-radius:4px;
margin-right: 20px;
}
}
}
&__topCenter{
position: relative;
margin: 10px 0 15px 0;
width: 100%;
height: calc(100% - 100px);
background:#ffffff;
border-radius:10px;
box-shadow:0px 2px 12px rgba(180, 182, 191, 0.09);
}
&__title{
color:#1d1d20;
font-size:16px;
margin: 30px 0 15px 25px;
font-weight: 600;
}
&__listBox{
padding: 0 25px;
display: flex;
align-items: center;
&__itemBox{
position: relative;
width: calc(20% - 15px);
height:100px;
background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%);
border-radius:4px;
box-shadow:0px 3px 10px rgba(164, 169, 181, 0.12);
margin-right: 15px;
&__img{
position: absolute;
left: 30px;
top: 26px;
width: 48px;
height: 48px;
}
&__wordTitle{
position: absolute;
top: 26px;
left: 102px;
font-weight:600;
color:#1d1d20;
font-size:18px;

}
&__wordContent{
position: absolute;
top: 54px;
left: 102px;
color:#565658;
font-size:15px;
}
}
}
&__bottomBox{
padding: 23px 35px 23px 25px;
height: 100%;
&__leftText{
float: left;
display: flex;
align-items: start;
margin-bottom: 25px;
&__text{
width:1430px;
min-height:70px;
padding: 25px;
background:rgba(96, 107, 122, 0.06);
border:1px solid;
border-color:rgba(96, 107, 122, 0.2);
border-radius:0px 10px 10px 10px;
color:rgba(29, 29, 32, 0.75);
font-size:15px;
}
}
&__rightText{
float: right;
display: flex;
align-items: start;
margin-bottom: 25px;
&__text{
width:791px;
min-height:70px;
padding: 25px;
background:#1664ff;
border:1px solid;
border-color:rgba(22, 100, 255, 0.3);
border-radius:10px 0px 10px 10px;
color:#fff;
font-size:15px;
}
}
&__titleBox{
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 39px;
&__left{
display: flex;
align-items: center;
font-weight:600;
color:#1d1d20;
font-size:16px;
&__word{
display: flex;
align-items: center;
width:206px;
height:27px;
margin-left: 14px;
background:rgba(22, 100, 255, 0.09);
border-radius:4px;
padding: 2px 20px 2px 16px;
color:#4c5360;
font-size:14px;
font-weight: normal;
&__circle{
position: relative;
width:15px;
height:15px;
border-radius: 50%;
background:#ffffff;
box-shadow:0px 1px 3px rgba(0, 82, 217, 0.09);
margin-right: 13px;
&__inner{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
width:9px;
height:9px;
border-radius: 50%;
background:#1664ff;
box-shadow:0px 1px 3px rgba(22, 100, 255, 0.27);
}
}
}
}
&__right{
display: flex;
align-items: center;
// width:647px;
height:39px;
background:rgba(138, 138, 138, 0.05);
border-radius:4px;
padding-left: 30px;
}
}
&__queryparams{
margin: 17px 0;
}
}
}

+ 413
- 0
react-ui/src/pages/knowledge/GraphCounter/index.tsx View File

@@ -0,0 +1,413 @@
import KFIcon from '@/components/KFIcon';
import { useCacheState } from '@/hooks/pageCacheState';
import { getConfigurationList } from '@/services/graph';
import { openAntdModal } from '@/utils/modal';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useSearchParams } from '@umijs/max';
import { Button, Input, Table, type TablePaginationConfig, type TableProps, Tabs } from 'antd';
// import getConfigurationList from 'mock/knowledge';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal';
import styles from './index.less';
export type CodeConfigData = {
id: number;
code_repo_name: string;
code_repo_vis: number;
git_url: string;
git_branch: string;
git_user_name: string;
git_password: string;
ssh_key: string;
verify_mode: number;
create_by: string;
create_time: string;
update_by: string;
update_time: string;
};
export type EditorData = {
id: number;
name: string;
status: string;
computing_resource: string;
update_by: string;
create_time: string;
url: string;
};
function DatasetAnnotation() {
const [dataList, setDataList] = useState<CodeConfigData[] | undefined>(undefined);
const [tableData, setTableData] = useState<EditorData[]>([]);
const [cacheState, setCacheState] = useCacheState();
const [searchParams] = useSearchParams();
const tabActive = searchParams.get('tabActive') || '';
const [activeTab, setActiveTab] = useState<string>('1');
const navigate = useNavigate();
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
pageSize: 10,
},
);
const handleTableChange: TableProps<EditorData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
};

const [total, setTotal] = useState(0);
useEffect(() => {
getList();
setActiveTab(tabActive);
}, []);
const items = [
{
key: '1',
label: `元素链接配置`,
icon: <KFIcon type="icon-moxingjianjie" />,
children: null,
},
{
key: '2',
label: `问答模板配置`,
icon: <KFIcon type="icon-moxingwenjian" />,
children: null,
},
{
key: '3',
label: `问答体验`,
icon: <KFIcon type="icon-moxingwenjian" />,
children: null,
},
];
const createCodeConfig = () => {
const { close } = openAntdModal(AddCodeConfigModal, {
opType: OperationType.Create,
onOk: () => {
// getDataList();
// close();
},
});
};
const handleSubjectDelete = () => {
modalConfirm({
title: '确定要删除XXXX吗?',
content: '删除后本体将不可恢复,是否确认删除?',
onOk: () => {},
});
};
const getList = () => {
let params = {};
getConfigurationList(params).then((ret) => {
setTableData(ret.data);
});
};
const columnsNow: TableProps<EditorData>['columns'] = [
{
title: '元素链接配置名称',
dataIndex: 'name',
key: 'name',
render: tableCellRender(),
},
{
title: '元素链接配置描述',
dataIndex: 'description',
key: 'description',
render: tableCellRender(),
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
render: tableCellRender(),
},
{
title: '生效时间',
dataIndex: 'effectiveTime',
key: 'effectiveTime',
render: tableCellRender(),
},

{
title: '操作',
dataIndex: 'operation',
width: 300,
key: 'operation',
render: (_: any, record: EditorData) => (
<div>
<Button type="link" size="small" key="stop" icon={<KFIcon type="icon-tingzhi" />}>
查看
</Button>
<Button type="link" size="small" key="stop" icon={<KFIcon type="icon-tingzhi" />}>
编辑
</Button>
<Button type="link" size="small" key="stop" icon={<KFIcon type="icon-tingzhi" />}>
删除
</Button>
</div>
),
},
];
const columnsFu: TableProps<EditorData>['columns'] = [
{
title: '运行编号',
dataIndex: 'name',
key: 'name',
width: '30%',
},
{
title: '事件类型',
dataIndex: 'status',
key: 'status',
width: '10%',
},
{
title: '本体',
dataIndex: 'computing_resource',
key: 'computing_resource',
width: '20%',
render: tableCellRender(),
},
{
title: '状态',
dataIndex: 'update_by',
key: 'update_by',
width: '20%',
render: tableCellRender(),
},
{
title: '开始时间',
dataIndex: 'create_time',
key: 'create_time',
width: '20%',
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '结束时间',
dataIndex: 'create_time',
key: 'create_time',
width: '20%',
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '操作',
dataIndex: 'operation',
width: 300,
key: 'operation',
// render: (_: any, record: EditorData) => (
// <div>
// {record.status === DevEditorStatus.Pending ||
// record.status === DevEditorStatus.Running ? (
// <Button
// type="link"
// size="small"
// key="stop"
// icon={<KFIcon type="icon-tingzhi" />}
// onClick={() => stopEditor(record.id)}
// >
// 停止
// </Button>
// ) : (
// <Button
// type="link"
// size="small"
// key="debug"
// icon={<KFIcon type="icon-tiaoshi" />}
// onClick={() => startEditor(record.id)}
// >
// 启动
// </Button>
// )}
// {record.status === DevEditorStatus.Running ? (
// <Button
// type="link"
// size="small"
// key="jingxiang"
// icon={<KFIcon type="icon-jingxiang" />}
// onClick={() => createMirror(record.id)}
// >
// 制作镜像
// </Button>
// ) : null}
// <ConfigProvider
// theme={{
// token: {
// colorLink: themes['warningColor'],
// },
// }}
// >
// <Button
// type="link"
// size="small"
// key="remove"
// icon={<KFIcon type="icon-shanchu" />}
// onClick={() => handleEditorDelete(record)}
// >
// 删除
// </Button>
// </ConfigProvider>
// </div>
// ),
},
];

return (
<div className={styles['graphCounter-page']}>
<div className={styles['graphCounter-page__container']}>
<span>552815033</span>
<Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
</div>
<div className={styles['graphCounter-page__topCenter']}>
<div className={styles['graphCounter-page__bottomBox']}>
{activeTab == '1' && (
<div>
<div className={styles['graphCounter-page__bottomBox__queryparams']}>
<Input.Search
placeholder="按镜像名称筛选"
allowClear
// onSearch={onSearch}
// onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
// value={inputText}
/>
<Button
key="info"
type="primary"
style={{ marginLeft: '20px' }}
// onClick={() => toDetail(record)}
>
查询
</Button>
<Button
key="info"
style={{ marginLeft: '20px' }}
onClick={() => handleSubjectDelete()}
>
批量删除
</Button>
</div>
<Table
dataSource={tableData}
columns={columnsNow}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="id"
/>
</div>
)}
{activeTab == '2' && (
<div>
<div className={styles['graphCounter-page__bottomBox__queryparams']}>
<Input.Search
placeholder="按镜像名称筛选"
allowClear
// onSearch={onSearch}
// onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
// value={inputText}
/>
<Button
key="info"
type="primary"
style={{ marginLeft: '20px' }}
// onClick={() => toDetail(record)}
>
查询
</Button>
<Button
key="info"
style={{ marginLeft: '20px' }}
onClick={() => handleSubjectDelete()}
>
批量删除
</Button>
</div>
<Table
dataSource={tableData}
columns={columnsNow}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="id"
/>
</div>
)}
{activeTab == '3' && (
<div style={{ height: '100%' }}>
<div className={styles['graphCounter-page__bottomBox__leftText']}>
<img
style={{ width: '40px', marginRight: '20px' }}
src={require('@/assets/img/computer-img.png')}
alt=""
draggable={false}
/>
<div className={styles['graphCounter-page__bottomBox__leftText__text']}>
Heyow too! You sound cute! How can I help you today? Oh, you're curious about my
smarts, huh? Well, let me assure you, I'm super up-to-date! I update every minute
and second, so I've got the latest scoop at my digital fingertips!
</div>
</div>
<div className={styles['graphCounter-page__bottomBox__rightText']}>
<div className={styles['graphCounter-page__bottomBox__rightText__text']}>
Heyow too! You sound cute! How can I help you today? Oh, you're curious about my
smarts, huh? Well, let me assure you, I'm super up-to-date! I update every minute
and second, so I've got the latest scoop at my digital fingertips!
</div>
<img
style={{ width: '40px', marginLeft: '20px' }}
src={require('@/assets/img/user-answer.png')}
alt=""
draggable={false}
/>
</div>
<div className={styles['graphCounter-page__bottomBox__leftText']}>
<img
style={{ width: '40px', marginRight: '20px' }}
src={require('@/assets/img/computer-img.png')}
alt=""
draggable={false}
/>
<div className={styles['graphCounter-page__bottomBox__leftText__text']}>
Heyow too! You sound cute! How can I help you today? Oh, you're curious about my
smarts, huh? Well, let me assure you, I'm super up-to-date! I update every minute
and second, so I've got the latest scoop at my digital fingertips!
</div>
</div>
<Input.TextArea
style={{
position: 'absolute',
bottom: '30px',
left: '40px',
width: 'calc(100% - 80px)',
}}
placeholder="请输入内容"
autoSize={{ minRows: 6, maxRows: 20 }}
allowClear
/>
</div>
)}
</div>
</div>
</div>
);
}

export default DatasetAnnotation;

+ 69
- 0
react-ui/src/pages/knowledge/GraphView/index.less View File

@@ -0,0 +1,69 @@
.subjectView-container {
display: flex;
height: 100%;
background-color: #fff;

&__workflow {
flex: 1 1 0;
min-width: 0;
height: 100%;
&__top {
display: flex;
align-items: center;
justify-content: end;
width: 100%;
height: 52px;
padding: 0 20px;
background: #ffffff;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);
&__circle{
width:8px;
height:8px;
border-radius: 50%;
margin-right: 10px;
}
}

&__graph {
width: 100%;
height: calc(100% - 52px);
background-color: @background-color;
background-image: url(@/assets/img/pipeline-canvas-bg.png);
background-size: 100% 100%;
}
}
}

:global {
.pipeline-context-menu {
width: 78px;
padding: 10px 0;
background: #ffffff;
border-radius: 6px;
box-shadow: 0px 0px 6px rgba(40, 84, 168, 0.21);

&__item {
display: flex;
align-items: center;
width: 100%;
height: 34px;
padding-left: 12px;
color: @text-color-secondary;
font-size: 15px;
cursor: pointer;

&:hover {
color: #0d5ef8;
font-weight: 500;
background-color: .addAlpha(#8895a8, 0.11) [];
}

&__icon {
width: 1em;
height: 1em;
margin-right: 9px;
fill: currentColor;
}
}
}
}

+ 293
- 0
react-ui/src/pages/knowledge/GraphView/index.tsx View File

@@ -0,0 +1,293 @@
import KFIcon from '@/components/KFIcon';
import { useCacheState } from '@/hooks/pageCacheState';
import { deleteSubject, getKnowledgeSubject } from '@/services/subject';
import { openAntdModal } from '@/utils/modal';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { App, Button, Input, Table, type TablePaginationConfig, type TableProps } from 'antd';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import AddSubjectConfigModal, { OperationType } from '../components/AddSubjectConfigModal';
import styles from './index.less';

export type CodeConfigData = {
id: number;
code_repo_name: string;
code_repo_vis: number;
git_url: string;
git_branch: string;
git_user_name: string;
git_password: string;
ssh_key: string;
verify_mode: number;
create_by: string;
create_time: string;
update_by: string;
update_time: string;
};
export type EditorData = {
id: number;
name: string;
updateTime: string;
conceptsCount: string;
update_by: string;
relationsCount: string;
description: string;
};
function GraphView() {
const [dataList, setDataList] = useState<CodeConfigData[] | undefined>(undefined);
const [tableData, setTableData] = useState<EditorData[]>([]);
const [cacheState, setCacheState] = useCacheState();
const [activeTab, setActiveTab] = useState<string>('1');

const navigate = useNavigate();
const [inputText, setInputText] = useState<string | undefined>(undefined);
const [pagination, setPagination] = useState<TablePaginationConfig>({
current: 1,
pageSize: 10,
});
const { message } = App.useApp();
const handleTableChange: TableProps<EditorData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
};
const [total, setTotal] = useState(0);
useEffect(() => {
getList();
}, []);
const items = [
{
key: 1,
label: `图谱版本`,
icon: <KFIcon type="icon-moxingjianjie" />,
children: null,
},
{
key: 2,
label: `更新日志`,
icon: <KFIcon type="icon-moxingwenjian" />,
children: null,
},
];
const createCodeConfig = () => {
const { close } = openAntdModal(AddSubjectConfigModal, {
opType: OperationType.Create,
onOk: () => {
getList();
close();
},
});
};
const getList = () => {
const params: Record<string, any> = {
pageNum: pagination.current,
pageSize: pagination.pageSize,
name: inputText,
};
getKnowledgeSubject(params).then((ret) => {
console.log(ret.rows);

setTableData(ret.rows);
});
};
// 处理删除
const goSubjectEdit = (row) => {
console.log(row);

navigate(`/knowledge/subjectView/${row.name}/${row.id}`);
};

const handleSubjectDelete = (row) => {
modalConfirm({
title: '确定要删除该主体吗?',
content: '删除后主体将不可恢复,是否确认删除?',
onOk: () => {
deleteSubject(row.id).then((res) => {
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
...prev,
current: 1,
}));
} else {
getList();
}
}
});
},
});
};
const columnsNow: TableProps<EditorData>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: '20%',
render: tableCellRender(false, TableCellValueType.Index, {
page: pagination.current! - 1,
pageSize: pagination.pageSize!,
}),
},
{
title: '主体名称',
dataIndex: 'name',
key: 'name',
width: '20%',
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
width: '20%',
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '概念个数',
dataIndex: 'conceptsCount',
key: 'conceptsCount',
width: '20%',
render: tableCellRender(),
},
{
title: '关系个数',
dataIndex: 'relationsCount',
key: 'relationsCount',
width: '20%',
render: tableCellRender(),
},

{
title: '主体描述',
dataIndex: 'description',
key: 'description',
width: '30%',
render: tableCellRender(),
},
{
title: '操作',
dataIndex: 'operation',
width: 300,
key: 'operation',
render: (_: any, record: EditorData) => (
<div>
<Button
type="link"
size="small"
key="edit"
onClick={goSubjectEdit}
icon={<KFIcon type="icon-chakanxiangqing" />}
>
查看
</Button>
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-bianji" />}
onClick={() => goSubjectEdit(record)}
>
编辑
</Button>
<Button type="link" size="small" key="run" icon={<KFIcon type="icon-bendishangchuan" />}>
导出
</Button>
<Button
type="link"
size="small"
key="remove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleSubjectDelete(record)}
>
删除
</Button>
</div>
),
},
];
return (
<div className={styles['mySubject-page']}>
<div className={styles['mySubject-page__container']}>
主体管理
<div className={styles['mySubject-page__container__box']}>
<img
className={styles['mySubject-page__container__box__img']}
src={require('@/assets/img/icon-graph.png')}
draggable={false}
alt=""
/>
<span>
您还可以创建<span style={{ fontSize: '16px', color: '#1664ff' }}>47</span>个框架{' '}
</span>
</div>
</div>
<div className={styles['mySubject-page__topCenter']}>
<div className={styles['mySubject-page__bottomBox']}>
<div className={styles['mySubject-page__bottomBox__queryparams']}>
<div className={styles['mySubject-page__bottomBox__queryparams__leftbox']}>
<Input.Search
placeholder="按名称筛选"
allowClear
onSearch={getList}
onChange={(e) => setInputText(e.target.value)}
value={inputText}
/>
{/* <Button
type="primary"
key="primary"
style={{ margin: '0 20px' }}
onClick={() => getList()}
>
查询
</Button> */}
<Button
style={{ marginLeft: '20px' }}
key="info"
onClick={() => handleSubjectDelete()}
>
批量删除
</Button>
</div>
<div>
<Button
onClick={() => createCodeConfig()}
// onClick={() => toDetail(record)}
>
创建主体
</Button>
<Button key="info" style={{ margin: '0 20px' }}>
obs导入
</Button>
</div>
</div>

<Table
dataSource={tableData}
columns={columnsNow}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default GraphView;

+ 94
- 0
react-ui/src/pages/knowledge/GraphView/utils.tsx View File

@@ -0,0 +1,94 @@
import { PipelineGlobalParam, PipelineNodeModelParameter } from '@/types';
import { parseJsonText } from '@/utils';
import { Graph, INode } from '@antv/g6';
import { type MenuProps } from 'antd';

// 找到节点所以的上游节点
export const findAllParentNodes = (graph: Graph, node: INode) => {
const parentNodes: INode[] = [];
let index = -1;
let targetNode = node;
while (targetNode) {
const neighbors: INode[] = graph.getNeighbors(targetNode, 'source');
for (const sourceNode of neighbors) {
// 避免重复,也避免循环
const idx = parentNodes.findIndex((item) => sourceNode.getID() === item.getID());
if (idx === -1 && sourceNode.getID() !== node.getID()) {
parentNodes.push(sourceNode);
}
}
targetNode = parentNodes[++index];
}

return parentNodes;
};

// 判断并找到全局参数第一个重复项,有重复项时,全局参数不允许保存
export function findFirstDuplicate(params: PipelineGlobalParam[]): string | null {
const seen = new Set();
for (const item of params) {
if (seen.has(item.param_name)) {
return item.param_name;
}
seen.add(item.param_name);
}
return null;
}

// 创建参数下拉菜单
export function createMenuItems(
params: PipelineGlobalParam[],
parentNodes: INode[],
): MenuProps['items'] {
const nodes: MenuProps['items'] = parentNodes.map((item) => {
const model = item.getModel();
const out_parameters = model.out_parameters as string | undefined | null;
const out_parametersObj = parseJsonText(out_parameters);
const outParametersList = Object.keys(out_parametersObj ?? {});
return {
key: model.id as string,
label: model.label as string,
children: outParametersList.map((key: string) => ({
key: key as string,
label: out_parametersObj[key].label,
})),
};
});

if (params.length > 0) {
return [
{
key: 'global',
label: '全局参数',
children: params.map((item) => ({
key: item.param_name,
label: item.param_name,
})),
},
...nodes,
];
} else {
return [...nodes];
}
}

export function getInParameterComponent(
parameter: PipelineNodeModelParameter,
): React.ReactNode | null {
if (parameter.value) {
}

return null;
}

// 判断是否允许输入
export function canInput(parameter: PipelineNodeModelParameter) {
const { type, item_type } = parameter;
return !(
type === 'ref' &&
(item_type === 'dataset' ||
item_type === 'model' ||
item_type === 'image' ||
item_type === 'code')
);
}

+ 54
- 0
react-ui/src/pages/knowledge/ModelMenu/index.less View File

@@ -0,0 +1,54 @@
.collapse {
flex: none;
width: 250px;
height: 100%;

:global {
.ant-collapse {
height: calc(100% - 60px);
overflow-y: auto;
background-color: #fff;
border-color: transparent !important;
}
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
margin-bottom: 5px;
padding: 20px 16px 15px 16px;
background-color: #fff;
border-color: transparent;
}

.ant-collapse > .ant-collapse-item {
margin: 0 10px;
border-bottom: 0.5px dashed rgba(20, 49, 179, 0.12);
border-radius: 0px;
}
.ant-collapse .ant-collapse-content {
padding-bottom: 15px;
border-top: 1px solid transparent;
}
.ant-collapse .ant-collapse-content > .ant-collapse-content-box {
padding: 0;
}
}
}
.collapseItem {
display: flex;
align-items: center;
height: 40px;
padding: 0 16px;
color: @text-color-secondary;
font-size: 14px;
border-radius: 4px;
cursor: pointer;

&:hover {
color: @primary-color;
background: rgba(22, 100, 255, 0.08);
}
}
.modelMenusTitle {
margin-bottom: 10px;
padding: 12px 25px;
color: #111111;
font-size: 16px;
}

+ 92
- 0
react-ui/src/pages/knowledge/ModelMenu/index.tsx View File

@@ -0,0 +1,92 @@
import { getComponentAll } from '@/services/pipeline/index.js';
import { PipelineNodeModel } from '@/types';
import { to } from '@/utils/promise';
import { Collapse, type CollapseProps } from 'antd';
import { useEffect, useState } from 'react';
import Styles from './index.less';

type ModelMenuData = {
key: string;
name: string;
value: PipelineNodeModel[];
};

type ModelMenuProps = {
onComponentDragEnd: (
data: PipelineNodeModel & { x: number; y: number; label: string; img: string },
) => void;
};
const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => {
const [modelMenusList, setModelMenusList] = useState<ModelMenuData[]>([]);
const [collapseItems, setCollapseItems] = useState<CollapseProps['items']>([]);

useEffect(() => {
getAllComponents();
}, []);

// 获取所有组件
const getAllComponents = async () => {
const [res] = await to(getComponentAll());
if (res && res.data) {
const menus = res.data as ModelMenuData[];
setModelMenusList(menus);
const items = menus.map((item) => {
return {
key: item.key,
label: item.name,
children: item.value.map((ele) => {
return (
<div
key={ele.id}
draggable="true"
onDragEnd={(e) => {
dragEnd(e, ele);
}}
className={Styles.collapseItem}
>
{ele.icon_path && (
<img
style={{ height: '16px', marginRight: '15px' }}
src={`/assets/images/${ele.icon_path}.png`}
draggable={false}
alt=""
/>
)}
{ele.component_label}
</div>
);
}),
};
});
setCollapseItems(items);
}
};

const dragEnd = (e: React.DragEvent<HTMLDivElement>, data: PipelineNodeModel) => {
onComponentDragEnd({
...data,
x: e.clientX,
y: e.clientY,
label: data.component_label,
img: `/assets/images/${data.icon_path}.png`,
});
};

const defaultActiveKey = modelMenusList.map((item) => item.key + '');
return (
<div className={Styles.collapse}>
<div className={Styles.modelMenusTitle}>组件库</div>
{/* 这样 defaultActiveKey 才能生效 */}
{modelMenusList.length > 0 ? (
<Collapse
collapsible="header"
expandIconPosition="end"
defaultActiveKey={defaultActiveKey}
items={collapseItems}
></Collapse>
) : null}
</div>
);
};

export default ModelMenu;

+ 115
- 0
react-ui/src/pages/knowledge/MyGraph/components/graphItem.tsx View File

@@ -0,0 +1,115 @@
import KFIcon from '@/components/KFIcon';
import { formatDate } from '@/utils/date';
import { Button, Typography } from 'antd';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import styles from './index.less';
export type CodeConfigData = {
id: number; //主键
name: string; //名称
spec: string; //图谱规格
createTime: string; //创建时间
relationCount: number; //关系数
entityCount: number; //实体数
};

type CodeConfigItemProps = {
item: CodeConfigData;
onClick?: (item: CodeConfigData) => void;
onEdit?: (item: CodeConfigData) => void;
onRemove?: (item: CodeConfigData) => void;
};
function graphItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps) {
const [dataList, setDataList] = useState<CodeConfigData[] | undefined>([]);
const navigate = useNavigate();

useEffect(() => {}, []);
return (
<div
className={styles['myGraph-page__centercontent__box']}
onClick={(e) => {
e.stopPropagation();
onClick?.(item);
}}
>
<div className={styles['myGraph-page__centercontent__box__top']}>
<div className={styles['myGraph-page__centercontent__box__top__left']}>
<img
className={styles['myGraph-page__centercontent__box__top__left__img']}
src={require('@/assets/img/graph-star.png')}
draggable={false}
alt=""
/>
<Typography.Paragraph
className={styles['myGraph-page__centercontent__box__top__left__name']}
ellipsis={{ tooltip: item.name }}
>
{item.name}
</Typography.Paragraph>
</div>
<div className={styles['myGraph-page__centercontent__box__top__right']}>
<Button
type="text"
shape="circle"
style={{ marginLeft: 'auto', marginRight: '4px' }}
onClick={(e) => {
e.stopPropagation();
onEdit?.(item);
}}
>
<KFIcon type="icon-bianji" font={17} />
</Button>
<Button
type="text"
shape="circle"
style={{ marginRight: '-4px' }}
onClick={(e) => {
e.stopPropagation();
onRemove?.(item);
}}
>
<KFIcon type="icon-shanchu" font={17} />
</Button>
</div>
</div>
<div className={styles['myGraph-page__centercontent__box__center']}>
<span>
<span className={styles['myGraph-page__centercontent__box__center__word']}>图谱规格</span>
<span className={styles['myGraph-page__centercontent__box__center__word']}>
{item.spec}
</span>
</span>
<span>
<span className={styles['myGraph-page__centercontent__box__center__word']}>
实体/关系
</span>
<span className={styles['myGraph-page__centercontent__box__center__word']}>
{item.entityCount / item.relationCount}
</span>
</span>
<span>
<span className={styles['myGraph-page__centercontent__box__center__word']}>创建时间</span>
<span className={styles['myGraph-page__centercontent__box__center__word']}>
{formatDate(item.createTime)}
</span>
</span>
</div>
<div className={styles['myGraph-page__centercontent__box__bottom']}>
<div className={styles['myGraph-page__centercontent__box__bottom__left']}>
<img
className={styles['myGraph-page__centercontent__box__bottom__left__img']}
src={require('@/assets/img/graph-correct.png')}
draggable={false}
alt=""
/>
<span>可用</span>
</div>
<span>
试用期剩余<span style={{ color: '#c73131' }}>27</span>天
</span>
</div>
</div>
);
}

export default graphItem;

+ 181
- 0
react-ui/src/pages/knowledge/MyGraph/components/index.less View File

@@ -0,0 +1,181 @@
.myGraph-page {
height: 100%;
&__container {
display: flex;
align-items: center;
height: 50px;
padding-left: 30px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
color:#1d1d20;
font-size:16px;
&__box{
display: flex;
align-items: center;
width:206px;
height:27px;
background:rgba(22, 100, 255, 0.09);
border-radius:4px;
color:#4c5360;
font-size:14px;
margin-left: 5px;
&__img{
width: 24px;
height: 24px;
margin: 0 5px 0 9px;
}
}
}
&__centercontent{
margin-top: 10px;
display: flex;
flex-wrap: wrap;
align-items: center;
&__box{
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
width: calc(25% - 15px);
height:229px;
background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%);
border:2px solid;
border-color:#ffffff;
border-radius:4px;
box-shadow:0px 3px 10px rgba(164, 169, 181, 0.13);
margin: 20px 0 0 20px;
padding: 20px;
cursor: pointer;
&:hover {
border-color: @primary-color;
}
&__top{
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 22px;
color:#1664ff;
font-size:16px;
&__left{
display: flex;
align-items: center;
&__img{
width: 16px;
height: 16px;
margin-right: 10px;
}
&__name{
position: relative;
margin-right: 20px;
margin-bottom: 0 !important;
color: @text-color;
font-weight: 500;
font-size: 16px;
&::after {
position: absolute;
top: 14px;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(
to right,
.addAlpha(@primary-color, 0.4) [] 0,
.addAlpha(@primary-color, 0) [] 100%
);
content: '';
}
}
}
&__right{
display: flex;
align-items: center;
&__img{
width: 20px;
height: 20px;
margin-left: 15px;
cursor: pointer;
}
}
}
&__center{
display: flex;
flex-direction: column;
justify-content: space-between;
width:100%;
height:114px;
background:rgba(22, 100, 255, 0.04);
border-radius:4px;
padding: 15px;
&__word{
color:#565658;
font-size:14px;
margin-right: 15px;
}
}
&__bottom{
width: 100%;
height: 18px;
display: flex;
justify-content: space-between;
align-items: center;
color:#808080;
font-size:13px;
&__left{
display: flex;
align-items: center;
&__img{
width: 16px;
height: 16px;
margin-right: 6px;
}
}
}
}
&__addBox{
width:350px;
height:189px;
background:rgba(22, 100, 255, 0.04);
border:1px dashed;
border-color:rgba(22, 100, 255, 0.5);
border-radius:4px;
margin: 20px 0 0 40px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
&__button{
position: relative;
width:53px;
height:53px;
background:rgba(22, 100, 255, 0.05);
border:1px dotted;
border-color:#1664ff;
border-radius: 50%;
&__horizontal{
width: 13px;
height: 0;
border-top: 2px solid #1664ff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-6.5px, -1px)
}
&__vertical{
width: 0;
height: 13px;
border-left: 2px solid #1664ff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-1px,-6.5px)
}
}
}
}
}

+ 181
- 0
react-ui/src/pages/knowledge/MyGraph/index.less View File

@@ -0,0 +1,181 @@
.myGraph-page {
height: 100%;
&__container {
display: flex;
align-items: center;
height: 50px;
padding-left: 30px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
color:#1d1d20;
font-size:16px;
&__box{
display: flex;
align-items: center;
width:206px;
height:27px;
background:rgba(22, 100, 255, 0.09);
border-radius:4px;
color:#4c5360;
font-size:14px;
margin-left: 5px;
&__img{
width: 24px;
height: 24px;
margin: 0 5px 0 9px;
}
}
}
&__centercontent{
margin-top: 10px;
display: flex;
flex-wrap: wrap;
align-items: center;
&__box{
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
width: calc(25% - 15px);
height:229px;
background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%);
border:2px solid;
border-color:#ffffff;
border-radius:4px;
box-shadow:0px 3px 10px rgba(164, 169, 181, 0.13);
margin: 20px 0 0 20px;
padding: 20px;
cursor: pointer;
&:hover {
border-color: @primary-color;
}
&__top{
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 22px;
color:#1664ff;
font-size:16px;
&__left{
display: flex;
align-items: center;
&__img{
width: 16px;
height: 16px;
margin-right: 10px;
}
&__name{
position: relative;
margin-right: 20px;
margin-bottom: 0 !important;
color: @text-color;
font-weight: 500;
font-size: 16px;
&::after {
position: absolute;
top: 14px;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(
to right,
.addAlpha(@primary-color, 0.4) [] 0,
.addAlpha(@primary-color, 0) [] 100%
);
content: '';
}
}
}
&__right{
display: flex;
align-items: center;
&__img{
width: 20px;
height: 20px;
margin-left: 15px;
cursor: pointer;
}
}
}
&__center{
display: flex;
flex-direction: column;
justify-content: space-between;
width:100%;
height:114px;
background:rgba(22, 100, 255, 0.04);
border-radius:4px;
padding: 15px;
&__word{
color:#565658;
font-size:14px;
margin-right: 15px;
}
}
&__bottom{
width: 100%;
height: 18px;
display: flex;
justify-content: space-between;
align-items: center;
color:#808080;
font-size:13px;
&__left{
display: flex;
align-items: center;
&__img{
width: 16px;
height: 16px;
margin-right: 6px;
}
}
}
}
&__addBox{
width:350px;
height:189px;
background:rgba(22, 100, 255, 0.04);
border:1px dashed;
border-color:rgba(22, 100, 255, 0.5);
border-radius:4px;
margin: 20px 0 0 40px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
&__button{
position: relative;
width:53px;
height:53px;
background:rgba(22, 100, 255, 0.05);
border:1px dotted;
border-color:#1664ff;
border-radius: 50%;
&__horizontal{
width: 13px;
height: 0;
border-top: 2px solid #1664ff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-6.5px, -1px)
}
&__vertical{
width: 0;
height: 13px;
border-left: 2px solid #1664ff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-1px,-6.5px)
}
}
}
}
}

+ 124
- 0
react-ui/src/pages/knowledge/MyGraph/index.tsx View File

@@ -0,0 +1,124 @@
import { deleteKnowledgeGraph, getKnowledgeGraph } from '@/services/graph';
import { openAntdModal } from '@/utils/modal';
import { modalConfirm } from '@/utils/ui';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal';
import GraphItem from './components/graphItem';
import styles from './index.less';
export type CodeConfigData = {
id: number; //主键
name: string; //名称
status: number; //状态,0代表不可用,1代表可用
spec: string; //图谱规格
createTime: string; //创建时间
relationCount: number; //关系数
entityCount: number; //实体数
};
// type CodeConfigItemProps = {
// item: CodeConfigData;
// onClick?: (item: CodeConfigData) => void;
// onEdit?: (item: CodeConfigData) => void;
// onRemove?: (item: CodeConfigData) => void;
// };
function DatasetAnnotation() {
const [dataList, setDataList] = useState<CodeConfigData[] | undefined>([]);
const navigate = useNavigate();
const createCodeConfig = () => {
const { close } = openAntdModal(AddCodeConfigModal, {
opType: OperationType.Create,
onOk: () => {
getList();
close();
},
});
};
// 删除
const handleRemove = (record: CodeConfigData) => {
console.log(record);

modalConfirm({
title: '确定删除这个图谱?',
onOk: () => {
deleteKnowledgeGraph(record.id);
getList();
},
});
};

// 修改
const handleEdit = (record: CodeConfigData) => {
const { close } = openAntdModal(AddCodeConfigModal, {
opType: OperationType.Update,
codeConfigData: record,
onOk: () => {
getList();
close();
},
});
};

// 查看
// const handleClick = (record: CodeConfigData) => {
// const { git_url, git_branch } = record;
// const url = getGitUrl(git_url, git_branch);
// window.open(url, '_blank');
// };
const getList = () => {
getKnowledgeGraph().then((ret) => {
console.log(ret.rows);

setDataList(ret.rows);
});
};
const clickGraph = (record: any) => {
navigate(`/knowledge/onlineGraph?id=${record.id}&version=${record.version}`);
// navigate({ pathname: `/knowledge/onlineGraph` });
};
useEffect(() => {
getList();
}, []);
return (
<div className={styles['myGraph-page']}>
<div className={styles['myGraph-page__container']}>
图谱管理
<div className={styles['myGraph-page__container__box']}>
<img
className={styles['myGraph-page__container__box__img']}
src={require('@/assets/img/icon-graph.png')}
draggable={false}
alt=""
/>
<span>
您还可以创建<span style={{ fontSize: '16px', color: '#1664ff' }}>4</span>个图谱{' '}
</span>
</div>
</div>
<div className={styles['myGraph-page__centercontent']}>
{dataList && dataList.length != 0 && (
<>
{dataList.map((item) => (
<GraphItem
item={item}
key={item.id}
onRemove={handleRemove}
onEdit={handleEdit}
onClick={clickGraph}
></GraphItem>
))}
</>
)}
<div className={styles['myGraph-page__centercontent__addBox']} onClick={createCodeConfig}>
<div className={styles['myGraph-page__centercontent__addBox__button']}>
<div
className={styles['myGraph-page__centercontent__addBox__button__horizontal']}
></div>
<div className={styles['myGraph-page__centercontent__addBox__button__vertical']}></div>
</div>
</div>
</div>
</div>
);
}

export default DatasetAnnotation;

+ 192
- 0
react-ui/src/pages/knowledge/MySubject/index.less View File

@@ -0,0 +1,192 @@
.mySubject-page {
height: 100%;
padding: 0 9px;
&__container {
display: flex;
align-items: center;
height: 50px;
padding:0 30px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
color:#1d1d20;
font-size:16px;
&__box{
display: flex;
align-items: center;
width:206px;
height:27px;
background:rgba(22, 100, 255, 0.09);
border-radius:4px;
color:#4c5360;
font-size:14px;
margin-left: 5px;
&__img{
width: 24px;
height: 24px;
margin: 0 5px 0 9px;
}

}
}
&__topCenter{
margin: 10px 0 15px 0;
width: 100%;
background:#ffffff;
border-radius:10px;
box-shadow:0px 2px 12px rgba(180, 182, 191, 0.09);
}
&__title{
color:#1d1d20;
font-size:16px;
margin: 30px 0 15px 25px;
font-weight: 600;
}
&__listBox{
padding: 0 25px;
display: flex;
align-items: center;
&__itemBox{
position: relative;
width: calc(20% - 15px);
height:100px;
background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%);
border-radius:4px;
box-shadow:0px 3px 10px rgba(164, 169, 181, 0.12);
margin-right: 15px;
&__img{
position: absolute;
left: 30px;
top: 26px;
width: 48px;
height: 48px;
}
&__wordTitle{
position: absolute;
top: 26px;
left: 102px;
font-weight:600;
color:#1d1d20;
font-size:18px;

}
&__wordContent{
position: absolute;
top: 54px;
left: 102px;
color:#565658;
font-size:15px;
}
}
}
&__centerListBox{
display: flex;
align-items: center;
padding: 20px 45px;
&__itemBox{
position: relative;
width: calc(20% - 20px);
height: 229px;
margin-right: 20px;
background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%);
border-radius:4px;
box-shadow:0px 3px 10px rgba(164, 169, 181, 0.13);
&__img{
width: 16px;
height: 16px;
position: absolute;
top: 24.5px;
left: 20px;
}
&__title{
position: absolute;
top: 20.1px;
left: 46px;
color:#1d1d20;
font-size:16px;
font-weight: 600;
}
&__content{
position: absolute;
width: calc(100% - 40px);
height: 114px;
top: 61px;
left: 20px;
background:rgba(22, 100, 255, 0.04);
border-radius:4px;
}
}
}
&__bottomBox{
padding: 23px 35px 23px 25px;
&__titleBox{
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 39px;
&__left{
display: flex;
align-items: center;
font-weight:600;
color:#1d1d20;
font-size:16px;
&__word{
display: flex;
align-items: center;
width:206px;
height:27px;
margin-left: 14px;
background:rgba(22, 100, 255, 0.09);
border-radius:4px;
padding: 2px 20px 2px 16px;
color:#4c5360;
font-size:14px;
font-weight: normal;
&__circle{
position: relative;
width:15px;
height:15px;
border-radius: 50%;
background:#ffffff;
box-shadow:0px 1px 3px rgba(0, 82, 217, 0.09);
margin-right: 13px;
&__inner{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
width:9px;
height:9px;
border-radius: 50%;
background:#1664ff;
box-shadow:0px 1px 3px rgba(22, 100, 255, 0.27);
}
}
}
}
&__right{
display: flex;
align-items: center;
// width:647px;
height:39px;
background:rgba(138, 138, 138, 0.05);
border-radius:4px;
padding-left: 30px;
}
}
&__queryparams{
margin: 17px 0;
display: flex;
align-items: center;
justify-content: space-between;
&__leftbox{
display: flex;
align-items: center;
}
}
}
}

+ 293
- 0
react-ui/src/pages/knowledge/MySubject/index.tsx View File

@@ -0,0 +1,293 @@
import KFIcon from '@/components/KFIcon';
import { useCacheState } from '@/hooks/pageCacheState';
import { deleteSubject, getKnowledgeSubject } from '@/services/subject';
import { openAntdModal } from '@/utils/modal';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { App, Button, Input, Table, type TablePaginationConfig, type TableProps } from 'antd';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import AddSubjectConfigModal, { OperationType } from '../components/AddSubjectConfigModal';
import styles from './index.less';

export type CodeConfigData = {
id: number;
code_repo_name: string;
code_repo_vis: number;
git_url: string;
git_branch: string;
git_user_name: string;
git_password: string;
ssh_key: string;
verify_mode: number;
create_by: string;
create_time: string;
update_by: string;
update_time: string;
};
export type EditorData = {
id: number;
name: string;
updateTime: string;
conceptsCount: string;
update_by: string;
relationsCount: string;
description: string;
};
function DatasetAnnotation() {
const [dataList, setDataList] = useState<CodeConfigData[] | undefined>(undefined);
const [tableData, setTableData] = useState<EditorData[]>([]);
const [cacheState, setCacheState] = useCacheState();
const [activeTab, setActiveTab] = useState<string>('1');

const navigate = useNavigate();
const [inputText, setInputText] = useState<string | undefined>(undefined);
const [pagination, setPagination] = useState<TablePaginationConfig>({
current: 1,
pageSize: 10,
});
const { message } = App.useApp();
const handleTableChange: TableProps<EditorData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
};
const [total, setTotal] = useState(0);
useEffect(() => {
getList();
}, []);
const items = [
{
key: 1,
label: `图谱版本`,
icon: <KFIcon type="icon-moxingjianjie" />,
children: null,
},
{
key: 2,
label: `更新日志`,
icon: <KFIcon type="icon-moxingwenjian" />,
children: null,
},
];
const createCodeConfig = () => {
const { close } = openAntdModal(AddSubjectConfigModal, {
opType: OperationType.Create,
onOk: () => {
getList();
close();
},
});
};
const getList = () => {
const params: Record<string, any> = {
pageNum: pagination.current,
pageSize: pagination.pageSize,
name: inputText,
};
getKnowledgeSubject(params).then((ret) => {
console.log(ret.rows);

setTableData(ret.rows);
});
};
// 处理删除
const goSubjectEdit = (row) => {
console.log(row);

navigate(`/knowledge/subjectView/${row.name}/${row.id}`);
};

const handleSubjectDelete = (row) => {
modalConfirm({
title: '确定要删除该主体吗?',
content: '删除后主体将不可恢复,是否确认删除?',
onOk: () => {
deleteSubject(row.id).then((res) => {
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
...prev,
current: 1,
}));
} else {
getList();
}
}
});
},
});
};
const columnsNow: TableProps<EditorData>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: '20%',
render: tableCellRender(false, TableCellValueType.Index, {
page: pagination.current! - 1,
pageSize: pagination.pageSize!,
}),
},
{
title: '主体名称',
dataIndex: 'name',
key: 'name',
width: '20%',
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
width: '20%',
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '概念个数',
dataIndex: 'conceptsCount',
key: 'conceptsCount',
width: '20%',
render: tableCellRender(),
},
{
title: '关系个数',
dataIndex: 'relationsCount',
key: 'relationsCount',
width: '20%',
render: tableCellRender(),
},

{
title: '主体描述',
dataIndex: 'description',
key: 'description',
width: '30%',
render: tableCellRender(),
},
{
title: '操作',
dataIndex: 'operation',
width: 300,
key: 'operation',
render: (_: any, record: EditorData) => (
<div>
<Button
type="link"
size="small"
key="edit"
onClick={goSubjectEdit}
icon={<KFIcon type="icon-chakanxiangqing" />}
>
查看
</Button>
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-bianji" />}
onClick={() => goSubjectEdit(record)}
>
编辑
</Button>
<Button type="link" size="small" key="run" icon={<KFIcon type="icon-bendishangchuan" />}>
导出
</Button>
<Button
type="link"
size="small"
key="remove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleSubjectDelete(record)}
>
删除
</Button>
</div>
),
},
];
return (
<div className={styles['mySubject-page']}>
<div className={styles['mySubject-page__container']}>
主体管理
<div className={styles['mySubject-page__container__box']}>
<img
className={styles['mySubject-page__container__box__img']}
src={require('@/assets/img/icon-graph.png')}
draggable={false}
alt=""
/>
<span>
您还可以创建<span style={{ fontSize: '16px', color: '#1664ff' }}>47</span>个框架{' '}
</span>
</div>
</div>
<div className={styles['mySubject-page__topCenter']}>
<div className={styles['mySubject-page__bottomBox']}>
<div className={styles['mySubject-page__bottomBox__queryparams']}>
<div className={styles['mySubject-page__bottomBox__queryparams__leftbox']}>
<Input.Search
placeholder="按名称筛选"
allowClear
onSearch={getList}
onChange={(e) => setInputText(e.target.value)}
value={inputText}
/>
{/* <Button
type="primary"
key="primary"
style={{ margin: '0 20px' }}
onClick={() => getList()}
>
查询
</Button> */}
<Button
style={{ marginLeft: '20px' }}
key="info"
onClick={() => handleSubjectDelete()}
>
批量删除
</Button>
</div>
<div>
<Button
onClick={() => createCodeConfig()}
// onClick={() => toDetail(record)}
>
创建主体
</Button>
<Button key="info" style={{ margin: '0 20px' }}>
obs导入
</Button>
</div>
</div>

<Table
dataSource={tableData}
columns={columnsNow}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default DatasetAnnotation;

+ 206
- 0
react-ui/src/pages/knowledge/OnlineGraph/index.less View File

@@ -0,0 +1,206 @@
.onlineGraph-page {
height: 100%;
padding: 0 9px;
&__container {
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
padding:0 30px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
color:#1d1d20;
font-size:16px;
&__box{
display: flex;
align-items: center;
color:#1d1d20;
font-size:15px;
&__update{
padding: 7px 10px;
background:rgba(22, 100, 255, 0.06);
border:1px solid rgba(22, 100, 255, 0.11);
border-radius:4px;
margin-right: 20px;
cursor: pointer;
}
}
}
&__topCenter{
margin: 10px 0 15px 0;
width: 100%;
background:#ffffff;
border-radius:10px;
box-shadow:0px 2px 12px rgba(180, 182, 191, 0.09);
}
&__title{
color:#1d1d20;
font-size:16px;
margin: 30px 0 15px 25px;
font-weight: 600;
}
&__listBox{
padding: 0 25px;
display: flex;
align-items: center;
&__itemBox{
position: relative;
width: calc(20% - 15px);
height:100px;
background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%);
border-radius:4px;
box-shadow:0px 3px 10px rgba(164, 169, 181, 0.12);
margin-right: 15px;
&__img{
position: absolute;
left: 30px;
top: 26px;
width: 48px;
height: 48px;
}
&__wordTitle{
position: absolute;
top: 26px;
left: 102px;
font-weight:600;
color:#1d1d20;
font-size:18px;

}
&__wordContent{
position: absolute;
top: 54px;
left: 102px;
color:#565658;
font-size:15px;
}
}
}
&__centerListBox{
display: flex;
align-items: center;
padding: 20px 45px;
&__itemBox{
position: relative;
width: calc(20% - 20px);
height: 229px;
margin-right: 20px;
background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%);
border-radius:4px;
box-shadow:0px 3px 10px rgba(164, 169, 181, 0.13);
&__img{
width: 16px;
height: 16px;
position: absolute;
top: 24.5px;
left: 20px;
}
&__title{
position: absolute;
top: 20.1px;
left: 46px;
color:#1d1d20;
font-size:16px;
font-weight: 600;
}
&__content{
position: absolute;
width: calc(100% - 40px);
height: 114px;
top: 61px;
left: 20px;
background:rgba(22, 100, 255, 0.04);
border-radius:4px;
padding: 15px;
&__center{
position: absolute;
left: 50%;
top: 50%;
color:#1664ff;
font-size:14px;
transform: translate(-50%,-50%);
cursor: pointer;

}
&__status{
color:#565658;
font-size:14px;
margin-right: 15px;
// width: 38px;
}
}
&__button{
position: absolute;
color:#1664ff;
font-size:13px;
bottom: 20px;
cursor: pointer;
}
}
}
&__bottomBox{
padding: 23px 35px 23px 25px;
&__titleBox{
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 39px;
&__left{
display: flex;
align-items: center;
font-weight:600;
color:#1d1d20;
font-size:16px;
&__word{
display: flex;
align-items: center;
width:206px;
height:27px;
margin-left: 14px;
background:rgba(22, 100, 255, 0.09);
border-radius:4px;
padding: 2px 20px 2px 16px;
color:#4c5360;
font-size:14px;
font-weight: normal;
&__circle{
position: relative;
width:15px;
height:15px;
border-radius: 50%;
background:#ffffff;
box-shadow:0px 1px 3px rgba(0, 82, 217, 0.09);
margin-right: 13px;
&__inner{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
width:9px;
height:9px;
border-radius: 50%;
background:#1664ff;
box-shadow:0px 1px 3px rgba(22, 100, 255, 0.27);
}
}
}
}
&__right{
display: flex;
align-items: center;
// width:647px;
height:39px;
background:rgba(138, 138, 138, 0.05);
border-radius:4px;
padding-left: 30px;
}
}
&__queryparams{
margin: 17px 0;
}
}
}

+ 632
- 0
react-ui/src/pages/knowledge/OnlineGraph/index.tsx View File

@@ -0,0 +1,632 @@
import KFIcon from '@/components/KFIcon';
import { useCacheState } from '@/hooks/pageCacheState';
import {
deleteKnowledgeGraphVersion,
fullUpdate,
getKnowledgeGraphById,
getKnowledgeGraphVersionList,
incrementalUpdate,
rollback,
} from '@/services/graph';
import { formatDate } from '@/utils/date';
import { openAntdModal } from '@/utils/modal';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useSearchParams } from '@umijs/max';
import {
App,
Button,
Input,
Select,
Table,
type TablePaginationConfig,
type TableProps,
Tabs,
} from 'antd';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import AddCodeConfigModal, { OperationType } from '../components/AddUpdateModal';
// import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal';
import styles from './index.less';
export type CodeConfigData = {
id: number;
code_repo_name: string;
code_repo_vis: number;
git_url: string;
git_branch: string;
git_user_name: string;
git_password: string;
ssh_key: string;
verify_mode: number;
create_by: string;
create_time: string;
update_by: string;
update_time: string;
};
export type topData = {
id: number; //主键
name: string; //名称
status: number; //状态,0代表不可用,1代表可用
spec: string; //图谱规格
createTime: string; //创建时间
updateTime: string; //最后更新时间
relationCount: number; //关系数
entityCount: number; //实体数
};
export type EditorData = {
id: number;
name: string;
status: string;
computing_resource: string;
update_by: string;
create_time: string;
url: string;
};
function DatasetAnnotation() {
const [dataList, setDataList] = useState<CodeConfigData[] | undefined>(undefined);
const [tableData, setTableData] = useState<CodeConfigData[] | undefined>(undefined);
const [topData, setTopData] = useState<topData>([]);
const [cacheState, setCacheState] = useCacheState();
const [searchParams] = useSearchParams();
const [activeTab, setActiveTab] = useState<string>('1');
const navigate = useNavigate();
const { message } = App.useApp();
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
pageSize: 10,
},
);
const updateMethodList = ['待更新', '增量更新', '全量更新'];
const stateList = ['离线', '在线'];
const graphId = searchParams.get('id') || '';
const graphVersion = searchParams.get('version') || '';
const handleTableChange: TableProps<EditorData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
};
const clickRouter = (val: string) => {
navigate(`/knowledge/graphCounter?tabActive=${val}`);
};
const getList = () => {
const params = {
id: graphId,
version: graphVersion,
};
getKnowledgeGraphById(params).then((ret) => {
console.log(ret);
setTopData(ret.data);
});
};

const getTableList = () => {
const params = {
id: graphId,
};
getKnowledgeGraphVersionList(params).then((ret) => {
console.log(ret);
setTableData(ret.rows);
setTotal(ret.total);
});
};
const incrementalUpdates = (row) => {
incrementalUpdate({
id: row.id,
}).then((ret) => {
if (ret.code == 200) {
getTableList();
}
});
};
const fullUpdates = (row) => {
fullUpdate({
id: row.id,
}).then((ret) => {
if (ret.code == 200) {
getTableList();
}
});
};

const singleRollback = (row) => {
rollback({
id: row.id,
}).then((ret) => {
if (ret.code == 200) {
getTableList();
}
});
};
const [total, setTotal] = useState(0);
useEffect(() => {
getList();
getTableList();
}, []);
const items = [
{
key: '1',
label: `图谱版本`,
icon: <KFIcon type="icon-moxingjianjie" />,
children: null,
},
// {
// key: 2,
// label: `更新日志`,
// icon: <KFIcon type="icon-moxingwenjian" />,
// children: null,
// },
];
const addCodeConfig = () => {
const { close } = openAntdModal(AddCodeConfigModal, {
opType: OperationType.Create,
codeConfigData: {
id: searchParams.get('id') || '',
},
onOk: () => {
getTableList();
close();
},
});
};
const handleVersionDelete = (row) => {
modalConfirm({
title: '确定要删除该版本吗?',
content: '删除后版本将不可恢复,是否确认删除?',
onOk: () => {
deleteKnowledgeGraphVersion(row.id).then((res) => {
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
...prev,
current: 1,
}));
getTableList();
} else {
getTableList();
}
}
});
},
});
};
const columnsNow: TableProps<EditorData>['columns'] = [
{
title: '版本名称',
dataIndex: 'name',
key: 'name',
width: '30%',
render: tableCellRender(),
},
{
title: '在线状态',
dataIndex: 'status',
key: 'status',
width: '10%',

render: (_: any, record: EditorData) => <div>{stateList[record.status]}</div>,
},
{
title: '更新方式',
dataIndex: 'updateMethod',
key: 'updateMethod',
width: '10%',
render: (_: any, record: EditorData) => <div>{updateMethodList[record.updateMethod]}</div>,
},
{
title: '实体个数',
dataIndex: 'entityCount',
key: 'entityCount',
width: '20%',
render: tableCellRender(),
},
{
title: '关系个数',
dataIndex: 'relationCount',
key: 'relationCount',
width: '20%',
render: tableCellRender(),
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: '20%',
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '描述',
dataIndex: 'remark',
key: 'remark',
width: '20%',
render: tableCellRender(),
},
{
title: '操作',
dataIndex: 'operation',
width: '30%',
key: 'operation',
render: (_: any, record: EditorData) => (
<div>
<Button
type="link"
size="small"
key="jingxiang"
disabled={record.updateMethod != 0}
icon={<KFIcon type="icon-chakanxiangqing" />}
onClick={() => incrementalUpdates(record)}
>
增量更新
</Button>
<Button
type="link"
size="small"
key="jingxiang"
disabled={record.updateMethod != 0}
icon={<KFIcon type="icon-chakanxiangqing" />}
onClick={() => fullUpdates(record)}
>
全量更新
</Button>
<Button
type="link"
size="small"
key="jingxiang"
disabled={!(record.status == 0 && record.updateMethod != 0)}
icon={<KFIcon type="icon-bianji" />}
onClick={() => singleRollback(record)}
>
回退
</Button>
<Button
type="link"
size="small"
key="remove"
disabled={record.status !== 0}
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleVersionDelete(record)}
>
删除
</Button>
{/* <ConfigProvider
theme={{
token: {
colorLink: themes['warningColor'],
},
}}
>
<Button
type="link"
size="small"
key="remove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleEditorDelete(record)}
>
删除
</Button>
</ConfigProvider> */}
</div>
),
},
];

return (
<div className={styles['onlineGraph-page']}>
<div className={styles['onlineGraph-page__container']}>
<span>{topData.name}</span>
<div className={styles['onlineGraph-page__container__box']}>
<div
onClick={addCodeConfig}
className={styles['onlineGraph-page__container__box__update']}
>
导入
</div>
{/* <div
onClick={allCodeConfig}
className={styles['onlineGraph-page__container__box__update']}
>
全量更新
</div> */}
</div>
</div>
<div className={styles['onlineGraph-page__topCenter']}>
<div className={styles['onlineGraph-page__title']}>在线版本图谱</div>
<div className={styles['onlineGraph-page__listBox']}>
{/* {dataList && dataList.length !== 0 && ( */}
<>
<div className={styles['onlineGraph-page__listBox__itemBox']}>
<img
className={styles['onlineGraph-page__listBox__itemBox__img']}
src={require('@/assets/img/runningstatus.png')}
draggable={false}
alt=""
/>
<div className={styles['onlineGraph-page__listBox__itemBox__wordTitle']}>
{topData.status == '1' ? '可用' : '不可用'}
</div>
<div className={styles['onlineGraph-page__listBox__itemBox__wordContent']}>
运行状态
</div>
</div>
<div className={styles['onlineGraph-page__listBox__itemBox']}>
<img
className={styles['onlineGraph-page__listBox__itemBox__img']}
src={require('@/assets/img/runningstatus.png')}
draggable={false}
alt=""
/>
<div className={styles['onlineGraph-page__listBox__itemBox__wordTitle']}>
{topData.entityCount}
</div>
<div className={styles['onlineGraph-page__listBox__itemBox__wordContent']}>
实体个数(个)
</div>
</div>
<div className={styles['onlineGraph-page__listBox__itemBox']}>
<img
className={styles['onlineGraph-page__listBox__itemBox__img']}
src={require('@/assets/img/runningstatus.png')}
draggable={false}
alt=""
/>
<div className={styles['onlineGraph-page__listBox__itemBox__wordTitle']}>
{topData.relationCount}
</div>
<div className={styles['onlineGraph-page__listBox__itemBox__wordContent']}>
关系个数(个)
</div>
</div>
<div className={styles['onlineGraph-page__listBox__itemBox']}>
<img
className={styles['onlineGraph-page__listBox__itemBox__img']}
src={require('@/assets/img/runningstatus.png')}
draggable={false}
alt=""
/>
<div className={styles['onlineGraph-page__listBox__itemBox__wordTitle']}>
{formatDate(topData.updateTime, 'YYYY-MM-DD')}
</div>
<div className={styles['onlineGraph-page__listBox__itemBox__wordContent']}>
最后更新时间
</div>
</div>
<div className={styles['onlineGraph-page__listBox__itemBox']}>
<img
className={styles['onlineGraph-page__listBox__itemBox__img']}
src={require('@/assets/img/runningstatus.png')}
draggable={false}
alt=""
/>
<div className={styles['onlineGraph-page__listBox__itemBox__wordTitle']}>
{formatDate(topData.createTime, 'YYYY-MM-DD')}
</div>
<div className={styles['onlineGraph-page__listBox__itemBox__wordContent']}>
创建时间
</div>
</div>
</>
{/* )} */}
</div>

<div className={styles['onlineGraph-page__title']}>在线版本图谱</div>
<div className={styles['onlineGraph-page__centerListBox']}>
<div className={styles['onlineGraph-page__centerListBox__itemBox']}>
<img
className={styles['onlineGraph-page__centerListBox__itemBox__img']}
src={require('@/assets/img/graph-star.png')}
draggable={false}
alt=""
/>
<div className={styles['onlineGraph-page__centerListBox__itemBox__title']}>
知识图谱预览查询
</div>
<div className={styles['onlineGraph-page__centerListBox__itemBox__content']}>
<div className={styles['onlineGraph-page__centerListBox__itemBox__content__center']}>
预览图谱
</div>
</div>
</div>
<div className={styles['onlineGraph-page__centerListBox__itemBox']}>
<img
className={styles['onlineGraph-page__centerListBox__itemBox__img']}
src={require('@/assets/img/graph-star.png')}
draggable={false}
alt=""
/>
<div className={styles['onlineGraph-page__centerListBox__itemBox__title']}>
知识图谱问答KBQA
</div>
<div className={styles['onlineGraph-page__centerListBox__itemBox__content']}>
<div style={{ marginBottom: '12px' }}>
<span
className={styles['onlineGraph-page__centerListBox__itemBox__content__status']}
>
状态
</span>
<span style={{ color: '#6ac21d', fontSize: '14px' }}>已开通</span>
</div>
<div style={{ display: 'flex', alignItems: 'start' }}>
<div
className={styles['onlineGraph-page__centerListBox__itemBox__content__status']}
>
状态
</div>
<div style={{ color: '#1d1d20', fontSize: '14px', flex: 1 }}>
利用NLP能力理解用户输入的不同类型问题、找到并返回知识图谱中相关的答案。
</div>
</div>
</div>
<div
onClick={() => {
clickRouter('3');
}}
className={styles['onlineGraph-page__centerListBox__itemBox__button']}
style={{ right: '20px' }}
>
接口信息
</div>
<div
onClick={() => {
clickRouter('2');
}}
className={styles['onlineGraph-page__centerListBox__itemBox__button']}
style={{ right: '101px' }}
>
问答体验
</div>
<div
onClick={() => {
clickRouter('1');
}}
className={styles['onlineGraph-page__centerListBox__itemBox__button']}
style={{ right: '182px' }}
>
问答配置
</div>
</div>
<div className={styles['onlineGraph-page__centerListBox__itemBox']}>
<img
className={styles['onlineGraph-page__centerListBox__itemBox__img']}
src={require('@/assets/img/graph-star.png')}
draggable={false}
alt=""
/>
<div className={styles['onlineGraph-page__centerListBox__itemBox__title']}>
实体链接
</div>
<div className={styles['onlineGraph-page__centerListBox__itemBox__content']}>
<div style={{ marginBottom: '12px' }}>
<span
className={styles['onlineGraph-page__centerListBox__itemBox__content__status']}
>
状态
</span>
<span style={{ color: '#6ac21d', fontSize: '14px' }}>已开通</span>
</div>
<div style={{ display: 'flex', alignItems: 'start' }}>
<div
className={styles['onlineGraph-page__centerListBox__itemBox__content__status']}
>
状态
</div>
<span style={{ color: '#1d1d20', fontSize: '14px', flex: 1 }}>
识别句子中出现的知识图谱中的实体,并返回实体相关信息。
</span>
</div>
</div>
<div
className={styles['onlineGraph-page__centerListBox__itemBox__button']}
style={{ right: '20px' }}
>
接口信息
</div>
</div>
</div>
<div className={styles['onlineGraph-page__bottomBox']}>
<div className={styles['onlineGraph-page__bottomBox__titleBox']}>
<div className={styles['onlineGraph-page__bottomBox__titleBox__left']}>
<span>版本统计</span>
<div className={styles['onlineGraph-page__bottomBox__titleBox__left__word']}>
<div
className={styles['onlineGraph-page__bottomBox__titleBox__left__word__circle']}
>
<div
className={
styles['onlineGraph-page__bottomBox__titleBox__left__word__circle__inner']
}
></div>
</div>
您还可以创建<span style={{ color: '#1664ff', fontSize: '16px' }}>1</span>个版本{' '}
</div>
</div>
<div className={styles['onlineGraph-page__bottomBox__titleBox__right']}>
<div>
<span style={{ color: '#565658', fontSize: '14px', marginRight: '10px' }}>
已上线
</span>
<span style={{ color: '#1d1d20', fontSize: '15px', marginRight: '61px' }}>1</span>
</div>
<div>
<span style={{ color: '#565658', fontSize: '14px', marginRight: '10px' }}>
未上线
</span>
<span style={{ color: '#1d1d20', fontSize: '15px', marginRight: '61px' }}>1</span>
</div>
<div>
<span style={{ color: '#565658', fontSize: '14px', marginRight: '10px' }}>
增量更新
</span>
<span style={{ color: '#1d1d20', fontSize: '15px', marginRight: '61px' }}>1</span>
</div>
<div>
<span style={{ color: '#565658', fontSize: '14px', marginRight: '10px' }}>
全量更新
</span>
<span style={{ color: '#1d1d20', fontSize: '15px', marginRight: '61px' }}>1</span>
</div>
<div>
<span style={{ color: '#565658', fontSize: '14px', marginRight: '10px' }}>
发布次数
</span>
<span style={{ color: '#1d1d20', fontSize: '15px', marginRight: '20px' }}>1</span>
</div>
</div>
</div>
<Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
<div className={styles['onlineGraph-page__bottomBox__queryparams']}>
<Input.Search
placeholder="按镜像名称筛选"
allowClear
// onSearch={onSearch}
// onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
// value={inputText}
/>
<Select
allowClear
style={{ width: '106px', margin: '0px 30px 0 20px' }}
placeholder="请选择数据集分类"
options={[]}
fieldNames={{ label: 'name', value: 'name' }}
optionFilterProp="name"
showSearch
/>
<Button
key="primary"
type="primary"
// onClick={() => toDetail(record)}
>
查询
</Button>
</div>

<Table
dataSource={tableData}
columns={columnsNow}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default DatasetAnnotation;

+ 976
- 0
react-ui/src/pages/knowledge/SubjectView/index.jsx View File

@@ -0,0 +1,976 @@
import colorB from '@/assets/img/tupu/b-circle.png';
import colorC from '@/assets/img/tupu/c-circle.png';
import { useStateRef, useVisible } from '@/hooks';
import { editKnowledgeSubject, getKnowledgeSubjectById } from '@/services/subject';
import { fittingString, parseJsonText, s8 } from '@/utils';
import { openAntdModal } from '@/utils/modal';
import G6 from '@antv/g6';
import { useNavigate, useParams } from '@umijs/max';
import { App, Button } from 'antd';
import { useEffect, useRef, useState } from 'react';
import GlobalParamsDrawer from '../components/GlobalParamsDrawer';
import AddSubjectConfigModal, { OperationType } from '../components/subjecDetailEdit';
import PropsEdge from '../components/SubjectEdgeDrawer';
import Props from '../components/SubjectNodeDrawer';
import styles from './index.less';
import { findAllParentNodes } from './utils';
let graph = null;

const EditPipeline = () => {
const navigate = useNavigate();
const locationParams = useParams(); //新版本获取路由参数接口
console.log(locationParams);
const graphRef = useRef();
const paramsDrawerRef = useRef();
const edgeDrawerRef = useRef();
const propsRef = useRef();
const propsEdgeRef = useRef();
const [paramsDrawerOpen, openParamsDrawer, closeParamsDrawer] = useVisible(false);
const [edgeDrawerOpen, edgeParamsDrawer, closeEdgeDrawer] = useVisible(false);
const [globalParam, setGlobalParam, globalParamRef] = useStateRef([]);
const { message } = App.useApp();
const [contextFlag, setContextFlag] = useState(false);
const [subjectName, setSubjectName] = useState('');
const colorList = [colorB, colorC];
const [subjectDesrc, setSubjectDesrc] = useState('');
let sourceAnchorIdx, targetAnchorIdx, dropAnchorIdx;
let dragSourceNode;

useEffect(() => {
initGraph();
getFirstWorkflow(locationParams.id);
console.log(locationParams);
const changeSize = () => {
if (!graph || graph.get('destroyed')) return;
if (!graphRef.current) return;
graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
graph.fitView();
};

window.addEventListener('resize', changeSize);
return () => {
window.removeEventListener('resize', changeSize);
};
}, []);

// 拖拽结束,添加新节点
const onDragEnd = (val) => {
const { x, y } = val;
const point = graph.getPointByClient(x, y);
// 元模型
const model = {
...val,
x: point.x,
y: point.y,
id: val.component_name + '-' + s8(),
isCluster: false,
formError: true,
};
// console.log('model', model);
graph.addItem('node', model, false);
};
const updateCodeConfig = () => {
getKnowledgeSubjectById(locationParams.id).then((ret) => {
console.log(ret);
if (ret.code == 200) {
const { close } = openAntdModal(AddSubjectConfigModal, {
opType: OperationType.Update,
codeConfigData: {
name: ret.data.name,
description: ret.data.description,
id: ret.data.id,
},
onOk: () => {
// getList();
getKnowledgeSubjectById(locationParams.id).then((res) => {
getFirstWorkflow(locationParams.id);
close();
});
},
});
}
});
};
// 节点数据发生变化
const handleFormEdgeChange = (val) => {
console.log(val);
if (graph) {
const data = graph.save();
const index = data.edges.findIndex((item) => {
return item.id === val.id;
});
data.edges[index].label = val.label;
const zoom = graph.getZoom();
// 在拉取新数据重新渲染页面之前先获取点(0, 0)在画布上的位置
const lastPoint = graph.getCanvasByPoint(0, 0);
graph.changeData(data);
graph.render();
graph.zoomTo(zoom);
// 获取重新渲染之后点(0, 0)在画布的位置
const newPoint = graph.getCanvasByPoint(0, 0);
// 移动画布相对位移;
graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y);
}
};
const handleFormChange = (val) => {
console.log(val);
if (graph) {
const data = graph.save();
const index = data.nodes.findIndex((item) => {
return item.id === val.id;
});
data.nodes[index] = val;
const zoom = graph.getZoom();
// 在拉取新数据重新渲染页面之前先获取点(0, 0)在画布上的位置
const lastPoint = graph.getCanvasByPoint(0, 0);
graph.changeData(data);
graph.render();
graph.zoomTo(zoom);
// 获取重新渲染之后点(0, 0)在画布的位置
const newPoint = graph.getCanvasByPoint(0, 0);
// 移动画布相对位移;
graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y);
}
};
// 保存
const savePipeline = async (val) => {
// const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields());
// if (globalParamError) {
// message.error('全局参数配置有误');
// openParamsDrawer();
// return;
// }
// closeParamsDrawer();

// const [propsRes, propsError] = await to(propsRef.current.validateFields());
// if (propsError) {
// message.error('节点必填项必须配置');
// return;
// }
// propsRef.current.close();

// setTimeout(() => {
// const data = graph.save();
// // console.log(data);
// const errorNode = data.nodes.find((item) => item.formError === true);
// if (errorNode) {
// message.error(`【${errorNode.label}】节点必填项必须配置`);
// const graphNode = graph.findById(errorNode.id);
// if (graphNode) {
// openNodeDrawer(graphNode, true);
// }
// return;
// }
const data = graph.save();
console.log(data);
const params = {
id: locationParams.id / 1,
name: subjectName,
description: subjectDesrc,
dag: JSON.stringify(data),
// global_param: JSON.stringify(globalParamRes.global_param),
};
editKnowledgeSubject(params).then((ret) => {
message.success('保存成功');
// setTimeout(() => {
// if (val) {
// navigate({ pathname: `/pipeline/template` });
// }
// }, 500);
});
// }, 500);
};

// 渲染数据
const getGraphData = (data) => {
if (graph && data) {
graph.data(data);
graph.render();
} else {
setTimeout(() => {
getGraphData(data);
}, 500);
}
};

// 处理并行边,暂时没有用
const processParallelEdgesOnAnchorPoint = (
edges,
offsetDiff = 15,
multiEdgeType = 'cubic-vertical',
singleEdgeType = undefined,
loopEdgeType = undefined,
) => {
const len = edges.length;
const cod = offsetDiff * 2;
const loopPosition = [
'top',
'top-right',
'right',
'bottom-right',
'bottom',
'bottom-left',
'left',
'top-left',
];
const edgeMap = {};
const tags = [];
const reverses = {};
for (let i = 0; i < len; i++) {
const edge = edges[i];
const { source, target, sourceAnchor, targetAnchor } = edge;
const sourceTarget = `${source}|${sourceAnchor}-${target}|${targetAnchor}`;

if (tags[i]) continue;
if (!edgeMap[sourceTarget]) {
edgeMap[sourceTarget] = [];
}
tags[i] = true;
edgeMap[sourceTarget].push(edge);
for (let j = 0; j < len; j++) {
if (i === j) continue;
const sedge = edges[j];
const {
source: src,
target: dst,
sourceAnchor: srcAnchor,
targetAnchor: dstAnchor,
} = sedge;

// 两个节点之间共同的边
// 第一条的source = 第二条的target
// 第一条的target = 第二条的source
if (!tags[j]) {
if (
source === dst &&
sourceAnchor === dstAnchor &&
target === src &&
targetAnchor === srcAnchor
) {
edgeMap[sourceTarget].push(sedge);
tags[j] = true;
reverses[
`${src}|${srcAnchor}|${dst}|${dstAnchor}|${edgeMap[sourceTarget].length - 1}`
] = true;
} else if (
source === src &&
sourceAnchor === srcAnchor &&
target === dst &&
targetAnchor === dstAnchor
) {
edgeMap[sourceTarget].push(sedge);
tags[j] = true;
}
}
}
}

// eslint-disable-next-line
for (const key in edgeMap) {
const arcEdges = edgeMap[key];
const { length } = arcEdges;
for (let k = 0; k < length; k++) {
const current = arcEdges[k];
if (current.source === current.target) {
if (loopEdgeType) current.type = loopEdgeType;
// 超过8条自环边,则需要重新处理
current.loopCfg = {
position: loopPosition[k % 8],
dist: Math.floor(k / 8) * 20 + 50,
};
continue;
}
if (
length === 1 &&
singleEdgeType &&
(current.source !== current.target || current.sourceAnchor !== current.targetAnchor)
) {
current.type = singleEdgeType;
continue;
}
current.type = multiEdgeType;
const sign =
(k % 2 === 0 ? 1 : -1) *
(reverses[
`${current.source}|${current.sourceAnchor}|${current.target}|${current.targetAnchor}|${k}`
]
? -1
: 1);
if (length % 2 === 1) {
current.curveOffset = sign * Math.ceil(k / 2) * cod;
} else {
current.curveOffset = sign * (Math.floor(k / 2) * cod + offsetDiff);
}
}
}
return edges;
};
// 判断两个节点之间是否有边
const hasEdge = (source, target) => {
const neighbors = source.getNeighbors();
for (const node of neighbors) {
// 新建边的时候,获取的 neighbors 的数据有问题,不全是 INode 类型,可能没有 getID 方法
if (node.getID?.() === target.getID?.()) return true;
}
return false;
};

// 复制节点
const cloneElement = (item) => {
let data = graph.save();
const nodeId = s8();
data.nodes.push({
...item.getModel(),
label: item.getModel().label + '-copy',
x: item.getModel().x + 150,
y: item.getModel().y,
id: item.getModel().component_name + '-' + nodeId,
});
graph.changeData(data);
};
const addElement = () => {
let data = graph.save();
const nodeId = s8();
data.nodes.push({
...item.getModel(),
label: item.getModel().label,
x: item.getModel().x + 150,
y: item.getModel().y,
id: item.getModel().component_name + '-' + nodeId,
});
graph.changeData(data);
};
// 获取流水线详情
const getFirstWorkflow = async (val) => {
getKnowledgeSubjectById(val).then((res) => {
if (res && res.data) {
setSubjectName(res.data.name);
setSubjectDesrc(res.data.description);
const { dag } = res.data;
if (dag) {
getGraphData(parseJsonText(dag));
}
}
});
// if (res && res.data) {
// const { global_param, dag } = res.data;
// setGlobalParam(global_param || []);
// if (dag) {
// getGraphData(parseJsonText(dag));
// }
// }
};

// 打开节点抽屉
const openNodeDrawer = (node, validate = false) => {
console.log(node);
// 获取所有的上游节点
const parentNodes = findAllParentNodes(graph, node);
// 如果没有打开过全局参数抽屉,获取不到全局参数
const globalParams =
paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
// 打开节点编辑抽屉
propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate);
};
// 打开边抽屉
const openEdgeDrawer = (edge, validate = false) => {
// // 获取所有的上游节点
// const parentNodes = findAllParentNodes(graph, node);
// // 如果没有打开过全局参数抽屉,获取不到全局参数
// const globalParams =
// edgeDrawerRef.current.getFieldsValue().global_param || globalParamRef.current;
// 打开节点编辑抽屉
console.log(edge);
propsEdgeRef.current.showDrawer(edge.getModel(), validate);
};
// 初始化图
const initGraph = () => {
// const contextMenu = initMenu();
G6.registerNode(
'rect-node',

{
// draw anchor-point circles according to the anchorPoints in afterDraw
getAnchorPoints(cfg) {
return (
cfg.anchorPoints || [
// 四个,上下左右
[0.5, 0.4],
// [0.5, 1],
// [0, 0.5],
// [1, 0.5],
]
);
},

afterDraw(cfg, group) {
group.addShape('image', {
name: 'custom-image',
attrs: {
x: -20,
y: -20,
width: 40,
height: 40,
img: cfg.color ? colorList[cfg.color - 1] : colorC,
cursor: 'pointer',
stroke: '#fff',
fill: 'transparent',
},
draggable: true,
});
if (cfg.label) {
group.addShape('text', {
attrs: {
text: fittingString(cfg.label, 70, 10),
x: 0,
y: 30,
fontSize: 10,
textAlign: 'center',
textBaseline: 'middle',
fill: '#000',
cursor: 'pointer',
},
name: 'text-shape',
draggable: true,
});
}

// if (cfg.formError) {
// group.addShape('image', {
// attrs: {
// x: 43,
// y: -24,
// width: 18,
// height: 18,
// img: require('@/assets/img/pipeline-warning.png'),
// cursor: 'pointer',
// },
// draggable: false,
// capture: false,
// });
// }
const bbox = group.getBBox();
// if (cfg.formError) {
// bbox.y += 6;
// bbox.width -= 6;
// bbox.height -= 6;
// }
const anchorPoints = this.getAnchorPoints(cfg);
anchorPoints.forEach((anchorPos, i) => {
group.addShape('circle', {
attrs: {
r: 3,
x: bbox.x + bbox.width * anchorPos[0],
y: bbox.y + bbox.height * anchorPos[1],
fill: '#fff',
stroke: '#a4a4a5',
cursor: 'crosshair',
lineWidth: 1,
},
name: `anchor-point`, // the name, for searching by group.find(ele => ele.get('name') === 'anchor-point')
anchorPointIdx: i, // flag the idx of the anchor-point circle
links: 0, // cache the number of edges connected to this shape
visible: false, // invisible by default, shows up when links > 1 or the node is in showAnchors state
draggable: true,
});
});
},

// response the state changes and show/hide the link-point circles
setState(name, value, item) {
const group = item.getContainer();
const shape = group.get('children')[0];
shape.attr('stroke', 'transparent');
const anchorPoints = group.findAll((item) => item.get('name') === 'anchor-point');
if (name === 'hover') {
if (value) {
// shape.attr('stroke', themes['primaryColor']);
anchorPoints.forEach((point) => {
point.show();
});
} else {
shape.attr('stroke', 'transparent');
anchorPoints.forEach((point) => {
point.hide();
});
}
} else if (name === 'drag') {
if (sourceAnchorIdx !== null && sourceAnchorIdx !== undefined) {
const anchorPoint = anchorPoints[sourceAnchorIdx];
// anchorPoint.attr('stroke', value ? themes['primaryColor'] : '#a4a4a5');
// anchorPoint.attr('lineWidth', value ? 2 : 1);
}
} else if (name === 'drop') {
if (dropAnchorIdx !== null && dropAnchorIdx !== undefined) {
const anchorPoint = anchorPoints[dropAnchorIdx];
// anchorPoint.attr('stroke', value ? themes['primaryColor'] : '#a4a4a5');
// anchorPoint.attr('lineWidth', value ? 2 : 1);
}
}
},
},
'rect',
);

graph = new G6.Graph({
container: graphRef.current,
width: graphRef.current.clientWidth || 500,
height: graphRef.current.clientHeight || '100%',
animate: false,
groupByTypes: true,
fitView: true,
// plugins: [contextMenu],
enabledStack: false,
fitView: true,
minZoom: 0.5,
maxZoom: 5,
fitViewPadding: 200,
// behaviors: [
// {
// type: 'create-edge',
// trigger: 'click',
// style: {
// fill: 'red',
// lineWidth: 2,
// },
// },
// ],
modes: {
default: [
// config the shouldBegin for drag-node to avoid node moving while dragging on the anchor-point circles
{
type: 'drag-node',
shouldBegin: (e) => {
if (e.target.get('name') === 'anchor-point') return false;
return true;
},
},
// config the shouldBegin and shouldEnd to make sure the create-edge is began and ended at anchor-point circles
{
type: 'create-edge',
trigger: 'drag',
shouldBegin: (e) => {
// avoid beginning at other shapes on the node
if (e.target && e.target.get('name') !== 'anchor-point') return false;
sourceAnchorIdx = e.target.get('anchorPointIdx');
e.target.set('links', e.target.get('links') + 1); // cache the number of edge connected to this anchor-point circle
dragSourceNode = e.item;
return true;
},
shouldEnd: (e) => {
// avoid ending at other shapes on the node
// if (e.target && e.target.get('name') !== 'anchor-point') return false;
// if (!dragSourceNode || !e.item) return false;
// 不允许连接自己
// if (dragSourceNode.getID() === e.item.getID()) return false;
// 两个节点不允许多条边
if (hasEdge(dragSourceNode, e.item)) return false;
// if (e.target) {
// targetAnchorIdx = e.target.get('anchorPointIdx');
// e.target.set('links', e.target.get('links') + 1); // cache the number of edge connected to this anchor-point circle
// return true;
// }
// targetAnchorIdx = undefined;
return true;
},
},
'drag-canvas',
'zoom-canvas',
],
},
defaultNode: {
type: 'rect-node',
size: [40, 40],
labelCfg: {
style: {
fill: 'transparent',
fontSize: 0,
},
},
style: {
fill: 'transparent',
lineWidth: 0,
},
},
defaultEdge: {
// type: 'polyline',
type: 'cubic',
style: {
endArrow: {
// 设置终点箭头
path: G6.Arrow.triangle(3, 3, 3), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应)
d: 4.5,
fill: '#CDD0DC',
},
cursor: 'pointer',
lineWidth: 1,
lineAppendWidth: 4,
opacity: 1,
stroke: '#CDD0DC',
radius: 1,
},
labelCfg: {
autoRotate: true,
style: {
fontSize: 10,
fill: 'rgba(22, 100, 255, 0.5)',
},
},
},
linkCenter: true,
});

// 修改历史数据样式问题
graph.node((node) => {
return {
style: {
stroke: 'transparent',
radius: 8,
},
};
});

// 绑定事件
bindEvents();
};

// 绑定事件
const bindEvents = () => {
graph.on('node:click', (e) => {
if (e.target.get('name') !== 'anchor-point' && e.item) {
openNodeDrawer(e.item);
}
});
// 监听边的点击事件
graph.on('edge:click', (e) => {
if (e.target.get('name') !== 'anchor-point' && e.item) {
openEdgeDrawer(e.item);
}
});
graph.on('aftercreateedge', (e) => {
// update the sourceAnchor and targetAnchor for the newly added edge
graph.updateItem(e.edge, {
sourceAnchor: sourceAnchorIdx,
targetAnchor: targetAnchorIdx,
});
});
// 删除边时,修改 anchor-point 的 links 值
// graph.on('afterremoveitem', (e) => {
// if (e.item && e.item.source && e.item.target) {
// const { source, target, sourceAnchor, targetAnchor } = e.item;
// const sourceNode = graph.findById(source);
// const targetNode = graph.findById(target);
// if (sourceNode && !isNaN(sourceAnchor)) {
// const sourceAnchorShape = sourceNode
// .getContainer()
// .find(
// (ele) =>
// ele.get('name') === 'anchor-point' && ele.get('anchorPointIdx') === sourceAnchor,
// );
// sourceAnchorShape.set('links', sourceAnchorShape.get('links') - 1);
// }
// if (targetNode && !isNaN(targetAnchor)) {
// const targetAnchorShape = targetNode
// .getContainer()
// .find(
// (ele) =>
// ele.get('name') === 'anchor-point' && ele.get('anchorPointIdx') === targetAnchor,
// );
// // targetAnchorShape.set('links', targetAnchorShape.get('links') - 1);
// }
// }
// });
// after drag on the first node, the edge is created, update the sourceAnchor
graph.on('afteradditem', (e) => {
const sourceAnchor = e.item.getModel().sourceAnchor;
if (e.item && e.item.getType() === 'edge' && !sourceAnchor) {
graph.updateItem(e.item, {
sourceAnchor: sourceAnchorIdx,
});
}
});
graph.on('node:mouseenter', (e) => {
graph.setItemState(e.item, 'hover', true);
});
graph.on('node:mouseleave', (e) => {
graph.setItemState(e.item, 'hover', false);
});
graph.on('node:dragstart', (e) => {
graph.setItemState(e.item, 'hover', true);
graph.setItemState(e.item, 'drag', true);
});
graph.on('node:dragend', (e) => {
graph.setItemState(e.item, 'hover', false);
graph.setItemState(e.item, 'drag', false);
});
graph.on('node:dragenter', (e) => {
if (e.item?.getID() === dragSourceNode?.getID()) return;
graph.setItemState(e.item, 'hover', true);
if (e.target.get('name') === 'anchor-point') {
dropAnchorIdx = e.target.get('anchorPointIdx');
graph.setItemState(e.item, 'drop', true);
} else {
graph.setItemState(e.item, 'drop', false);
}
});
graph.on('node:dragleave', (e) => {
if (e.item?.getID() === dragSourceNode?.getID()) return;
graph.setItemState(e.item, 'hover', false);
graph.setItemState(e.item, 'drop', false);
dropAnchorIdx = undefined;
});
graph.on('node:drop', (e) => {
graph.setItemState(e.item, 'hover', false);
graph.setItemState(e.item, 'drop', false);
dropAnchorIdx = undefined;
});

graph.on('contextmenu', (e) => {
e.preventDefault();
console.log(e);
// 获取点击位置
const point = graph.getPointByClient(e.clientX, e.clientY);
const contextMenu = document.getElementById('contextMenu');

// 显示右键菜单
contextMenu.style.display = 'block';
contextMenu.style.left = `${e.clientX - 250}px`;
contextMenu.style.top = `${e.clientY}px`;
if (e.item) {
setContextFlag(true);
} else {
setContextFlag(false);
}
// 新增节点
document.getElementById('addNode').onclick = () => {
const newNode = {
id: `node-${Date.now()}`,
label: '材料',
x: point.x,
y: point.y,
properties: [
{
name: '',
isMultivalued: '',
type: '',
flag: false,
},
],
};
graph.addItem('node', newNode);
graph.refresh();
contextMenu.style.display = 'none';
};
document.getElementById('cloneNode').onclick = () => {
let data = graph.save();
const nodeId = s8();
data.nodes.push({
...e.item.getModel(),
label: e.item.getModel().label + '-copy',
x: e.item.getModel().x + 150,
y: e.item.getModel().y,
id: e.item.getModel().component_name + '-' + nodeId,
properties: e.item.getModel().properties || [
{
name: '',
isMultivalued: '',
type: '',
flag: false,
},
],
});
graph.changeData(data);
contextMenu.style.display = 'none';
};

document.getElementById('deleteNode').onclick = () => {
graph.removeItem(e.item);
contextMenu.style.display = 'none';
};

// 清空画布
// document.getElementById('clearCanvas').onclick = () => {
// graph.clear();
// contextMenu.style.display = 'none';
// };
// 点击其他地方隐藏菜单
document.addEventListener('click', () => {
contextMenu.style.display = 'none';
});
});

// 初始数据
const data = {
nodes: [
{ id: 'node1', x: 100, y: 100, label: '节点1' },
{ id: 'node2', x: 300, y: 100, label: '节点2' },
],
edges: [{ source: 'node1', target: 'node2' }],
};
};

// 上下文菜单
// const initMenu = () => {
// const contextMenu = new G6.Menu({
// className: 'pipeline-context-menu',
// getContent(evt) {
// const type = evt.item.getType();
// const cloneDisplay = type === 'node' ? 'flex' : 'none';
// return `
// <div>
// <div class="pipeline-context-menu__item" id="add">
// <svg class="pipeline-context-menu__item__icon" id="clone-svg">
// <use xlink:href="#icon-fuzhi1" />
// </svg>
// 新建
// </div>
// <div class="pipeline-context-menu__item" style="display: ${cloneDisplay}" id="clone">
// <svg class="pipeline-context-menu__item__icon" id="clone-svg">
// <use xlink:href="#icon-fuzhi1" />
// </svg>
// 复制
// </div>
// <div class="pipeline-context-menu__item" id="delete">
// <svg class="pipeline-context-menu__item__icon" id="delete-svg">
// <use xlink:href="#icon-shanchu1" />
// </svg>
// 删除
// </div>
// </div>`;
// },
// handleMenuClick: (target, item) => {
// const id = target.id;
// if (id.startsWith('add')) {
// addElement(item);
// }
// if (id.startsWith('clone')) {
// cloneElement(item);
// } else if (id.startsWith('delete')) {
// graph.removeItem(item);
// }
// },
// // offsetX and offsetY include the padding of the parent container
// // 需要加上父级容器的 padding-left 16 与自身偏移量 10
// offsetX: 16 + 10,
// // 需要加上父级容器的 padding-top 24 、画布兄弟元素高度、与自身偏移量 10
// offsetY: 0,
// // the types of items that allow the menu show up
// // 在哪些类型的元素上响应
// itemTypes: ['node', 'edge', 'canvas'],
// });

// return contextMenu;
// };

return (
<div className={styles['subjectView-container']}>
{/* <ModelMenu onComponentDragEnd={onDragEnd}></ModelMenu> */}
<div>{subjectName}</div>
<Button type="link" size="small" onClick={() => updateCodeConfig()}>
修改主体
</Button>
<div className={styles['subjectView-container__workflow']}>
<div className={styles['subjectView-container__workflow__top']}>
<Button type="link" size="small" onClick={() => savePipeline()}>
保存
</Button>
<span>图例:</span>
<div
className={styles['subjectView-container__workflow__top__circle']}
style={{ background: '#1664ff' }}
></div>
<span style={{ color: '#1664ff', fontSize: '15px', marginRight: '20px' }}>XXXX</span>
<div
className={styles['subjectView-container__workflow__top__circle']}
style={{ background: '#63a728' }}
></div>
<span style={{ color: '#63a728', fontSize: '15px' }}>XXXX</span>
{/* <Button
type="default"
icon={<KFIcon type="icon-quanjucanshu" />}
style={{ marginRight: '20px' }}
onClick={openParamsDrawer}
>
全局参数
</Button>
<Button
type="primary"
icon={<KFIcon type="icon-baocun" />}
style={{ marginRight: '20px' }}
onClick={() => {
savePipeline(false);
}}
>
保存
</Button>
<Button
type="primary"
style={{
border: '1px solid',
borderColor: '#1664ff',
background: '#fff',
color: '#1664ff',
}}
icon={<KFIcon type="icon-baocunbingfanhui" />}
onClick={() => {
savePipeline(true);
}}
>
保存并返回
</Button> */}
</div>
<div className={styles['subjectView-container__workflow__graph']} ref={graphRef}></div>
</div>
<Props ref={propsRef} onFormChange={handleFormChange}></Props>
<PropsEdge ref={propsEdgeRef} onFormChange={handleFormEdgeChange}></PropsEdge>
<GlobalParamsDrawer
ref={paramsDrawerRef}
open={paramsDrawerOpen}
globalParam={globalParam}
onClose={closeParamsDrawer}
></GlobalParamsDrawer>
{/* <EdgeParamsDrawer
ref={edgeDrawerRef}
open={edgeDrawerOpen}
globalParam={globalParam}
onClose={closeEdgeDrawer}
></EdgeParamsDrawer> */}
<div className={styles['contextMenu']} id="contextMenu">
<ul>
<li>
<Button
type="link"
size="small"
disabled={contextFlag}
id="addNode"
// onClick={() => createMirror(record.id)}
>
新增节点
</Button>
</li>
<li id="cloneNode">
<Button
type="link"
size="small"
disabled={!contextFlag}
// onClick={() => createMirror(record.id)}
>
复制节点
</Button>
</li>
<li id="deleteNode">
<Button
type="link"
disabled={!contextFlag}
size="small"
// onClick={() => createMirror(record.id)}
>
删除节点
</Button>
</li>
</ul>
</div>
</div>
);
};
export default EditPipeline;

+ 93
- 0
react-ui/src/pages/knowledge/SubjectView/index.less View File

@@ -0,0 +1,93 @@
.subjectView-container {
display: flex;
height: 100%;
background-color: #fff;
&__workflow {
flex: 1 1 0;
min-width: 0;
height: 100%;
&__top {
display: flex;
align-items: center;
justify-content: end;
width: 100%;
height: 52px;
padding: 0 20px;
background: #ffffff;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);
&__circle{
width:8px;
height:8px;
border-radius: 50%;
margin-right: 10px;
}
}

&__graph {
position: relative;
width: 100%;
height: calc(100% - 52px);
background-color: @background-color;
background-image: url(@/assets/img/pipeline-canvas-bg.png);
background-size: 100% 100%;
}
}
.contextMenu {
position: absolute;
background: #fff;
border: 1px solid #ccc;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
padding: 10px 5px;
display: none;
border-radius: 10px;
}
.contextMenu ul {
list-style: none;
margin: 0;
padding: 0;
}
.contextMenu ul li {
padding: 5px 5px;
cursor: pointer;
}
.contextMenu ul li:hover {
background: #f0f0f0;
}
}

:global {
.pipeline-context-menu {
width: 78px;
padding: 10px 0;
background: #ffffff;
border-radius: 6px;
box-shadow: 0px 0px 6px rgba(40, 84, 168, 0.21);

&__item {
display: flex;
align-items: center;
width: 100%;
height: 34px;
padding-left: 12px;
color: @text-color-secondary;
font-size: 15px;
cursor: pointer;

&:hover {
color: #0d5ef8;
font-weight: 500;
background-color: .addAlpha(#8895a8, 0.11) [];
}

&__icon {
width: 1em;
height: 1em;
margin-right: 9px;
fill: currentColor;
}
}
}
}

+ 94
- 0
react-ui/src/pages/knowledge/SubjectView/utils.tsx View File

@@ -0,0 +1,94 @@
import { PipelineGlobalParam, PipelineNodeModelParameter } from '@/types';
import { parseJsonText } from '@/utils';
import { Graph, INode } from '@antv/g6';
import { type MenuProps } from 'antd';

// 找到节点所以的上游节点
export const findAllParentNodes = (graph: Graph, node: INode) => {
const parentNodes: INode[] = [];
let index = -1;
let targetNode = node;
while (targetNode) {
const neighbors: INode[] = graph.getNeighbors(targetNode, 'source');
for (const sourceNode of neighbors) {
// 避免重复,也避免循环
const idx = parentNodes.findIndex((item) => sourceNode.getID() === item.getID());
if (idx === -1 && sourceNode.getID() !== node.getID()) {
parentNodes.push(sourceNode);
}
}
targetNode = parentNodes[++index];
}

return parentNodes;
};

// 判断并找到全局参数第一个重复项,有重复项时,全局参数不允许保存
export function findFirstDuplicate(params: PipelineGlobalParam[]): string | null {
const seen = new Set();
for (const item of params) {
if (seen.has(item.param_name)) {
return item.param_name;
}
seen.add(item.param_name);
}
return null;
}

// 创建参数下拉菜单
export function createMenuItems(
params: PipelineGlobalParam[],
parentNodes: INode[],
): MenuProps['items'] {
const nodes: MenuProps['items'] = parentNodes.map((item) => {
const model = item.getModel();
const out_parameters = model.out_parameters as string | undefined | null;
const out_parametersObj = parseJsonText(out_parameters);
const outParametersList = Object.keys(out_parametersObj ?? {});
return {
key: model.id as string,
label: model.label as string,
children: outParametersList.map((key: string) => ({
key: key as string,
label: out_parametersObj[key].label,
})),
};
});

if (params.length > 0) {
return [
{
key: 'global',
label: '全局参数',
children: params.map((item) => ({
key: item.param_name,
label: item.param_name,
})),
},
...nodes,
];
} else {
return [...nodes];
}
}

export function getInParameterComponent(
parameter: PipelineNodeModelParameter,
): React.ReactNode | null {
if (parameter.value) {
}

return null;
}

// 判断是否允许输入
export function canInput(parameter: PipelineNodeModelParameter) {
const { type, item_type } = parameter;
return !(
type === 'ref' &&
(item_type === 'dataset' ||
item_type === 'model' ||
item_type === 'image' ||
item_type === 'code')
);
}

+ 0
- 0
react-ui/src/pages/knowledge/components/AddCodeConfigModal/index.less View File


+ 228
- 0
react-ui/src/pages/knowledge/components/AddCodeConfigModal/index.tsx View File

@@ -0,0 +1,228 @@
import KFModal from '@/components/KFModal';
import { AvailableRange } from '@/enums';
import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { addKnowledgeGraph, editKnowledgeGraph, getEntityGraph } from '@/services/graph';
import { to } from '@/utils/promise';
import { Form, Input, Select, message, type ModalProps } from 'antd';
import { useEffect, useState } from 'react';

export enum VerifyMode {
Password = 0, // 用户名密码
SSH = 1, // SSH Key
}

export enum OperationType {
Create = 0, // 新建
Update = 1, // 更新
}

type FormData = Partial<CodeConfigData>;

interface AddCodeConfigModalProps extends Omit<ModalProps, 'onOk'> {
opType: OperationType;
codeConfigData?: CodeConfigData;
onOk: () => void;
}

function AddCodeConfigModal({ opType, codeConfigData, onOk, ...rest }: AddCodeConfigModalProps) {
const [form] = Form.useForm();
const [ontologyList, setOntologyList] = useState([]);
const isPublic = Form.useWatch('code_repo_vis', form) === AvailableRange.Public;
useEffect(() => {
getEntityGraph().then((ret) => {
setOntologyList(ret.data);
});
}, []);
// 创建
const createCodeConfig = async (formData: FormData) => {
console.log(OperationType);

const params: FormData & { id?: number } = {
...formData,
};
// 清除多余的信息
// if (formData.code_repo_vis === AvailableRange.Public) {
// omit(params, ['verify_mode', 'git_user_name', 'git_password', 'ssh_key']);
// }
// if (formData.verify_mode === VerifyMode.Password) {
// omit(params, ['ssh_key']);
// } else if (formData.verify_mode === VerifyMode.SSH) {
// omit(params, ['git_user_name', 'git_password']);
// }
if (opType === OperationType.Update) {
params.id = codeConfigData?.id;
}
const request = opType === OperationType.Create ? addKnowledgeGraph : editKnowledgeGraph;
const [res] = await to(request(params));
if (res) {
message.success(opType === OperationType.Create ? '创建成功' : '修改成功');
onOk?.();
}
};

// 提交
const onFinish = (formData: FormData) => {
createCodeConfig(formData);
};

// 设置初始值
const initialValues: FormData = codeConfigData ?? {
code_repo_vis: AvailableRange.Public,
verify_mode: VerifyMode.Password,
};
if (initialValues.verify_mode === undefined || initialValues.verify_mode === null) {
initialValues.verify_mode = VerifyMode.Password;
}

return (
<KFModal
{...rest}
title={opType === OperationType.Create ? '创建图谱' : '修改图谱'}
image={require('@/assets/img/create-experiment.png')}
width={825}
okButtonProps={{
htmlType: 'submit',
form: 'form',
}}
destroyOnClose
>
<Form
name="form"
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={initialValues}
autoComplete="off"
>
<Form.Item
label="图谱名称"
name="name"
required
rules={[
{
required: true,
message: '请输入图谱名称',
},
]}
>
<Input placeholder="请输入图谱名称" showCount allowClear maxLength={64} />
</Form.Item>
<Form.Item
label="关联实体"
name="ontologyId"
rules={[
{
required: false,
message: '请选择关联实体',
},
]}
>
<Select
allowClear
placeholder="请选择关联实体"
options={ontologyList}
fieldNames={{ label: 'name', value: 'id' }}
optionFilterProp="name"
showSearch
/>
</Form.Item>
{/* <Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues?.code_repo_vis !== currentValues?.code_repo_vis
}
>
{({ getFieldValue }) => {
return getFieldValue('code_repo_vis') === AvailableRange.Private ? (
<>
<Form.Item
label="验证方式"
name="verify_mode"
rules={[
{
required: true,
message: '请选择验证方式',
},
]}
>
<Radio.Group>
<Radio value={VerifyMode.Password}>用户名/密码</Radio>
<Radio value={VerifyMode.SSH}>SSH Key</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues?.verify_mode !== currentValues?.verify_mode
}
>
{({ getFieldValue }) => {
return getFieldValue('verify_mode') === VerifyMode.Password ? (
<>
<Form.Item
label="Git 用户名"
name="git_user_name"
rules={[
{
required: true,
message: '请输入 Git 用户名',
},
]}
>
<Input
placeholder="请输入 Git 用户名"
autoComplete="off"
showCount
allowClear
maxLength={64}
/>
</Form.Item>
<Form.Item
label="Git 密码"
name="git_password"
rules={[
{
required: true,
message: '请输入 Git 密码',
},
]}
>
<Input.Password
autoComplete="new-password"
placeholder="请输入 Git 密码"
allowClear
/>
</Form.Item>
</>
) : (
<Form.Item
label="SSH Key"
name="ssh_key"
rules={[
{
required: true,
message: '请输入 SSH Key',
},
]}
>
<Input.TextArea
placeholder="请输入 SSH Key"
showCount
maxLength={4096}
autoSize={{ minRows: 4, maxRows: 8 }}
allowClear
/>
</Form.Item>
);
}}
</Form.Item>
</>
) : null;
}}
</Form.Item> */}
</Form>
</KFModal>
);
}

export default AddCodeConfigModal;

+ 0
- 0
react-ui/src/pages/knowledge/components/AddSubjectConfigModal/index.less View File


+ 136
- 0
react-ui/src/pages/knowledge/components/AddSubjectConfigModal/index.tsx View File

@@ -0,0 +1,136 @@
import KFModal from '@/components/KFModal';
import { AvailableRange } from '@/enums';
import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { addKnowledgeSubject } from '@/services/subject';
import { to } from '@/utils/promise';
import { Form, Input, message, type FormRule, type ModalProps } from 'antd';
import { useMemo, useState } from 'react';

export enum VerifyMode {
Password = 0, // 用户名密码
SSH = 1, // SSH Key
}

export enum OperationType {
Create = 0, // 新建
Update = 1, // 更新
}

type FormData = Partial<CodeConfigData>;

interface AddCodeConfigModalProps extends Omit<ModalProps, 'onOk'> {
opType: OperationType;
codeConfigData?: CodeConfigData;
onOk: () => void;
}

function AddCodeConfigModal({ opType, codeConfigData, onOk, ...rest }: AddCodeConfigModalProps) {
const [form] = Form.useForm();
const [typeList, setTypeList] = useState([]);
const isPublic = Form.useWatch('code_repo_vis', form) === AvailableRange.Public;

const urlExample = useMemo(
() =>
isPublic
? 'https://gitlink.org.cn/ci4s/ci4sManagement-cloud.git'
: 'git@code.gitlink.org.cn:ci4s/ci4sManagement-cloud.git',
[isPublic],
);

// /^(git@[\w.-]+:[\w./-]+\.git)$/
const urlRules: FormRule[] = useMemo(
() =>
isPublic
? [
{
type: 'url',
message: '请输入正确的 Git 地址',
},
]
: ([] as FormRule[]),
[isPublic],
);

// 创建
const createCodeConfig = async (formData: FormData) => {
const params: FormData & { id?: number } = {
...formData,
};

if (opType === OperationType.Update) {
params.id = codeConfigData?.id;
}
const request = opType === OperationType.Create ? addKnowledgeSubject : '';
const [res] = await to(request(params));
if (res) {
message.success(opType === OperationType.Create ? '创建成功' : '修改成功');
onOk?.();
}
};

// 提交
const onFinish = (formData: FormData) => {
createCodeConfig(formData);
};

// 设置初始值
const initialValues: FormData = codeConfigData ?? {
code_repo_vis: AvailableRange.Public,
verify_mode: VerifyMode.Password,
};
if (initialValues.verify_mode === undefined || initialValues.verify_mode === null) {
initialValues.verify_mode = VerifyMode.Password;
}

return (
<KFModal
{...rest}
title="创建主体"
image={require('@/assets/img/create-experiment.png')}
width={825}
okButtonProps={{
htmlType: 'submit',
form: 'form',
}}
destroyOnClose
>
<Form
name="form"
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={initialValues}
autoComplete="off"
>
<Form.Item
label="主体名称"
name="name"
required
rules={[
{
required: true,
message: '请输入主体名称',
},
]}
>
<Input placeholder="请输入主体名称" showCount allowClear maxLength={64} />
</Form.Item>
<Form.Item
label="主体描述"
name="description"
required
rules={[
{
required: true,
message: '请输入主体描述',
},
]}
>
<Input placeholder="请输入主体描述" showCount allowClear maxLength={64} />
</Form.Item>
</Form>
</KFModal>
);
}

export default AddCodeConfigModal;

+ 0
- 0
react-ui/src/pages/knowledge/components/AddUpdateModal/index.less View File


+ 161
- 0
react-ui/src/pages/knowledge/components/AddUpdateModal/index.tsx View File

@@ -0,0 +1,161 @@
import { getAccessToken } from '@/access';
import KFModal from '@/components/KFModal';
import { AvailableRange } from '@/enums';
import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { getEntityGraph, postVersion } from '@/services/graph';
import { getFileListFromEvent } from '@/utils/ui';
import { Button, Form, message, Tabs, Upload, type ModalProps, type UploadProps } from 'antd';
import { useEffect, useState } from 'react';
import './index.less';

export enum VerifyMode {
Password = 0, // 用户名密码
SSH = 1, // SSH Key
}

export enum OperationType {
Create = 0, // 新建
Update = 1, // 更新
}

type FormData = Partial<CodeConfigData>;

interface AddCodeConfigModalProps extends Omit<ModalProps, 'onOk'> {
opType: OperationType;
codeConfigData?: CodeConfigData;
onOk: () => void;
}

function AddCodeConfigModal({ opType, codeConfigData, onOk, ...rest }: AddCodeConfigModalProps) {
const items = [
{
key: '1',
label: `CSV`,
children: null,
},
];

const [form] = Form.useForm();
const [activeTab, setActiveTab] = useState<string>('1');
const [ontologyList, setOntologyList] = useState([]);
const isPublic = Form.useWatch('code_repo_vis', form) === AvailableRange.Public;
useEffect(() => {
getEntityGraph().then((ret) => {
setOntologyList(ret.data);
});
}, []);
const [fileList, setFileList] = useState([]);

const handleChange: UploadProps['onChange'] = (info) => {
const { status } = info;
if (status == 'done') {
console.log(info);
}
};

// 上传前认证
const beforeUpload: UploadProps['beforeUpload'] = () => {
const fileList = form.getFieldValue('fileList');
if (Array.isArray(fileList) && fileList.length >= 1) {
message.error('只允许上传一个文件');
return Upload.LIST_IGNORE;
}
return true;
};
const uploadProps: UploadProps = {
action: '/api//kg/version/upload',
headers: {
Authorization: getAccessToken() || '',
},
maxCount: 1,
defaultFileList: [],
};
// 创建
// const createCodeConfig = async (formData: FormData) => {
// console.log(OperationType);

// const params: FormData & { id?: number } = {
// ...formData,
// };
// // 清除多余的信息
// // if (formData.code_repo_vis === AvailableRange.Public) {
// // omit(params, ['verify_mode', 'git_user_name', 'git_password', 'ssh_key']);
// // }
// // if (formData.verify_mode === VerifyMode.Password) {
// // omit(params, ['ssh_key']);
// // } else if (formData.verify_mode === VerifyMode.SSH) {
// // omit(params, ['git_user_name', 'git_password']);
// // }
// if (opType === OperationType.Update) {
// params.id = codeConfigData?.id;
// }
// const request = opType === OperationType.Create ? addKnowledgeGraph : editKnowledgeGraph;
// const [res] = await to(request(params));
// if (res) {
// message.success(opType === OperationType.Create ? '增量更新成功' : '全量更新成功');
// onOk?.();
// }
// };

// 提交
const onFinish = (formData: FormData) => {
console.log(formData);
postVersion({
path: formData.fileList[0].response.data,
kgId: codeConfigData.id,
}).then((ret) => {
if (ret.code == 200) {
message.success('导入成功');
onOk?.();
}
});
};

// 设置初始值
const initialValues: FormData = codeConfigData ?? {
code_repo_vis: AvailableRange.Public,
verify_mode: VerifyMode.Password,
};
if (initialValues.verify_mode === undefined || initialValues.verify_mode === null) {
initialValues.verify_mode = VerifyMode.Password;
}

return (
<KFModal
{...rest}
title={'导入'}
image={require('@/assets/img/create-experiment.png')}
width={825}
okButtonProps={{
htmlType: 'submit',
form: 'form',
}}
destroyOnClose
>
<Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs>
<Form
name="form"
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={initialValues}
autoComplete="off"
>
<Form.Item
name="fileList"
valuePropName="fileList"
getValueFromEvent={getFileListFromEvent}
required
>
<Upload {...uploadProps} beforeUpload={beforeUpload} fileList={fileList}>
<Button type="link" style={{ paddingLeft: 0, paddingRight: 0 }}>
选择CSV文件
</Button>
</Upload>
</Form.Item>
</Form>
</KFModal>
);
}

export default AddCodeConfigModal;

+ 108
- 0
react-ui/src/pages/knowledge/components/CodeConfigItem/index.less View File

@@ -0,0 +1,108 @@
.code-config-item {
position: relative;
width: calc(25% - 15px);
padding: 20px;
background: linear-gradient(180deg, #f7faff 0%, #ffffff 100%);
border: 2px solid white;
border-radius: 4px;
box-shadow: 0px 3px 10px rgba(164, 169, 181, 0.13);
cursor: pointer;

&:hover {
border-color: @primary-color;
}

@media screen and (max-width: 1860px) {
& {
width: calc(33.33% - 13.33px);
}
}

&__icon {
flex: none;
width: 16px;
height: 16px;
margin-right: 10px;
}

&__name {
position: relative;
margin-right: 20px;
margin-bottom: 0 !important;
color: @text-color;
font-weight: 500;
font-size: 16px;

&::after {
position: absolute;
top: 14px;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(
to right,
.addAlpha(@primary-color, 0.4) [] 0,
.addAlpha(@primary-color, 0) [] 100%
);
content: '';
}
}

&:hover &__name {
color: @primary-color;
}

&__tag {
flex: none;
padding: 1px 10px;
font-size: 13px;
border-radius: 2px;

&--public {
color: @primary-color;
background-color: .addAlpha(@primary-color, 0.1) [];
border: 1px solid .addAlpha(@primary-color, 0.5) [];
}

&--private {
color: @warning-color;
background-color: .addAlpha(@warning-color, 0.1) [];
border: 1px solid .addAlpha(@warning-color, 0.5) [];
}
}

:global {
.ant-btn {
flex: none;
color: #808080;
}
}

&__url-box {
margin-bottom: 15px;
padding: 14px;
background-color: .addAlpha(@primary-color, 0.04) [];
border-radius: 4px;
}

&__url {
margin-bottom: 15px !important;
color: @text-color;
font-size: 14px;
}

&__branch {
color: @text-color-secondary;
font-size: 14px;
}

&__user,
&__time {
display: flex;
flex: 0 1 content;
align-items: center;
width: 100%;
color: #808080;
font-size: 13px;
}
}

+ 94
- 0
react-ui/src/pages/knowledge/components/CodeConfigItem/index.tsx View File

@@ -0,0 +1,94 @@
import clock from '@/assets/img/clock.png';
import creatByImg from '@/assets/img/creatBy.png';
import KFIcon from '@/components/KFIcon';
import { AvailableRange } from '@/enums';
import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { formatDate } from '@/utils/date';
import { Button, Flex, Typography } from 'antd';
import classNames from 'classnames';
import styles from './index.less';

type CodeConfigItemProps = {
item: CodeConfigData;
onClick?: (item: CodeConfigData) => void;
onEdit?: (item: CodeConfigData) => void;
onRemove?: (item: CodeConfigData) => void;
};

function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps) {
return (
<div className={styles['code-config-item']} onClick={() => onClick?.(item)}>
<Flex justify="space-between" align="center" style={{ marginBottom: '20px', height: '32px' }}>
<img
className={styles['code-config-item__icon']}
src={require('@/assets/img/code-name-icon.png')}
alt=""
/>
<Typography.Paragraph
className={styles['code-config-item__name']}
ellipsis={{ tooltip: item.code_repo_name }}
>
{item.code_repo_name}
</Typography.Paragraph>
<div
className={classNames(
styles['code-config-item__tag'],
item.code_repo_vis === AvailableRange.Public
? styles['code-config-item__tag--public']
: styles['code-config-item__tag--private'],
)}
>
{item.code_repo_vis === AvailableRange.Public ? '公开' : '私有'}
</div>
<Button
type="text"
shape="circle"
style={{ marginLeft: 'auto', marginRight: '4px' }}
onClick={(e) => {
e.stopPropagation();
onEdit?.(item);
}}
>
<KFIcon type="icon-bianji" font={17} />
</Button>
<Button
type="text"
shape="circle"
style={{ marginRight: '-4px' }}
onClick={(e) => {
e.stopPropagation();
onRemove?.(item);
}}
>
<KFIcon type="icon-shanchu" font={17} />
</Button>
</Flex>
<div className={styles['code-config-item__url-box']}>
<Typography.Paragraph
className={styles['code-config-item__url']}
ellipsis={{ tooltip: item.git_url }}
>
{item.git_url}
</Typography.Paragraph>
<div className={styles['code-config-item__branch']}>{item.git_branch}</div>
</div>
<Flex justify="space-between">
<div className={styles['code-config-item__user']}>
<img
style={{ width: '16px', marginRight: '6px' }}
src={creatByImg}
alt=""
draggable={false}
/>
<span>{item.create_by}</span>
</div>
<div className={styles['code-config-item__time']}>
<img style={{ width: '12px', marginRight: '5px' }} src={clock} alt="" draggable={false} />
<span>最近更新: {formatDate(item.update_time, 'YYYY-MM-DD')}</span>
</div>
</Flex>
</div>
);
}

export default CodeConfigItem;

+ 50
- 0
react-ui/src/pages/knowledge/components/CodeSelectorModal/index.less View File

@@ -0,0 +1,50 @@
.code-selector {
width: 100%;
height: 100%;

&__search {
width: 100%;
}

:global {
.ant-input-affix-wrapper {
border-radius: 23px !important;
.ant-input-prefix {
margin-inline-end: 12px;
}
.ant-input-suffix {
margin-inline-end: 12px;
}
.ant-input-clear-icon {
font-size: 16px;
}
}

.ant-input-group-addon {
display: none;
}

.ant-pagination {
.ant-select-single {
height: 32px !important;
}
}
}

&__content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
width: 100%;
max-height: 50vh;
margin-top: 24px;
margin-bottom: 30px;
overflow-x: hidden;
overflow-y: auto;
}

&__empty {
padding-top: 40px;
}
}

+ 120
- 0
react-ui/src/pages/knowledge/components/CodeSelectorModal/index.tsx View File

@@ -0,0 +1,120 @@
/*
* @Author: 赵伟
* @Date: 2024-04-11 16:31:18
* @Description: 选择代码
*/

import KFModal from '@/components/KFModal';
import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { getCodeConfigListReq } from '@/services/codeConfig';
import { to } from '@/utils/promise';
import { Icon } from '@umijs/max';
import type { ModalProps, PaginationProps } from 'antd';
import { Empty, Input, Pagination } from 'antd';
import { useEffect, useState } from 'react';
import CodeConfigItem from '../CodeConfigItem';
import styles from './index.less';

export { type CodeConfigData };

export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> {
onOk?: (params: CodeConfigData | undefined) => void;
}

function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
const [dataList, setDataList] = useState<CodeConfigData[]>([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<PaginationProps>({
current: 1,
pageSize: 20,
});
const [searchText, setSearchText] = useState<string | undefined>(undefined);
const [inputText, setInputText] = useState<string | undefined>(undefined);

useEffect(() => {
getDataList();
}, [pagination, searchText]);

// 获取数据请求
const getDataList = async () => {
const params = {
page: pagination.current! - 1,
size: pagination.pageSize,
code_repo_name: searchText !== '' ? searchText : undefined,
};
const [res] = await to(getCodeConfigListReq(params));
if (res && res.data && res.data.content) {
setDataList(res.data.content);
setTotal(res.data.totalElements);
}
};

// 搜索
const handleSearch = (value: string) => {
setSearchText(value);
};

const handleClick = (item: CodeConfigData) => {
onOk?.(item);
};

// 分页切换
const handlePageChange: PaginationProps['onChange'] = (page, pageSize) => {
setPagination({
current: page,
pageSize: pageSize,
});
};

return (
<KFModal
{...rest}
title="选择代码配置"
image={require('@/assets/img/modal-code-config.png')}
width={920}
footer={null}
destroyOnClose
>
<div className={styles['code-selector']}>
<Input.Search
className={styles['code-selector__search']}
placeholder="按代码仓库名称筛选"
allowClear
onSearch={handleSearch}
size="large"
onChange={(e) => setInputText(e.target.value)}
suffix={null}
value={inputText}
prefix={
<Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} />
}
/>
{dataList?.length !== 0 ? (
<>
<div className={styles['code-selector__content']}>
{dataList?.map((item) => (
<CodeConfigItem item={item} key={item.id} onClick={handleClick} />
))}
</div>
<Pagination
align="center"
total={total}
showSizeChanger
defaultPageSize={20}
pageSizeOptions={[20, 40, 60, 80, 100]}
showQuickJumper
onChange={handlePageChange}
{...pagination}
/>
</>
) : (
<div className={styles['code-selector__empty']}>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty>
</div>
)}
</div>
</KFModal>
);
}

export default CodeSelectorModal;

+ 29
- 0
react-ui/src/pages/knowledge/components/GlobalParamsDrawer/index.less View File

@@ -0,0 +1,29 @@
.form-item {
position: relative;
padding-top: 40px;
border-bottom: 1px dashed rgba(20, 49, 179, 0.12);

&__delete-button {
position: absolute;
top: 5px;
right: 24px;
}

:global {
.anticon.anticon-question-circle {
margin-top: -12px;
}
}
}

.form-item-add {
margin-top: 15px;

&:first-child {
margin-top: 0;
}

&__add-button {
padding: 0;
}
}

+ 189
- 0
react-ui/src/pages/knowledge/components/GlobalParamsDrawer/index.tsx View File

@@ -0,0 +1,189 @@
import KFIcon from '@/components/KFIcon';
import { getParamComponent, getParamRules } from '@/pages/Experiment/components/AddExperimentModal';
import { type PipelineGlobalParam } from '@/types';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Drawer, Form, Input, Radio, Tooltip } from 'antd';
import { NamePath } from 'antd/es/form/interface';
import { forwardRef, useImperativeHandle } from 'react';
import styles from './index.less';

type GlobalParamsDrawerProps = {
open: boolean;
onClose: () => void;
globalParam: PipelineGlobalParam[] | null;
};

const GlobalParamsDrawer = forwardRef(
({ open = false, onClose, globalParam = [] }: GlobalParamsDrawerProps, ref) => {
const [form] = Form.useForm();

useImperativeHandle(ref, () => ({
validateFields: async () => {
const [values, error] = await to(form.validateFields());
if (!error && values) {
return values;
} else {
return Promise.reject(error);
}
},
getFieldsValue: () => {
return form.getFieldsValue();
},
}));

// 处理参数类型变化
const handleTypeChange = (name: NamePath) => {
form.setFieldValue(name, null);
};

const removeParameter = (name: number, remove: (param: number) => void) => {
modalConfirm({
title: '确认删除该参数吗?',
onOk: () => {
remove(name);
},
});
};

return (
<Drawer
rootStyle={{ marginTop: '45px' }}
title="全局参数"
placement="right"
closeIcon={false}
getContainer={false}
onClose={onClose}
open={open}
width={520}
>
<Form
name="global_params_form"
autoComplete="off"
form={form}
labelCol={{ span: 5 }}
wrapperCol={{ span: 19 }}
initialValues={{ global_param: globalParam }}
labelAlign="left"
>
<Form.List name="global_param">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<div key={key} className={styles['form-item']}>
<Form.Item
{...restField}
name={[name, 'param_name']}
label="参数名称"
validateTrigger={[]}
rules={[
{ required: true, message: '请输入参数名称' },
{
validator: (_, value) => {
const list = form.getFieldValue('global_param') || [];
const names = list.filter((item: any) => item?.param_name === value);
if (value && names.length > 1) {
return Promise.reject('参数名称不能重复');
} else {
return Promise.resolve();
}
},
},
]}
>
<Input
placeholder="请输入参数名称"
allowClear
onBlur={() => form.validateFields()}
/>
</Form.Item>
<Form.Item
{...restField}
name={[name, 'description']}
label="参数描述"
rules={[{ required: true, message: '请输入参数描述' }]}
>
<Input.TextArea
placeholder="请输入参数描述"
autoSize={{ minRows: 2, maxRows: 4 }}
allowClear
/>
</Form.Item>
<Form.Item
{...restField}
name={[name, 'param_type']}
label="类  型"
rules={[{ required: true, message: '请选择类型' }]}
>
<Radio.Group
onChange={() => handleTypeChange(['global_param', name, 'param_value'])}
>
<Radio value={1}>字符串</Radio>
<Radio value={2}>整型</Radio>
<Radio value={3}>布尔类型</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prev, cur) =>
prev.global_param?.[name]?.param_type !==
cur.global_param?.[name]?.param_type
}
>
{({ getFieldValue }) => (
<Form.Item
{...restField}
name={[name, 'param_value']}
label="值"
rules={getParamRules(
getFieldValue(['global_param', name, 'param_type']),
true,
)}
>
{getParamComponent(getFieldValue(['global_param', name, 'param_type']))}
</Form.Item>
)}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'is_sensitive']}
label="脱敏显示"
rules={[{ required: true, message: '请选择' }]}
tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示"
>
<Radio.Group>
<Radio value={1}>是</Radio>
<Radio value={0}>否</Radio>
</Radio.Group>
</Form.Item>
<Tooltip title="删除参数">
<Button
className={styles['form-item__delete-button']}
type="link"
onClick={() => removeParameter(name, remove)}
icon={<KFIcon type="icon-shanchu" />}
></Button>
</Tooltip>
</div>
))}
<Form.Item className={styles['form-item-add']}>
<Button
className={styles['form-item-add__add-button']}
type="link"
onClick={() => add()}
icon={<PlusOutlined />}
>
流水线参数
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Drawer>
);
},
);

export default GlobalParamsDrawer;

+ 61
- 0
react-ui/src/pages/knowledge/components/GraphNodeDrawer/index.less View File

@@ -0,0 +1,61 @@
.pipeline-drawer {
:global {
label {
width: 100%;

&::after {
display: none;
}
}
.ant-tabs-nav .ant-tabs-nav-list{
width: 100%;
justify-content: space-between;
}
.ant-tabs .ant-tabs-tab{
padding: 12px 20px 12px 24px;
margin: 0;
}
.ant-tabs .ant-tabs-ink-bar{
background-color: #1664ff;
}
}

&__title {
display: flex;
align-items: center;
width: 100%;
height: 43px;
margin-bottom: 20px;
padding: 0 24px;
color: @text-color;
font-size: @font-size;
background: #f8fbff;
}

&__ref-row {
display: flex;
align-items: center;

&__select-button {
display: flex;
flex: none;
align-items: center;
justify-content: flex-start;
margin-left: 10px;
padding-right: 0;
padding-left: 0;
}
}
.graph-drawer-box{
padding: 12px 20px;
color:#1d1d20;
font-size:15px;
&__slice{
width: 100%;
height:0px;
border:1px solid;
border-color:rgba(20, 49, 179, 0.12);
margin: 5px 0 20px 0;
}
}
}

+ 382
- 0
react-ui/src/pages/knowledge/components/GraphNodeDrawer/index.tsx View File

@@ -0,0 +1,382 @@
import KFIcon from '@/components/KFIcon';
import { requiredValidator } from '@/components/ParameterInput';
import { CommonTabKeys } from '@/enums';
import { useComputingResource } from '@/hooks/resource';
import { createMenuItems } from '@/pages/Pipeline/Info/utils';
import {
PipelineGlobalParam,
PipelineNodeModel,
PipelineNodeModelParameter,
PipelineNodeModelSerialize,
} from '@/types';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import { INode } from '@antv/g6';
import { Drawer, Form, Input, MenuProps, Tabs } from 'antd';
import { NamePath } from 'antd/es/form/interface';
import { forwardRef, useImperativeHandle, useState } from 'react';
import CodeSelectorModal from '../CodeSelectorModal';
import PropsLabel from '../PropsLabel';
import ResourceSelectorModal, {
ResourceSelectorType,
selectorTypeConfig,
} from '../ResourceSelectorModal';
import styles from './index.less';
const { TextArea } = Input;

type PipelineNodeParameterProps = {
onFormChange: (data: PipelineNodeModelSerialize) => void;
};

const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParameterProps, ref) => {
const [form] = Form.useForm();
const [activeTab, setActiveTab] = useState<string>('1');
const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>(
{} as PipelineNodeModelSerialize,
);
const [open, setOpen] = useState(false);
const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模
const [menuItems, setMenuItems] = useState<MenuProps['items']>([]);
const items = [
{
key: '1',
label: `属性`,
children: null,
},
{
key: '2',
label: `条件过滤`,
children: null,
},
{
key: '3',
label: `多条过滤`,
children: null,
},
];
const afterOpenChange = async () => {
if (!open) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_values, error] = await to(form.validateFields());
const fields = form.getFieldsValue();
const control_strategy = JSON.stringify(fields.control_strategy);
const in_parameters = JSON.stringify(fields.in_parameters);
const out_parameters = JSON.stringify(fields.out_parameters);
// console.log('getFieldsValue', fields);

const res = {
...stagingItem,
...fields,
control_strategy: control_strategy,
in_parameters: in_parameters,
out_parameters: out_parameters,
formError: !!error,
};

console.log('res', res);
onFormChange(res);
}
};
const onClose = () => {
setOpen(false);
};

useImperativeHandle(ref, () => ({
showDrawer(
model: PipelineNodeModel,
params: PipelineGlobalParam[],
parentNodes: INode[],
validate: boolean = false,
) {
try {
const nodeData: PipelineNodeModelSerialize = {
...model,
in_parameters: JSON.parse(model.in_parameters),
out_parameters: JSON.parse(model.out_parameters),
control_strategy: JSON.parse(model.control_strategy),
};
// console.log('model', nodeData);
setStagingItem({
...nodeData,
});
form.resetFields();
form.setFieldsValue({
...nodeData,
});
if (validate) {
form.validateFields();
}
} catch (error) {
console.error('JSON.parse error: ', error);
}
setOpen(true);

// 参数下拉菜单
setMenuItems(createMenuItems(params, parentNodes));
},
close: () => {
onClose();
},
validateFields: async () => {
if (!open) {
return;
}
const [values, error] = await to(form.validateFields());
if (!error && values) {
return values;
} else {
form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' });
return Promise.reject(error);
}
},
}));

// ref 类型选择
const selectRefData = (
formItemName: NamePath,
item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>,
) => {
if (item.item_type === 'code') {
selectCodeConfig(formItemName, item);
} else {
selectResource(formItemName, item);
}
};

// 选择代码配置
const selectCodeConfig = (
formItemName: NamePath,
item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>,
) => {
const { close } = openAntdModal(CodeSelectorModal, {
onOk: (res) => {
if (res) {
const value = JSON.stringify({
id: res.id,
name: res.code_repo_name,
code_path: res.git_url,
branch: res.git_branch,
username: res.git_user_name,
password: res.git_password,
ssh_private_key: res.ssh_key,
});
form.setFieldValue(formItemName, {
...item,
value,
showValue: res.code_repo_name,
fromSelect: true,
});
}
close();
},
});
};

// 选择数据集、模型、镜像
const selectResource = (
formItemName: NamePath,
item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>,
) => {
let type: ResourceSelectorType;
switch (item.item_type) {
case 'dataset':
type = ResourceSelectorType.Dataset;
break;
case 'model':
type = ResourceSelectorType.Model;
break;
default:
type = ResourceSelectorType.Mirror;
break;
}
const fieldValue = form.getFieldValue(formItemName);
const activeTab = fieldValue?.activeTab as CommonTabKeys | undefined;
const expandedKeys = Array.isArray(fieldValue?.expandedKeys) ? fieldValue?.expandedKeys : [];
const checkedKeys = Array.isArray(fieldValue?.checkedKeys) ? fieldValue?.checkedKeys : [];
const { close } = openAntdModal(ResourceSelectorModal, {
type,
defaultExpandedKeys: expandedKeys,
defaultCheckedKeys: checkedKeys,
defaultActiveTab: activeTab,
onOk: (res) => {
if (res) {
if (type === ResourceSelectorType.Mirror) {
const { activeTab, id, version, path } = res;
if (formItemName === 'image') {
// 单独的选择镜像
form.setFieldValue(formItemName, path);
} else {
// 输入参数选择镜像
form.setFieldValue(formItemName, {
...item,
value: path,
showValue: path,
fromSelect: true,
activeTab,
expandedKeys: [id],
checkedKeys: [`${id}-${version}`],
});
}
} else {
const { activeTab, id, name, version, path, identifier, owner } = res;
const value = JSON.stringify({
id,
name,
version,
path,
identifier,
owner,
});
const showValue = `${name}:${version}`;
form.setFieldValue(formItemName, {
...item,
value,
showValue,
fromSelect: true,
activeTab,
expandedKeys: [id],
checkedKeys: [`${id}-${version}`],
});
}
} else {
if (type === ResourceSelectorType.Mirror && formItemName === 'image') {
form.setFieldValue(formItemName, undefined);
} else {
form.setFieldValue(formItemName, {
...item,
value: undefined,
showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: [],
checkedKeys: [],
});
}
}
form.validateFields([formItemName]);
close();
},
});
};

// 获取选择数据集、模型后面按钮 icon
const getSelectBtnIcon = (item: { item_type: string }) => {
const type = item.item_type;
if (type === 'code') {
return <KFIcon type="icon-xuanzedaimapeizhi" />;
}

let selectorType: ResourceSelectorType;
if (type === 'dataset') {
selectorType = ResourceSelectorType.Dataset;
} else if (type === 'model') {
selectorType = ResourceSelectorType.Model;
} else {
selectorType = ResourceSelectorType.Mirror;
}

return <KFIcon type={selectorTypeConfig[selectorType].buttonIcon} />;
};

// 参数回填
const handleParameterClick = (name: NamePath, value: any) => {
form.setFieldValue(name, value);
};

// form item label
const getLabel = (
item: { key: string; value: PipelineNodeModelParameter },
namePrefix: string,
) => {
return item.value.type === 'select' ? (
item.value.label + '(' + item.key + ')'
) : (
<PropsLabel
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'}
onClick={(value) => {
handleParameterClick([namePrefix, item.key], {
...item.value,
value,
fromSelect: true,
showValue: value,
});
}}
/>
);
};

// 必填项校验规则
const getFormRules = (item: { key: string; value: PipelineNodeModelParameter }) => {
return item.value.require
? [
{
validator: requiredValidator,
message: '必填项',
},
]
: [];
};

// 控制策略
const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map(
([key, value]) => ({ key, value }),
);

// 输入参数
const inParametersList = Object.entries(stagingItem.in_parameters ?? {}).map(([key, value]) => ({
key,
value,
}));

// 输出参数
const outParametersList = Object.entries(stagingItem.out_parameters ?? {}).map(
([key, value]) => ({ key, value }),
);

return (
<Drawer
placement="right"
rootStyle={{ marginTop: '52px' }}
getContainer={false}
closeIcon={false}
onClose={onClose}
afterOpenChange={afterOpenChange}
open={open}
width={358}
className={styles['pipeline-drawer']}
>
<Tabs
style={{ width: '100%' }}
activeKey={activeTab}
items={items}
onChange={(key) => setActiveTab(key)}
></Tabs>
<div className={styles['graph-drawer-box']}>
<div style={{ marginBottom: '15px' }}>
<span>ID:</span>
<span style={{ marginLeft: '10px' }}>9855312121212121212</span>
</div>
<div style={{ marginBottom: '15px' }}>
<span>Label:</span>
<span style={{ marginLeft: '10px' }}>9855312121212121212</span>
</div>
<div className={styles['graph-drawer-box__slice']}></div>
<div style={{ marginBottom: '15px' }}>
<span>Name:</span>
<span style={{ marginLeft: '10px' }}>小时一西</span>
</div>
<div style={{ marginBottom: '15px' }}>
<span>XXX时间:</span>
<span style={{ marginLeft: '10px' }}>15: 10: 205分26秒</span>
</div>
<div style={{ marginBottom: '15px' }}>
<span>XXX:</span>
<span style={{ marginLeft: '10px' }}>15: 10: 205分26秒</span>
</div>
</div>
</Drawer>
);
});

export default PipelineNodeParameter;

+ 54
- 0
react-ui/src/pages/knowledge/components/ModelMenu/index.less View File

@@ -0,0 +1,54 @@
.collapse {
flex: none;
// width: 250px;
height: 100%;
padding: 20px 0 0 15px;
:global {
.ant-collapse {
height: calc(100% - 60px);
overflow-y: auto;
background-color: #fff;
border-color: transparent !important;
}
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
margin-bottom: 5px;
padding: 20px 16px 15px 16px;
background-color: #fff;
border-color: transparent;
}

.ant-collapse > .ant-collapse-item {
margin: 0 10px;
border-bottom: 0.5px dashed rgba(20, 49, 179, 0.12);
border-radius: 0px;
}
.ant-collapse .ant-collapse-content {
padding-bottom: 15px;
border-top: 1px solid transparent;
}
.ant-collapse .ant-collapse-content > .ant-collapse-content-box {
padding: 0;
}
}
}
.collapseItem {
display: flex;
align-items: center;
height: 40px;
padding: 0 16px;
color: @text-color-secondary;
font-size: 14px;
border-radius: 4px;
cursor: pointer;

&:hover {
color: @primary-color;
background: rgba(22, 100, 255, 0.08);
}
}
.modelMenusTitle {
margin-bottom: 10px;
padding: 12px 25px;
color: #111111;
font-size: 16px;
}

+ 108
- 0
react-ui/src/pages/knowledge/components/ModelMenu/index.tsx View File

@@ -0,0 +1,108 @@
import { getComponentAll } from '@/services/pipeline/index.js';
import { PipelineNodeModel } from '@/types';
import { to } from '@/utils/promise';
import { Button, Collapse, type CollapseProps, Input } from 'antd';
import { useEffect, useState } from 'react';
import Styles from './index.less';

type ModelMenuData = {
key: string;
name: string;
value: PipelineNodeModel[];
};

type ModelMenuProps = {
onComponentDragEnd: (
data: PipelineNodeModel & { x: number; y: number; label: string; img: string },
) => void;
};
const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => {
const [modelMenusList, setModelMenusList] = useState<ModelMenuData[]>([]);
const [collapseItems, setCollapseItems] = useState<CollapseProps['items']>([]);

useEffect(() => {
getAllComponents();
}, []);

// 获取所有组件
const getAllComponents = async () => {
const [res] = await to(getComponentAll());
if (res && res.data) {
const menus = res.data as ModelMenuData[];
setModelMenusList(menus);
const items = menus.map((item) => {
return {
key: item.key,
label: item.name,
children: item.value.map((ele) => {
return (
<div
key={ele.id}
draggable="true"
onDragEnd={(e) => {
dragEnd(e, ele);
}}
className={Styles.collapseItem}
>
{ele.icon_path && (
<img
style={{ height: '16px', marginRight: '15px' }}
src={`/assets/images/${ele.icon_path}.png`}
draggable={false}
alt=""
/>
)}
{ele.component_label}
</div>
);
}),
};
});
setCollapseItems(items);
}
};

const dragEnd = (e: React.DragEvent<HTMLDivElement>, data: PipelineNodeModel) => {
onComponentDragEnd({
...data,
x: e.clientX,
y: e.clientY,
label: data.component_label,
img: `/assets/images/${data.icon_path}.png`,
});
};

const defaultActiveKey = modelMenusList.map((item) => item.key + '');
return (
<div className={Styles.collapse}>
<Input.Search
placeholder="按XXXX筛选"
allowClear
// onSearch={onSearch}
// onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
// value={inputText}
/>
<Button
key="info"
type="primary"
style={{ marginLeft: '20px' }}
// onClick={() => toDetail(record)}
>
查询
</Button>
{/* <div className={Styles.modelMenusTitle}>组件库</div> */}
{/* 这样 defaultActiveKey 才能生效 */}
{modelMenusList.length > 0 ? (
<Collapse
collapsible="header"
expandIconPosition="end"
defaultActiveKey={defaultActiveKey}
items={collapseItems}
></Collapse>
) : null}
</div>
);
};

export default ModelMenu;

+ 6
- 0
react-ui/src/pages/knowledge/components/PropsLabel/index.less View File

@@ -0,0 +1,6 @@
.props-label {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}

+ 44
- 0
react-ui/src/pages/knowledge/components/PropsLabel/index.tsx View File

@@ -0,0 +1,44 @@
import { Dropdown, type MenuProps } from 'antd';
import { useEffect } from 'react';
import styles from './index.less';

type PropsLabelProps = {
title: string;
menuItems: MenuProps['items'];
onClick?: (key: string) => void;
};

function PropsLabel({ title, menuItems, onClick }: PropsLabelProps) {
useEffect(() => {}, []);

const handleItemClick: MenuProps['onClick'] = (e) => {
const keyPath = e.keyPath.reverse();
if (keyPath[0] === 'global') {
onClick?.(`\${${e.key}}`);
} else {
onClick?.(`{{${keyPath.join('.')}}}`);
}
};

return (
<div className={styles['props-label']}>
<div>{title}</div>
{menuItems && menuItems.length > 0 && (
<Dropdown
menu={{
items: menuItems,
onClick: handleItemClick,
triggerSubMenuAction: 'hover',
}}
trigger={['click']}
placement="topRight"
arrow
>
<a onClick={(e) => e.preventDefault()}>参数</a>
</Dropdown>
)}
</div>
);
}

export default PropsLabel;

+ 251
- 0
react-ui/src/pages/knowledge/components/ResourceSelectorModal/config.tsx View File

@@ -0,0 +1,251 @@
import datasetImg from '@/assets/img/modal-select-dataset.png';
import mirrorImg from '@/assets/img/modal-select-mirror.png';
import modelImg from '@/assets/img/modal-select-model.png';
import { AvailableRange, CommonTabKeys } from '@/enums';
import { ResourceData, ResourceVersionData } from '@/pages/Dataset/config';
import { MirrorVersionData } from '@/pages/Mirror/Info';
import { MirrorData } from '@/pages/Mirror/List';
import {
getDatasetInfo,
getDatasetList,
getDatasetVersionList,
getModelInfo,
getModelList,
getModelVersionList,
} from '@/services/dataset/index.js';
import { getMirrorListReq, getMirrorVersionListReq } from '@/services/mirror';
import type { TabsProps, TreeDataNode } from 'antd';
import { pick } from 'lodash';

export enum ResourceSelectorType {
Model = 'Model', // 模型
Dataset = 'Dataset', // 数据集
Mirror = 'Mirror', //镜像
}

// 数据集、模型列表转为树形结构
const convertDatasetToTreeData = (list: ResourceData[]): TreeDataNode[] => {
return list.map((v) => ({
...v,
key: `${v.id}`,
title: v.name,
isLeaf: false,
checkable: false,
}));
};

// 镜像列表转为树形结构
const convertMirrorToTreeData = (list: MirrorData[]): TreeDataNode[] => {
return list.map((v) => ({
key: `${v.id}`,
title: v.name,
isLeaf: false,
checkable: false,
}));
};

// 数据集版本列表转为树形结构
const convertDatasetVersionToTreeData = (
parentId: string,
info: ResourceData,
list: ResourceVersionData[],
): TreeDataNode[] => {
return list.map((item: ResourceVersionData) => ({
...pick(info, ['id', 'name', 'owner', 'identifier']),
version: item.name,
title: item.name,
key: `${parentId}-${item.name}`,
isLeaf: true,
checkable: true,
}));
};

// 镜像版本列表转为树形结构
const convertMirrorVersionToTreeData = (
parentId: string,
list: MirrorVersionData[],
): TreeDataNode[] => {
return list.map((item: MirrorVersionData) => ({
url: item.url,
title: item.tag_name,
key: `${parentId}-${item.id}`,
isLeaf: true,
checkable: true,
}));
};

interface SelectorTypeInfo {
getList: (isPublic: boolean) => Promise<any>; // 获取资源列表
getVersions: (parentKey: string, parentNode: any) => Promise<any>; // 获取资源版本列表
getFiles: (parentKey: string, parentNode: any) => Promise<any>; // 获取资源文件列表
readonly modalIcon: string; // modal icon
readonly buttonIcon: string; // button icon
readonly name: string; // 名称
readonly tabItems: TabsProps['items']; // tab 列表
readonly buttontTitle: string; // 按钮 title
}

export class DatasetSelector implements SelectorTypeInfo {
readonly name = '数据集';
readonly modalIcon = datasetImg;
readonly buttonIcon = 'icon-xuanzeshujuji';
readonly tabItems = [
{
key: CommonTabKeys.Private,
label: '我的数据集',
},
{
key: CommonTabKeys.Public,
label: '公开数据集',
},
];
readonly buttontTitle = '选择数据集';

async getList(isPublic: boolean) {
const res = await getDatasetList({ is_public: isPublic, page: 0, size: 2000 });
if (res && res.data) {
const list = res.data.content || [];
return convertDatasetToTreeData(list);
} else {
return Promise.reject('获取数据集列表失败');
}
}
async getVersions(parentKey: string, parentNode: ResourceData) {
const res = await getDatasetVersionList(pick(parentNode, ['owner', 'identifier']));
if (res && res.data) {
const list = res.data;
return convertDatasetVersionToTreeData(parentKey, parentNode, list);
} else {
return Promise.reject('获取数据集版本列表失败');
}
}

async getFiles(_parentKey: string, parentNode: ResourceData & ResourceVersionData) {
const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version']);
const res = await getDatasetInfo(params);
if (res && res.data) {
const path = res.data.relative_paths || '';
const list = res.data.dataset_version_vos || [];
return {
path,
content: list,
};
} else {
return Promise.reject('获取数据集文件列表失败');
}
}
}

export class ModelSelector implements SelectorTypeInfo {
readonly name = '模型';
readonly modalIcon = modelImg;
readonly buttonIcon = 'icon-xuanzemoxing';
readonly tabItems = [
{
key: CommonTabKeys.Private,
label: '我的模型',
},
{
key: CommonTabKeys.Public,
label: '公开模型',
},
];
readonly buttontTitle = '选择模型';

async getList(isPublic: boolean) {
const res = await getModelList({ is_public: isPublic, page: 0, size: 2000 });
if (res && res.data) {
const list = res.data.content || [];
return convertDatasetToTreeData(list);
} else {
return Promise.reject('获取模型列表失败');
}
}
async getVersions(key: string, parentNode: ResourceData) {
const res = await getModelVersionList(pick(parentNode, ['owner', 'identifier']));
if (res && res.data) {
const list = res.data;
return convertDatasetVersionToTreeData(key, parentNode, list);
} else {
return Promise.reject('获取模型版本列表失败');
}
}

async getFiles(_parentKey: string, parentNode: ResourceData & ResourceVersionData) {
const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version']);
const res = await getModelInfo(params);
if (res && res.data) {
const path = res.data.relative_paths || '';
const list = res.data.model_version_vos || [];
return {
path,
content: list,
};
} else {
return Promise.reject('获取模型文件列表失败');
}
}
}

export class MirrorSelector implements SelectorTypeInfo {
readonly name = '镜像';
readonly modalIcon = mirrorImg;
readonly buttonIcon = 'icon-xuanzejingxiang';
readonly tabItems = [
{
key: CommonTabKeys.Private,
label: '我的镜像',
},
{
key: CommonTabKeys.Public,
label: '公开镜像',
},
];
readonly buttontTitle = '选择镜像';

async getList(isPublic: boolean) {
const res = await getMirrorListReq({
image_type: isPublic ? AvailableRange.Public : AvailableRange.Private,
page: 0,
size: 2000,
});
if (res && res.data) {
const list = res.data.content || [];
return convertMirrorToTreeData(list);
} else {
return Promise.reject('获取镜像列表失败');
}
}
async getVersions(parentKey: string) {
const res = await getMirrorVersionListReq({
image_id: parentKey,
page: 0,
size: 2000,
});
if (res && res.data) {
const list = res.data.content || [];
return convertMirrorVersionToTreeData(parentKey, list);
} else {
return Promise.reject('获取镜像版本列表失败');
}
}

async getFiles(_parentKey: string, parentNode: MirrorVersionData) {
const { url } = parentNode;
return {
path: url,
content: [
{
url: url,
file_name: `${url}`,
},
],
};
}
}

export const selectorTypeConfig: Record<ResourceSelectorType, SelectorTypeInfo> = {
[ResourceSelectorType.Model]: new ModelSelector(),
[ResourceSelectorType.Dataset]: new DatasetSelector(),
[ResourceSelectorType.Mirror]: new MirrorSelector(),
};

+ 76
- 0
react-ui/src/pages/knowledge/components/ResourceSelectorModal/index.less View File

@@ -0,0 +1,76 @@
.model-tabs {
margin-left: 8px;
:global {
.ant-tabs-tab {
padding-top: 0;
}
}
}

.model-selector {
display: flex;
align-items: flex-start;

:global {
.ant-input-affix-wrapper .ant-input-prefix {
margin-inline-end: 12px;
}
}

&__left {
width: 488px;
height: 398px;
margin-right: 15px;
padding: 15px;
background-color: @background-color-primary;
border: 1px solid @border-color;
border-radius: 8px;

&__search {
margin-bottom: 14px;
padding-left: 0;
background-color: transparent;
border-width: 0;
border-bottom: 1px solid @border-color-secondary;
border-radius: 0;
}

&__tree-title {
display: inline-block;
.singleLine();
}
}

&__right {
width: calc(100% - 488px - 15px);
height: 398px;
padding: 15px;
background-color: @background-color-primary;
border: 1px solid @border-color;
border-radius: 8px;

&__title {
height: 46px;
margin-bottom: 15px;
padding: 3px 0 6px;
color: @text-color;
font-size: @font-size;
line-height: 46px;
border-bottom: 1px solid @border-color-secondary;
}
&__files {
height: calc(100% - 75px);
overflow-y: auto;

&__file {
margin-bottom: 10px;
padding: 3px 10px;
color: @text-color-secondary;
font-size: 13px;
word-break: break-all;
background: @background-color-gray;
border-radius: 4px;
}
}
}
}

+ 314
- 0
react-ui/src/pages/knowledge/components/ResourceSelectorModal/index.tsx View File

@@ -0,0 +1,314 @@
/*
* @Author: 赵伟
* @Date: 2024-04-11 16:31:18
* @Description: 选择数据集、模型、镜像
*/

import KFModal from '@/components/KFModal';
import { CommonTabKeys } from '@/enums';
import { ResourceFileData } from '@/pages/Dataset/config';
import { to } from '@/utils/promise';
import { Icon } from '@umijs/max';
import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd';
import { Input, Tabs, Tree } from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ResourceSelectorType, selectorTypeConfig } from './config';
import styles from './index.less';
export { ResourceSelectorType, selectorTypeConfig };

// 选择数据集\模型\镜像的返回类型
export type ResourceSelectorResponse = {
id: string; // 数据集\模型\镜像 id
name: string; // 数据集\模型\镜像 name
version: string; // 数据集\模型\镜像版本
path: string; // 数据集\模型\镜像版本路径
identifier: string; // 数据集\模型 identifier
owner: string; // 数据集\模型 owner
activeTab: CommonTabKeys; // 是我的还是公开的
};

export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
type: ResourceSelectorType; // 数据集\模型\镜像
defaultExpandedKeys?: React.Key[];
defaultCheckedKeys?: React.Key[];
defaultActiveTab?: CommonTabKeys;
onOk?: (params: ResourceSelectorResponse | undefined) => void;
}

type TreeRef = GetRef<typeof Tree<TreeDataNode>>;

// 更新树形结构的 children
const updateChildren = (parentId: string, children: TreeDataNode[]) => {
return (node: TreeDataNode) => {
if (node.key === parentId) {
return {
...node,
children,
};
}
return node;
};
};

// 得到数据集\模型\镜像 id 和下属版本号
const getIdAndVersion = (versionKey: string) => {
const index = versionKey.indexOf('-');
const id = versionKey.slice(0, index);
const version = versionKey.slice(index + 1);
return {
id,
version,
};
};

function ResourceSelectorModal({
type,
defaultExpandedKeys = [],
defaultCheckedKeys = [],
defaultActiveTab = CommonTabKeys.Private,
onOk,
...rest
}: ResourceSelectorModalProps) {
const [activeTab, setActiveTab] = useState<string>(defaultActiveTab);
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]);
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]);
const [files, setFiles] = useState<ResourceFileData[]>([]);
const [versionPath, setVersionPath] = useState('');
const [searchText, setSearchText] = useState('');
const [firstLoadList, setFirstLoadList] = useState(false);
const [firstLoadVersions, setFirstLoadVersions] = useState(false);
const treeRef = useRef<TreeRef>(null);
const config = selectorTypeConfig[type];

useEffect(() => {
setExpandedKeys([]);
setCheckedKeys([]);
setLoadedKeys([]);
setFiles([]);
setVersionPath('');
setSearchText('');
getTreeData();
}, [activeTab, type]);

const treeData = useMemo(
() =>
originTreeData.filter((v) =>
(v.title as string).toLowerCase()?.includes(searchText.toLowerCase()),
),
[originTreeData, searchText],
);

// 获取数据集\模型\镜像列表
const getTreeData = async () => {
const isPublic = activeTab === CommonTabKeys.Private ? false : true;
const [res] = await to(config.getList(isPublic));
if (res) {
setOriginTreeData(res);

// 恢复上一次的 Expand 操作
restoreLastExpand();
} else {
setOriginTreeData([]);
}
};

// 获取数据集\模型\镜像版本列表
const getVersions = async (parentId: string, parentNode: any) => {
const [res, error] = await to(config.getVersions(parentId, parentNode));
if (res) {
// 更新 treeData children
setOriginTreeData((prev) => prev.map(updateChildren(parentId, res)));

// 缓存 loadedKeys
const index = loadedKeys.find((v) => v === parentId);
if (!index) {
setLoadedKeys((prev) => prev.concat(parentId));
}

// 恢复上一次的 Check 操作
setTimeout(() => {
restoreLastCheck(parentId, res);
}, 300);
} else {
setExpandedKeys([]);
return Promise.reject(error);
}
};

// 获取版本下的文件
const getFiles = async (parentId: string, parentNode: any) => {
const [res] = await to(config.getFiles(parentId, parentNode));
if (res) {
setVersionPath(res.path);
setFiles(res.content);
} else {
setVersionPath('');
setFiles([]);
}
};

// 动态加载 tree children
const onLoadData = ({ key, children, ...rest }: TreeDataNode) => {
if (children) {
return Promise.resolve();
} else {
return getVersions(key as string, rest);
}
};

// 扩展
const onExpand: TreeProps['onExpand'] = (expandedKeysValue) => {
const lastKeys = expandedKeysValue.slice(-1);
setExpandedKeys(lastKeys);
};

// 选中
const onCheck: TreeProps['onCheck'] = (checkedKeysValue, { checkedNodes }) => {
const lastKeys = (checkedKeysValue as React.Key[]).slice(-1);
setCheckedKeys(lastKeys);
if (lastKeys.length && checkedNodes.length) {
const last = lastKeys[0] as string;
const lastNode = checkedNodes[checkedNodes.length - 1];
getFiles(last, lastNode);
} else {
setVersionPath('');
setFiles([]);
}
};

// 恢复上一次的 Expand 操作
// 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys
// fisrtLoadList 标志位
const restoreLastExpand = () => {
if (!firstLoadList && defaultExpandedKeys.length > 0) {
setTimeout(() => {
setExpandedKeys(defaultExpandedKeys);
setFirstLoadList(true);
setTimeout(() => {
treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' });
}, 100);
}, 0);
}
};

// 恢复上一次的 Check 操作
// 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口
// fisrtLoadVersions 标志位
const restoreLastCheck = (parentId: string, versions: TreeDataNode[]) => {
if (!firstLoadVersions && defaultCheckedKeys.length > 0) {
const last = defaultCheckedKeys[0] as string;
const { id } = getIdAndVersion(last);
// 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致
if (id === parentId) {
setTimeout(() => {
setCheckedKeys(defaultCheckedKeys);
const parentNode = versions.find((v) => v.key === last);
getFiles(last, parentNode);
setFirstLoadVersions(true);
setTimeout(() => {
treeRef?.current?.scrollTo({
key: defaultCheckedKeys[0],
align: 'bottom',
});
}, 100);
}, 0);
}
}
};

// 提交
const handleOk = () => {
if (checkedKeys.length > 0) {
const last = checkedKeys[0] as string;
const { id, version } = getIdAndVersion(last);
const treeNode = treeData.find((v) => v.key === id) as any;
const name = (treeNode?.title ?? '') as string;
const identifier = (treeNode?.identifier ?? '') as string;
const owner = (treeNode?.owner ?? '') as string;
const res = {
id,
name,
path: versionPath,
version,
identifier,
owner,
activeTab: activeTab as CommonTabKeys,
};
onOk?.(res);
} else {
onOk?.(undefined);
}
};

const title = `选择${config.name}`;
const palceholder = `请输入${config.name}名称`;
const fileTitle =
type === ResourceSelectorType.Mirror ? '已选镜像' : `已选${config.name}文件(${files.length})`;
const tabItems = config.tabItems;
const titleImg = config.modalIcon;

return (
<KFModal {...rest} title={title} image={titleImg} onOk={handleOk} width={920} destroyOnClose>
<div>
<Tabs
activeKey={activeTab}
items={tabItems}
onChange={setActiveTab}
className={styles['model-tabs']}
/>
<div className={styles['model-selector']}>
<div className={styles['model-selector__left']}>
<Input
className={styles['model-selector__left__search']}
placeholder={palceholder}
allowClear
variant="borderless"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />}
/>
<Tree
ref={treeRef}
rootStyle={{ backgroundColor: 'transparent' }}
loadData={onLoadData}
treeData={treeData}
onCheck={onCheck}
checkedKeys={checkedKeys}
multiple={false}
selectable={false}
height={324}
loadedKeys={loadedKeys}
expandedKeys={expandedKeys}
onExpand={onExpand}
checkable
titleRender={(nodeData) => {
return (
<span
className={styles['model-selector__left__tree-title']}
style={{ width: nodeData.isLeaf ? '370px' : '420px' }}
>
{nodeData.title as string}
</span>
);
}}
/>
</div>
<div className={styles['model-selector__right']}>
<div className={styles['model-selector__right__title']}>{fileTitle}</div>
<div className={styles['model-selector__right__files']}>
{files.map((v) => (
<div key={v.url} className={styles['model-selector__right__files__file']}>
{v.file_name}
</div>
))}
</div>
</div>
</div>
</div>
</KFModal>
);
}

export default ResourceSelectorModal;

+ 91
- 0
react-ui/src/pages/knowledge/components/SubjectEdgeDrawer/index.less View File

@@ -0,0 +1,91 @@
.subject-drawer {
:global {
label {
width: 100%;

&::after {
display: none;
}
}
}

&__title {
display: flex;
align-items: center;
width: 100%;
height: 43px;
margin-bottom: 20px;
padding: 0 24px;
color: @text-color;
font-size: @font-size;
background: #f8fbff;
}

&__ref-row {
display: flex;
align-items: center;

&__select-button {
display: flex;
flex: none;
align-items: center;
justify-content: flex-start;
margin-left: 10px;
padding-right: 0;
padding-left: 0;
}
}
.subject-drawer{
height:58px;
background:#ffffff;
border:1px solid;
border-color:#e6e6e6;
border-radius:6px;
display: flex;
align-items: center;
&__left{
width: 40px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
&__circle{
width:17px;
height:17px;
border-radius: 50%;
background:#f98e1b;
}

}
&__slice{
width:0px;
height:57px;
border:1px solid;
border-color:#e6e6e6;
}
&__right{
padding: 0 10px;
display: flex;
align-items: center;
&__circle{
display: flex;
justify-content: center;
align-items: center;
width:18px;
height:18px;
background:#fff;
margin-right: 8px;
border-radius: 50%;
cursor: pointer;
border:1px solid;

&__center{
width: 14px;
height: 14px;
border-radius: 50%;
background-color: #164fff;
}
}
}
}
}

+ 234
- 0
react-ui/src/pages/knowledge/components/SubjectEdgeDrawer/index.tsx View File

@@ -0,0 +1,234 @@
import KFIcon from '@/components/KFIcon';
import { useComputingResource } from '@/hooks/resource';
import {
subjectEdgeModelSerialize,
subjectGlobalParam,
subjectNodeModel,
subjectNodeModelParameter,
} from '@/types';
import { to } from '@/utils/promise';
import { INode } from '@antv/g6';
import { Drawer, Form, Input, MenuProps } from 'antd';
import { NamePath } from 'antd/es/form/interface';
import { forwardRef, useImperativeHandle, useState } from 'react';
import PropsLabel from '../PropsLabel';
import { ResourceSelectorType, selectorTypeConfig } from '../ResourceSelectorModal';
import styles from './index.less';
const { TextArea } = Input;
export type ServiceData = {
name: string;
isMultivalued: string;
type: string;
flag: boolean;
};
type subjectNodeParameterProps = {
onFormChange: (data: subjectEdgeModelSerialize) => void;
};

const subjectNodeParameter = forwardRef(({ onFormChange }: subjectNodeParameterProps, ref) => {
const [form] = Form.useForm();
const [stagingItem, setStagingItem] = useState<subjectEdgeModelSerialize>(
{} as subjectEdgeModelSerialize,
);
const [open, setOpen] = useState(false);
const [typeList, setTypeList] = useState([]);
const [singleTypeList, setSingleTypeList] = useState([]);
const [doubleTypeList, setDoubleTypeList] = useState([]);
const [tableData, setTableData] = useState([]);
const [formId, setFormId] = useState('');
const [circleColor, setCircleColor] = useState(0);
const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模
const [menuItems, setMenuItems] = useState<MenuProps['items']>([]);

const afterOpenChange = async () => {
if (!open) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_values, error] = await to(form.validateFields());
console.log(_values);

const fields = form.getFieldsValue();
console.log(fields);
// const out_parameters = JSON.stringify(fields.out_parameters);
// console.log('getFieldsValue', fields);

const res = {
...stagingItem,
...fields,

id: formId,
};

console.log('res', res);
onFormChange(res);
}
};
const onClose = () => {
setOpen(false);
};

useImperativeHandle(ref, () => ({
showDrawer(
model: subjectNodeModel,
params: subjectGlobalParam[],
parentNodes: INode[],
validate: boolean = false,
) {
try {
const edgeData: subjectEdgeModelSerialize = {
...model,
label: model.label,
id: model.id,
source: model.source,
labelCfg: model.labelCfg,
curveOffset: model.curveOffset,
curvePosition: model.curvePosition,
endPoint: model.endPoint,
sourceAnchor: model.sourceAnchor,
startPoint: model.startPoint,
style: model.style,
target: model.target,
type: model.type,
// color: JSON.parse(model.color),
// control_strategy: JSON.parse(model.control_strategy),
};
// console.log('model', edgeData);
// setStagingItem({
// ...edgeData,
// });
console.log(edgeData);
setCircleColor(model.color || 0);
form.resetFields();
form.setFieldsValue({
...edgeData,
});
console.log(form.getFieldsValue());
setTableData(model.properties);
setFormId(model.id);
console.log(form.getFieldsValue());

if (validate) {
form.validateFields();
}
} catch (error) {
console.error('JSON.parse error: ', error);
}
setOpen(true);

// 参数下拉菜单
// setMenuItems(createMenuItems(params, parentNodes));
},
close: () => {
onClose();
},

validateFields: async () => {
if (!open) {
return;
}
const [values, error] = await to(form.validateFields());
if (!error && values) {
return values;
} else {
form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' });
return Promise.reject(error);
}
},
}));

// 获取选择数据集、模型后面按钮 icon
const getSelectBtnIcon = (item: { item_type: string }) => {
const type = item.item_type;
if (type === 'code') {
return <KFIcon type="icon-xuanzedaimapeizhi" />;
}

let selectorType: ResourceSelectorType;
if (type === 'dataset') {
selectorType = ResourceSelectorType.Dataset;
} else if (type === 'model') {
selectorType = ResourceSelectorType.Model;
} else {
selectorType = ResourceSelectorType.Mirror;
}

return <KFIcon type={selectorTypeConfig[selectorType].buttonIcon} />;
};

// 参数回填
const handleParameterClick = (name: NamePath, value: any) => {
form.setFieldValue(name, value);
};

// form item label
const getLabel = (
item: { key: string; value: subjectNodeModelParameter },
namePrefix: string,
) => {
return item.value.type === 'select' ? (
item.value.label + '(' + item.key + ')'
) : (
<PropsLabel
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'}
onClick={(value) => {
handleParameterClick([namePrefix, item.key], {
...item.value,
value,
fromSelect: true,
showValue: value,
});
}}
/>
);
};

return (
<Drawer
title="关系"
placement="right"
rootStyle={{ marginTop: '52px' }}
getContainer={false}
closeIcon={false}
onClose={onClose}
afterOpenChange={afterOpenChange}
open={open}
width={500}
className={styles['subject-drawer']}
>
<Form
name="form"
form={form}
style={{
maxWidth: 600,
height: '100%',
}}
autoComplete="off"
scrollToFirstError
>
{/* <div className={styles['subject-drawer__title']}>
<SubAreaTitle
image={require('@/assets/img/static-message.png')}
title="基本信息"
></SubAreaTitle>
</div> */}
<Form.Item
label="关系名称"
name="label"
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
layout="horizontal"
rules={[
{
required: true,
message: '请输入概念名称',
},
]}
>
<Input placeholder="请输入概念名称" allowClear />
</Form.Item>
</Form>
</Drawer>
);
});

export default subjectNodeParameter;

+ 91
- 0
react-ui/src/pages/knowledge/components/SubjectNodeDrawer/index.less View File

@@ -0,0 +1,91 @@
.subject-drawer {
:global {
label {
width: 100%;

&::after {
display: none;
}
}
}

&__title {
display: flex;
align-items: center;
width: 100%;
height: 43px;
margin-bottom: 20px;
padding: 0 24px;
color: @text-color;
font-size: @font-size;
background: #f8fbff;
}

&__ref-row {
display: flex;
align-items: center;

&__select-button {
display: flex;
flex: none;
align-items: center;
justify-content: flex-start;
margin-left: 10px;
padding-right: 0;
padding-left: 0;
}
}
.subject-drawer{
height:58px;
background:#ffffff;
border:1px solid;
border-color:#e6e6e6;
border-radius:6px;
display: flex;
align-items: center;
&__left{
width: 40px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
&__circle{
width:17px;
height:17px;
border-radius: 50%;
background:#f98e1b;
}

}
&__slice{
width:0px;
height:57px;
border:1px solid;
border-color:#e6e6e6;
}
&__right{
padding: 0 10px;
display: flex;
align-items: center;
&__circle{
display: flex;
justify-content: center;
align-items: center;
width:18px;
height:18px;
background:#fff;
margin-right: 8px;
border-radius: 50%;
cursor: pointer;
border:1px solid;

&__center{
width: 14px;
height: 14px;
border-radius: 50%;
background-color: #164fff;
}
}
}
}
}

+ 635
- 0
react-ui/src/pages/knowledge/components/SubjectNodeDrawer/index.tsx View File

@@ -0,0 +1,635 @@
import KFIcon from '@/components/KFIcon';
import { useComputingResource } from '@/hooks/resource';
import { getSubjectType } from '@/services/subject';
import {
subjectGlobalParam,
subjectNodeModel,
subjectNodeModelParameter,
subjectNodeModelSerialize,
} from '@/types';
import { to } from '@/utils/promise';
import { INode } from '@antv/g6';
import { Button, Drawer, Form, Input, MenuProps, Select, Table, type TableProps } from 'antd';
import { NamePath } from 'antd/es/form/interface';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import PropsLabel from '../PropsLabel';
import { ResourceSelectorType, selectorTypeConfig } from '../ResourceSelectorModal';
import styles from './index.less';
const { TextArea } = Input;
export type ServiceData = {
name: string;
isMultivalued: string;
type: string;
flag: boolean;
};
type subjectNodeParameterProps = {
onFormChange: (data: subjectNodeModelSerialize) => void;
};

const subjectNodeParameter = forwardRef(({ onFormChange }: subjectNodeParameterProps, ref) => {
const isMultivaluedList = [
{ label: '单值', value: 'kg_entity_property_single_value' },
{ label: '双值', value: 'kg_entity_property_multi_value' },
];
const [form] = Form.useForm();
const [stagingItem, setStagingItem] = useState<subjectNodeModelSerialize>(
{} as subjectNodeModelSerialize,
);
const [open, setOpen] = useState(false);
const [typeList, setTypeList] = useState([]);
const [singleTypeList, setSingleTypeList] = useState([]);
const [doubleTypeList, setDoubleTypeList] = useState([]);
const [tableData, setTableData] = useState([]);
const [formId, setFormId] = useState('');
const [circleColor, setCircleColor] = useState(0);
const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模
const [menuItems, setMenuItems] = useState<MenuProps['items']>([]);
useEffect(() => {
getSubjectType('kg_entity_property_single_value').then((ret) => {
setSingleTypeList(ret.data);
});
getSubjectType('kg_entity_property_multi_value').then((ret) => {
setDoubleTypeList(ret.data);
});
}, []);
const columns: TableProps<ServiceData>['columns'] = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: '20%',
render: (text, record, index) => {
if (record.flag) {
return (
<Input
onChange={(e) => {
console.log(e, index, record);
const newData = [...tableData];
newData[index].name = e.target.value;

setTableData(newData);
}}
defaultValue={record.name}
autoFocus
/>
);
} else {
return record.name;
}
},
},

{
title: '单值/多值',
dataIndex: 'isMultivalued',
key: 'isMultivalued',
width: '25%',
render: (text, record, index) => {
if (record.flag) {
return (
<Select
allowClear
onChange={(e) => {
console.log(e, index, record);
const newData = [...tableData];
newData[index].isMultivalued = e;
setTableData(newData);
if (e == 'kg_entity_property_single_value') {
setTypeList(singleTypeList);
} else {
setTypeList(doubleTypeList);
}
// getSubjectType(e).then((ret) => {
// setTypeList(ret.data);
// });
}}
options={isMultivaluedList}
fieldNames={{ label: 'label', value: 'value' }}
optionFilterProp="name"
showSearch
/>
);
} else {
console.log(isMultivaluedList.filter((item) => item.value == record.isMultivalued)[0]);
return isMultivaluedList.filter((item) => item.value == record.isMultivalued)[0]?.label;
}
},
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: '30%',
render: (text, record, index) => {
if (record.flag) {
return (
<Select
allowClear
onChange={(e) => {
console.log(e, index, record);
const newData = [...tableData];
newData[index].type = e;

setTableData(newData);
}}
options={typeList}
fieldNames={{ label: 'dictLabel', value: 'dictValue' }}
optionFilterProp="name"
showSearch
/>
);
} else {
return record.isMultivalued && record.isMultivalued == 'kg_entity_property_single_value'
? singleTypeList.filter((item) => item.dictValue == record.type)[0]?.dictLabel
: doubleTypeList.filter((item) => item.dictValue == record.type)[0]?.dictLabel;
}
},
},
{
title: '操作',
dataIndex: 'operation',
width: '30%',
key: 'operation',
render: (_: any, record: any, index: any) =>
record.flag ? (
<div>
<Button
type="link"
size="small"
key="edit"
onClick={() => {
editTable(record, index);
}}
icon={<KFIcon type="icon-baocunbingfanhui" />}
></Button>
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-fuzhi" />}
onClick={() => {
cloneTable(record, index);
}}
></Button>
<Button
type="link"
size="small"
key="run"
onClick={() => {
deleteTable(record, index);
}}
icon={<KFIcon type="icon-shanchu" />}
></Button>
</div>
) : (
<div>
<Button
type="link"
size="small"
key="edit"
onClick={() => {
editTable(record, index);
}}
icon={<KFIcon type="icon-bianji" />}
></Button>
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-fuzhi" />}
onClick={() => {
cloneTable(record, index);
}}
></Button>
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => {
deleteTable(record, index);
}}
></Button>
</div>
),
},
];
const editTable = (val: any, index: any) => {
tableData[index] = {
...tableData[index],
flag: !val.flag,
};
const newTableData = JSON.parse(JSON.stringify(tableData));
setTableData(newTableData);
console.log(tableData);
};

const addTable = () => {
setTableData([
...tableData,
{
name: '',
isMultivalued: '',
type: '',
flag: false,
},
]);
};
const cloneTable = (val, index) => {
setTableData([...tableData, val]);
};

const deleteTable = (val, index) => {
tableData.splice(index, 1);
setTableData(JSON.parse(JSON.stringify(tableData)));
};
const afterOpenChange = async () => {
if (!open) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_values, error] = await to(form.validateFields());
console.log(_values);

const fields = form.getFieldsValue();
const label = fields.label;
const color = circleColor;
// const out_parameters = JSON.stringify(fields.out_parameters);
// console.log('getFieldsValue', fields);

const res = {
...stagingItem,
...fields,
properties: tableData,
label,
color,
id: formId,
};

console.log('res', res);
onFormChange(res);
}
};
const onClose = () => {
setOpen(false);
};

const changeColor = (val: any) => {
console.log(val);

setCircleColor(val);
form.setFieldsValue({
color: circleColor,
});
};

useImperativeHandle(ref, () => ({
showDrawer(
model: subjectNodeModel,
params: subjectGlobalParam[],
parentNodes: INode[],
validate: boolean = false,
) {
try {
const nodeData: subjectNodeModelSerialize = {
...model,
label: model.label,
id: model.id,
// color: JSON.parse(model.color),
// control_strategy: JSON.parse(model.control_strategy),
};
// console.log('model', nodeData);
// setStagingItem({
// ...nodeData,
// });
console.log(model);
setCircleColor(model.color || 0);
form.resetFields();
form.setFieldsValue({
...nodeData,
});
setTableData(model.properties);
setFormId(model.id);
console.log(form.getFieldsValue());

if (validate) {
form.validateFields();
}
} catch (error) {
console.error('JSON.parse error: ', error);
}
setOpen(true);

// 参数下拉菜单
// setMenuItems(createMenuItems(params, parentNodes));
},
close: () => {
onClose();
},

validateFields: async () => {
if (!open) {
return;
}
const [values, error] = await to(form.validateFields());
if (!error && values) {
return values;
} else {
form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' });
return Promise.reject(error);
}
},
}));

// ref 类型选择
// const selectRefData = (
// formItemName: NamePath,
// item: subjectNodeModelParameter | Pick<subjectNodeModelParameter, 'item_type'>,
// ) => {
// if (item.item_type === 'code') {
// selectCodeConfig(formItemName, item);
// } else {
// selectResource(formItemName, item);
// }
// };

// // 选择代码配置
// const selectCodeConfig = (
// formItemName: NamePath,
// item: subjectNodeModelParameter | Pick<subjectNodeModelParameter, 'item_type'>,
// ) => {
// const { close } = openAntdModal(CodeSelectorModal, {
// onOk: (res) => {
// if (res) {
// const value = JSON.stringify({
// id: res.id,
// name: res.code_repo_name,
// code_path: res.git_url,
// branch: res.git_branch,
// username: res.git_user_name,
// password: res.git_password,
// ssh_private_key: res.ssh_key,
// });
// form.setFieldValue(formItemName, {
// ...item,
// value,
// showValue: res.code_repo_name,
// fromSelect: true,
// });
// }
// close();
// },
// });
// };

// // 选择数据集、模型、镜像
// const selectResource = (
// formItemName: NamePath,
// item: subjectNodeModelParameter | Pick<subjectNodeModelParameter, 'item_type'>,
// ) => {
// let type: ResourceSelectorType;
// switch (item.item_type) {
// case 'dataset':
// type = ResourceSelectorType.Dataset;
// break;
// case 'model':
// type = ResourceSelectorType.Model;
// break;
// default:
// type = ResourceSelectorType.Mirror;
// break;
// }
// const fieldValue = form.getFieldValue(formItemName);
// const activeTab = fieldValue?.activeTab as CommonTabKeys | undefined;
// const expandedKeys = Array.isArray(fieldValue?.expandedKeys) ? fieldValue?.expandedKeys : [];
// const checkedKeys = Array.isArray(fieldValue?.checkedKeys) ? fieldValue?.checkedKeys : [];
// const { close } = openAntdModal(ResourceSelectorModal, {
// type,
// defaultExpandedKeys: expandedKeys,
// defaultCheckedKeys: checkedKeys,
// defaultActiveTab: activeTab,
// onOk: (res) => {
// // if (res) {
// // if (type === ResourceSelectorType.Mirror) {
// // const { activeTab, id, version, path } = res;
// // if (formItemName === 'image') {
// // // 单独的选择镜像
// // form.setFieldValue(formItemName, path);
// // } else {
// // // 输入参数选择镜像
// // form.setFieldValue(formItemName, {
// // ...item,
// // value: path,
// // showValue: path,
// // fromSelect: true,
// // activeTab,
// // expandedKeys: [id],
// // checkedKeys: [`${id}-${version}`],
// // });
// // }
// // } else {
// // const { activeTab, id, name, version, path, identifier, owner } = res;
// // const value = JSON.stringify({
// // id,
// // name,
// // version,
// // path,
// // identifier,
// // owner,
// // });
// // const showValue = `${name}:${version}`;
// // form.setFieldValue(formItemName, {
// // ...item,
// // value,
// // showValue,
// // fromSelect: true,
// // activeTab,
// // expandedKeys: [id],
// // checkedKeys: [`${id}-${version}`],
// // });
// // }
// // } else {
// // if (type === ResourceSelectorType.Mirror && formItemName === 'image') {
// // form.setFieldValue(formItemName, undefined);
// // } else {
// // form.setFieldValue(formItemName, {
// // ...item,
// // value: undefined,
// // showValue: undefined,
// // fromSelect: false,
// // activeTab: undefined,
// // expandedKeys: [],
// // checkedKeys: [],
// // });
// // }
// // }
// // form.validateFields([formItemName]);
// // close();
// },
// });
// };

// 获取选择数据集、模型后面按钮 icon
const getSelectBtnIcon = (item: { item_type: string }) => {
const type = item.item_type;
if (type === 'code') {
return <KFIcon type="icon-xuanzedaimapeizhi" />;
}

let selectorType: ResourceSelectorType;
if (type === 'dataset') {
selectorType = ResourceSelectorType.Dataset;
} else if (type === 'model') {
selectorType = ResourceSelectorType.Model;
} else {
selectorType = ResourceSelectorType.Mirror;
}

return <KFIcon type={selectorTypeConfig[selectorType].buttonIcon} />;
};

// 参数回填
const handleParameterClick = (name: NamePath, value: any) => {
form.setFieldValue(name, value);
};

// form item label
const getLabel = (
item: { key: string; value: subjectNodeModelParameter },
namePrefix: string,
) => {
return item.value.type === 'select' ? (
item.value.label + '(' + item.key + ')'
) : (
<PropsLabel
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'}
onClick={(value) => {
handleParameterClick([namePrefix, item.key], {
...item.value,
value,
fromSelect: true,
showValue: value,
});
}}
/>
);
};

return (
<Drawer
title="概念"
placement="right"
rootStyle={{ marginTop: '52px' }}
getContainer={false}
closeIcon={false}
onClose={onClose}
afterOpenChange={afterOpenChange}
open={open}
width={500}
className={styles['subject-drawer']}
>
<Form
name="form"
form={form}
style={{
maxWidth: 600,
height: '100%',
}}
autoComplete="off"
scrollToFirstError
>
{/* <div className={styles['subject-drawer__title']}>
<SubAreaTitle
image={require('@/assets/img/static-message.png')}
title="基本信息"
></SubAreaTitle>
</div> */}
<Form.Item
label="概念名称"
name="label"
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
layout="horizontal"
rules={[
{
required: true,
message: '请输入概念名称',
},
]}
>
<Input placeholder="请输入概念名称" allowClear />
</Form.Item>
<Form.Item
label="ICON"
name="color"
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
layout="horizontal"
>
<div className={styles['subject-drawer']}>
<div className={styles['subject-drawer__left']}>
<div
className={styles['subject-drawer__left__circle']}
style={{ backgroundColor: circleColor == 1 ? '#1664ff' : '#f98e1b' }}
></div>
</div>
<div className={styles['subject-drawer__slice']}></div>
<div className={styles['subject-drawer__right']}>
<div
onClick={() => {
changeColor(1);
}}
style={{ borderColor: circleColor == 1 ? '#1664ff' : 'transparent' }}
className={styles['subject-drawer__right__circle']}
>
<div className={styles['subject-drawer__right__circle__center']}></div>
</div>
<div
onClick={() => {
changeColor(2);
}}
style={{
borderColor: circleColor == 2 ? '#1664ff' : 'transparent',
}}
className={styles['subject-drawer__right__circle']}
>
<div
className={styles['subject-drawer__right__circle__center']}
style={{ background: '#f98e1b' }}
></div>
</div>
</div>
</div>
</Form.Item>
<Form.Item
label="属性"
// name="properties"
layout="vertical"
style={{ position: 'relative' }}
labelCol={{ span: 24 }}
wrapperCol={{ span: 24 }}
>
<div style={{ position: 'absolute', width: '100%' }}>
<Table
dataSource={tableData}
columns={columns}
pagination={false}
// onChange={handleTableChange}
rowKey="id"
/>
<Button
type="link"
size="small"
key="edit"
onClick={addTable}
icon={<KFIcon type="icon-xinjian2" />}
>
增加属性
</Button>
</div>
</Form.Item>
</Form>
</Drawer>
);
});

export default subjectNodeParameter;

+ 0
- 0
react-ui/src/pages/knowledge/components/subjecDetailEdit/index.less View File


+ 144
- 0
react-ui/src/pages/knowledge/components/subjecDetailEdit/index.tsx View File

@@ -0,0 +1,144 @@
import KFModal from '@/components/KFModal';
import { AvailableRange } from '@/enums';
import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { editKnowledgeSubject } from '@/services/subject';
import { to } from '@/utils/promise';
import { Form, Input, message, type FormRule, type ModalProps } from 'antd';
import { useMemo, useState } from 'react';

export enum VerifyMode {
Password = 0, // 用户名密码
SSH = 1, // SSH Key
}

export enum OperationType {
Create = 0, // 新建
Update = 1, // 更新
}

type FormData = Partial<CodeConfigData>;

interface AddCodeConfigModalProps extends Omit<ModalProps, 'onOk'> {
opType: OperationType;
codeConfigData?: CodeConfigData;
onOk: () => void;
}

function AddCodeConfigModal({ opType, codeConfigData, onOk, ...rest }: AddCodeConfigModalProps) {
const [form] = Form.useForm();
const [typeList, setTypeList] = useState([]);
const isPublic = Form.useWatch('code_repo_vis', form) === AvailableRange.Public;

const urlExample = useMemo(
() =>
isPublic
? 'https://gitlink.org.cn/ci4s/ci4sManagement-cloud.git'
: 'git@code.gitlink.org.cn:ci4s/ci4sManagement-cloud.git',
[isPublic],
);

// /^(git@[\w.-]+:[\w./-]+\.git)$/
const urlRules: FormRule[] = useMemo(
() =>
isPublic
? [
{
type: 'url',
message: '请输入正确的 Git 地址',
},
]
: ([] as FormRule[]),
[isPublic],
);

// 创建
const createCodeConfig = async (formData: FormData) => {
console.log(OperationType);

const params: FormData & { id?: number } = {
...formData,
};
// 清除多余的信息
// if (formData.code_repo_vis === AvailableRange.Public) {
// omit(params, ['verify_mode', 'git_user_name', 'git_password', 'ssh_key']);
// }
// if (formData.verify_mode === VerifyMode.Password) {
// omit(params, ['ssh_key']);
// } else if (formData.verify_mode === VerifyMode.SSH) {
// omit(params, ['git_user_name', 'git_password']);
// }
params.id = codeConfigData?.id;
const request = editKnowledgeSubject;
const [res] = await to(request(params));
if (res) {
message.success('修改成功');
onOk?.();
}
};

// 提交
const onFinish = (formData: FormData) => {
createCodeConfig(formData);
};

// 设置初始值
const initialValues: FormData = codeConfigData ?? {
code_repo_vis: AvailableRange.Public,
verify_mode: VerifyMode.Password,
};
if (initialValues.verify_mode === undefined || initialValues.verify_mode === null) {
initialValues.verify_mode = VerifyMode.Password;
}

return (
<KFModal
{...rest}
title={'修改主体'}
image={require('@/assets/img/create-experiment.png')}
width={825}
okButtonProps={{
htmlType: 'submit',
form: 'form',
}}
destroyOnClose
>
<Form
name="form"
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={initialValues}
autoComplete="off"
>
<Form.Item
label="主体名称"
name="name"
required
rules={[
{
required: true,
message: '请输入主体名称',
},
]}
>
<Input placeholder="请输入主体名称" showCount allowClear maxLength={64} />
</Form.Item>
<Form.Item
label="主体描述"
name="description"
required
rules={[
{
required: true,
message: '请输入主体描述',
},
]}
>
<Input placeholder="请输入主体描述" showCount allowClear maxLength={64} />
</Form.Item>
</Form>
</KFModal>
);
}

export default AddCodeConfigModal;

+ 0
- 0
react-ui/src/pages/knowledge/config.tsx View File


+ 93
- 0
react-ui/src/services/graph/index.ts View File

@@ -0,0 +1,93 @@
import { request } from '@umijs/max';
import mock from 'mock/knowledge'
// // 添加图谱
export function addKnowledgeGraph(data:any) {
return request(`/api/kg`, {
method: 'POST',
data,
});
}
// 修改图谱
export function editKnowledgeGraph(data:any) {
return request(`/api/kg`, {
method: 'PUT',
data,
});
}

// 获取图谱
export function getKnowledgeGraph() {
return request(`/api/kg/list`, {
method: 'GET',
});
}
// 删除图谱
export function deleteKnowledgeGraph(data:any) {
return request(`/api/kg/${data}`, {
method: 'DELETE',
});
}
// 查看图谱详情
export function getKnowledgeGraphById(data:any) {
return request(`/api/kg`, {
method: 'GET',
params:data
});
}
// 查看图谱版本列表
export function getKnowledgeGraphVersionList(data:any) {
return request(`/api/kg/version/list`, {
method: 'GET',
params:data
});
}
// // 查看元素配置列表
export function getConfigurationList(data:any) {
return request(`/api/kg/configuration/list`, {
method: 'GET',
params:data
});
}
// 获取关联实体
export function getEntityGraph() {
return request(`/api/kg/ontology/getBasicInfo`, {
method: 'GET',
data:{}
});
}
// 图谱版本新增

export function postVersion(data:any) {
return request(`/api/kg/version`, {
method: 'POST',
data
});
}
// 删除图谱版本
export function deleteKnowledgeGraphVersion(data:any) {
return request(`/api/mmp//kg/version/${data}`, {
method: 'DELETE',
});
}
// 增量更新
export function incrementalUpdate(data:any) {
return request(`/api/kg/version/incrementalUpdate`, {
method: 'POST',
data
});
}
// 全量更新
export function fullUpdate(data:any) {
return request(`/api/kg/version/fullUpdate`, {
method: 'POST',
data
});
}
// 全量更新
export function rollback(data:any) {
return request(`/api/kg/version/rollback`, {
method: 'POST',
data
});
}


+ 64
- 0
react-ui/src/services/subject/index.ts View File

@@ -0,0 +1,64 @@
import { request } from '@umijs/max';
import mock from 'mock/knowledge'
// // 添加主体
export function addKnowledgeSubject(data:any) {
return request(`/api/mmp//kg/ontology`, {
method: 'POST',
data,
});
}
// 修改主体
export function editKnowledgeGraph(data:any) {
return request(`/api/kg`, {
method: 'PUT',
data,
});
}

// 获取主体
export function getKnowledgeSubject(data:any) {
return request(`/api/kg/ontology/list`, {
method: 'POST',
data:data,
});
}
// 删除单个主体
export function deleteSubject(data:any) {
return request(`/api/kg/ontology/${data}`, {
method: 'DELETE',
});
}
// 查看主体详情
export function getKnowledgeSubjectById(data:any) {
return request(`/api/kg/ontology/${data}`, {
method: 'GET',

});
}
// 查看主体版本列表
export function getKnowledgeGraphVersionList(data:any) {
return request(`/api/kg/version/list`, {
method: 'GET',
params:data
});
}
// // 查看元素配置列表
export function getConfigurationList(data:any) {
return request(`/api/kg/configuration/list`, {
method: 'GET',
params:data
});
}
// 修改基本主体
export function editKnowledgeSubject(data:any) {
return request(`/api/kg/ontology/info`, {
method: 'PUT',
data,
});
}
// 修改基本主体
export function getSubjectType(dictcode:any) {
return request(`/api/system/dict/data/type/${dictcode}`, {
method: 'get',
});
}

+ 15
- 0
react-ui/src/types.ts View File

@@ -114,3 +114,18 @@ export type ComputingResource = {
standard: string;
create_by: string;
};
// 主体边规格
export type subjectEdgeModelSerialize = {
label:string,
id: number,
source: string,
labelCfg: string,
curveOffset: string,
curvePosition: string,
endPoint: string,
sourceAnchor: string,
startPoint: string,
style: string,
target: string,
type: string,
};

Loading…
Cancel
Save