diff --git a/web/packages/tca-analysis/src/components/delete-modal/index.tsx b/web/packages/tca-analysis/src/components/delete-modal/index.tsx new file mode 100644 index 000000000..f2e3529a3 --- /dev/null +++ b/web/packages/tca-analysis/src/components/delete-modal/index.tsx @@ -0,0 +1,106 @@ +// Copyright (c) 2021-2022 THL A29 Limited +// +// This source code file is made available under MIT License +// See LICENSE for details +// ============================================================================== + +/** + * 确认删除操作弹框 + */ + +import React, { useEffect, useState } from 'react'; +import { Modal, Form, Input, message, Button } from 'coding-oa-uikit'; +import { t } from '@src/i18n/i18next'; + +import s from './style.scss'; + +interface DeleteModalProps { + actionType: string; + objectType: string; + confirmName: string; + visible: boolean; + addtionInfo?: string; + onCancel: () => void; + onOk: () => void; +} + +const DeleteModal = ({ actionType, objectType, confirmName, addtionInfo='', visible, onCancel, onOk }: DeleteModalProps) => { + const [form] = Form.useForm(); + const [confirmed, setConfirmed] = useState(true); + + useEffect(() => { + visible && form.resetFields(); + visible && setConfirmed(false); + }, [visible]); + + /** + * 表单提交操作 + * @param formData 参数 + */ + const onSubmitHandle = () => { + form.validateFields().then((formData) => { + if (formData?.confirm === confirmName) { + onOk(); + } else { + message.error(t('验证失败,请重新输入')); + } + }); + }; + + const checkConfirm = (changedValues: any) => { + if (changedValues?.confirm === confirmName) { + setConfirmed(true); + } else { + setConfirmed(false); + } + }; + + return ( + + {t('确认')}{actionType} + , + , + ]} + > +

+ {t('您正在')}{actionType}{objectType} {confirmName}{' '}
+

+ {addtionInfo &&

{addtionInfo}

} +

{t(`为确认${actionType}操作,请输入您要${actionType}的`)}{objectType}

+
+ + + +
+
+ ); +}; + +export default DeleteModal; diff --git a/web/packages/tca-analysis/src/components/delete-modal/style.scss b/web/packages/tca-analysis/src/components/delete-modal/style.scss new file mode 100644 index 000000000..59d9b1e32 --- /dev/null +++ b/web/packages/tca-analysis/src/components/delete-modal/style.scss @@ -0,0 +1,21 @@ +.delete-modal { + + .warning-message { + margin-bottom: 20px; + } + + .confirm-message { + margin-bottom: 8px; + font-weight: 600; + } + + .confirm-text { + background-color: #ebedf1; + font-weight: 500; + padding: 0 6px; + } + + .confirm-input { + margin-bottom: 0px; + } +} \ No newline at end of file diff --git a/web/packages/tca-analysis/src/modules/project-team/overview/index.tsx b/web/packages/tca-analysis/src/modules/project-team/overview/index.tsx index 4784ca61a..9f00b7832 100644 --- a/web/packages/tca-analysis/src/modules/project-team/overview/index.tsx +++ b/web/packages/tca-analysis/src/modules/project-team/overview/index.tsx @@ -5,14 +5,17 @@ // ============================================================================== import React, { useState, useEffect } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, useHistory } from 'react-router-dom'; import { Form, Button, Input, message } from 'coding-oa-uikit'; -import { pick } from 'lodash'; +import { pick, get, find } from 'lodash'; +import { useSelector } from 'react-redux'; // 项目内 +import { getProjectListRouter, getProjectOverviewRouter } from '@src/utils/getRoutePath'; import { t } from '@src/i18n/i18next'; import { formatDateTime, getUserName } from '@src/utils'; -import { getProjectTeam, putProjectTeam } from '@src/services/common'; +import { getProjectTeam, putProjectTeam, disableProject} from '@src/services/common'; +import DeleteModal from '@src/components/delete-modal'; const layout = { labelCol: { span: 6 }, @@ -23,6 +26,14 @@ const Overview = () => { const [team, setTeam] = useState({}); const [edit, setEdit] = useState(false); const { org_sid: orgSid, team_name: teamName }: any = useParams(); + // 判断是否有权限删除团队项目 + const history: any = useHistory(); + const APP = useSelector((state: any) => state.APP); + const isSuperuser = get(APP, 'user.is_superuser', false); // 当前用户是否是超级管理员 + const userName = get(APP, 'user.username', null); + const isAdmin = !!find(team?.admins, { username: userName }); // 当前用户是否是项目管理员 + const deletable = isAdmin || isSuperuser; // 删除权限 + const [deleteVisible, setDeleteVisible] = useState(false); // 重置 const onReset = () => { @@ -39,6 +50,9 @@ const Overview = () => { message.success(t('项目信息更新成功')); setTeam(response); onReset(); + if (values.name !== teamName) { + history.replace(getProjectOverviewRouter(orgSid, values.name)); + } }); }; @@ -55,8 +69,28 @@ const Overview = () => { } }, [orgSid, teamName]); + const handleDeleteTeam = () => { + disableProject(orgSid, teamName, {status: 2}).then(() => { + message.success('项目已禁用'); + history.push(getProjectListRouter(orgSid)); + }).finally(() => setDeleteVisible(false)); + }; + + const onDelete = () => { + setDeleteVisible(true); + }; + return (
+ setDeleteVisible(false)} + onOk={handleDeleteTeam} + />

{t('项目概览')}

{ initialValues={team} onFinish={values => onFinish(values)} > - + {team.name} { {t('编辑')} )} + {deletable && }
diff --git a/web/packages/tca-analysis/src/modules/projects/project/project-list.tsx b/web/packages/tca-analysis/src/modules/projects/project/project-list.tsx index d0dfd33ae..6e7f2ebbb 100644 --- a/web/packages/tca-analysis/src/modules/projects/project/project-list.tsx +++ b/web/packages/tca-analysis/src/modules/projects/project/project-list.tsx @@ -12,9 +12,10 @@ import React, { useEffect, useState } from 'react'; import { Link, useHistory, useParams } from 'react-router-dom'; -import { Table, Tooltip, Button, Input } from 'coding-oa-uikit'; -import { pickBy, isNumber, get, toNumber } from 'lodash'; +import { Table, Tooltip, Button, Input, Menu, Dropdown, message } from 'coding-oa-uikit'; +import { pickBy, isNumber, get, toNumber, find } from 'lodash'; import qs from 'qs'; +import { useSelector } from 'react-redux'; import SelectDropdown from '../../../components/select-dropdown'; import QuestionCircle from 'coding-oa-uikit/lib/icon/QuestionCircle'; @@ -23,10 +24,12 @@ import { useStateStore } from '@src/context/store'; import { DEFAULT_PAGER } from '@src/common/constants'; import { useQuery } from '@src/utils/hooks'; import { getProjectRouter, getSchemeRouter } from '@src/utils/getRoutePath'; -import { getProjects } from '@src/services/projects'; +import { getProjects, delProject } from '@src/services/projects'; +import { getMembers } from '@src/services/common'; import ScanModal from './scan-modal'; import NewProjectModal from './new-project-modal'; +import DeleteModal from '@src/components/delete-modal'; import style from '../style.scss'; @@ -51,6 +54,17 @@ const ProjectList = (props: ProjectListProps) => { const [visible, setVisible] = useState(false); const [projectId, setProjectId] = useState() as any; const [createProjectVsb, setCreateProjectVsb] = useState(false); + const [curProjId, setCurProjId] = useState(null); + const [reload, setReload] = useState(false); + + // 判断是否有权限删除分支项目 + const APP = useSelector((state: any) => state.APP); + const isSuperuser = get(APP, 'user.is_superuser', false); // 当前用户是否是超级管理员 + const userName = get(APP, 'user.username', null); + const [admins, setAdmins] = useState([]); + const isAdmin = !!find(admins, { username: userName }); // 当前用户是否是代码库管理员 + const deletable = isAdmin || isSuperuser; // 删除权限 + const [deleteVisible, setDeleteVisible] = useState(false); const [searchParams, setSearchParams] = useState({ scan_scheme: query.get('scan_scheme') || '', @@ -67,8 +81,18 @@ const ProjectList = (props: ProjectListProps) => { pager.pageStart, searchParams.scan_scheme, searchParams.branch_or_scheme, + reload, ]); + useEffect(() => { + // 获取代码库成员 + if (repoId) { + getMembers(orgSid, teamName, repoId).then((response) => { + setAdmins(response.admins); + }); + } + }, [repoId]); + const getListData = (limit: number = pageSize, offset: number = pageStart) => { const params = { limit, @@ -104,6 +128,21 @@ const ProjectList = (props: ProjectListProps) => { setPager(DEFAULT_PAGER); }; + const onDeleteProject = (id: number) => { + setCurProjId(id); + setDeleteVisible(true); + }; + + const handleDeleteProject = () => { + delProject(orgSid, teamName, repoId, curProjId).then(() => { + message.success('已删除分支项目'); + setReload(!reload); + }).finally(() => { + setDeleteVisible(false); + setCurProjId(null); + }); + }; + return (
@@ -137,9 +176,8 @@ const ProjectList = (props: ProjectListProps) => { onClick={() => setCreateProjectVsb(true)} > 添加分支项目 - +
- { title="操作" dataIndex="id" width={240} - render={id => ( - <> + render={ id => { + const menu = ( + + + + 分支概览 + + + + + 分析历史 + + + {deletable && + onDeleteProject(id)} + > + 删除项目 + + } + + ); + return ( + <> { }} > 启动分析 - - - 分支概览 - - - 分析历史 - - - )} + + + + 更多操作 + + + + ); + } + } />
{ // getSchemes(branch); }} /> + setDeleteVisible(false)} + onOk={handleDeleteProject} + />
); }; diff --git a/web/packages/tca-analysis/src/modules/projects/style.scss b/web/packages/tca-analysis/src/modules/projects/style.scss index 5c6972b52..08e53ade3 100644 --- a/web/packages/tca-analysis/src/modules/projects/style.scss +++ b/web/packages/tca-analysis/src/modules/projects/style.scss @@ -9,12 +9,12 @@ :global(.ant-table-tbody) { &>tr { - td>a.link-name { + td>.link-name { color: $grey-8; } &:hover { - td>a.link-name { + td>.link-name { color: $blue-5 !important; } } diff --git a/web/packages/tca-analysis/src/modules/repos/create.tsx b/web/packages/tca-analysis/src/modules/repos/create.tsx index 43f8aaf27..7cacf7369 100644 --- a/web/packages/tca-analysis/src/modules/repos/create.tsx +++ b/web/packages/tca-analysis/src/modules/repos/create.tsx @@ -203,16 +203,15 @@ const Create = () => { ))} - @@ -222,6 +221,27 @@ const Create = () => {

+ prevValues.scm_auth_id !== curValues.scm_auth_id} + > + {({ getFieldValue }) => { + const scmAuth = getFieldValue('scm_auth_id'); + return scmAuth?.startsWith('ssh_token') && ( + <> + + + + + ); + }} + ; @@ -51,81 +56,122 @@ const RepoList = ({ repos }: IProps) => { const repoId = toNumber(params.repoId); const { org_sid: orgSid, team_name: teamName }: any = params; const history = useHistory(); + const dispatch = useDispatchStore(); const { curRepo, curRepoMember } = useInitRepo(orgSid, teamName, repoId); const { admins = [], users = [] } = curRepoMember; const tabValue = getTabValue(); + // 判断是否有权限删除代码库 + const APP = useSelector((state: any) => state.APP); + const isSuperuser = get(APP, 'user.is_superuser', false); // 当前用户是否是超级管理员 + const userName = get(APP, 'user.username', null); + const isAdmin = !!find(admins, { username: userName }); // 当前用户是否是代码库管理员 + const deletable = isAdmin || isSuperuser; // 删除权限 + const [deleteVisible, setDeleteVisible] = useState(false); // tab 切换跳转路由 const onTabChange = (key: string) => { history.push(getRepoRouter(orgSid, teamName, repoId, key)); }; + const onDeleteRepo = () => { + setDeleteVisible(true); + }; + + const handleDeleteRepo = () => { + delRepo(orgSid, teamName, repoId).then(() => { + message.success('已删除代码库'); + remove(repos, (item: any) => item?.id === repoId); + const firstRepoId = isEmpty(repos) ? null : repos[0].id; + dispatch({ + type: SET_REPOS, + payload: repos, + }); + if (firstRepoId) { + history.push(getRepoRouter(orgSid, teamName, firstRepoId)); + } else { + history.push(getReposRouter(orgSid, teamName)); + } + }).finally(() => { + setDeleteVisible(false); + }); + }; + return ( - - - - {curRepo ? ( - <> -
-
- {curRepo.name} - -
-
- {curRepo.scm_url} - -
-
- onTabChange(key)} - className={s.tabs} - > - - - - - - - - - - - - ) : ( -
- - {t('正在加载')}... -
- )} - -
+ + setDeleteVisible(false)} + onOk={handleDeleteRepo} + /> + + + {curRepo ? ( + <> +
+
+ {curRepo.name} + +
+
+ {curRepo.scm_url} + +
+
+ onTabChange(key)} + className={s.tabs} + > + + + + + + + + + + + + ) : ( +
+ + {t('正在加载')}... +
+ )} + +
); }; diff --git a/web/packages/tca-analysis/src/modules/repos/repo-list/tabs/overview.tsx b/web/packages/tca-analysis/src/modules/repos/repo-list/tabs/overview.tsx index c06b2431b..edfaca826 100644 --- a/web/packages/tca-analysis/src/modules/repos/repo-list/tabs/overview.tsx +++ b/web/packages/tca-analysis/src/modules/repos/repo-list/tabs/overview.tsx @@ -4,7 +4,7 @@ // See LICENSE for details // ============================================================================== -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Form, Input, Button, message, Avatar } from 'coding-oa-uikit'; import UserIcon from 'coding-oa-uikit/lib/icon/User'; import { merge } from 'lodash'; @@ -20,13 +20,15 @@ interface IProps { orgSid: string; teamName: string; repoId: number; + deletable: boolean; + onDelete: () => void; } const layout = { labelCol: { span: 4 }, }; -const Overview = ({ curRepo, orgSid, teamName, repoId }: IProps) => { +const Overview = ({ curRepo, orgSid, teamName, repoId, deletable, onDelete }: IProps) => { const [form] = Form.useForm(); const [edit, setEdit] = useState(false); const dispatch = useDispatchStore(); @@ -38,10 +40,9 @@ const Overview = ({ curRepo, orgSid, teamName, repoId }: IProps) => { form.resetFields(); }; - const onFinish = (name: string) => { - putRepo(orgSid, teamName, repoId, merge(curRepo, { name })).then((response) => { + const onFinish = (values: any) => { + putRepo(orgSid, teamName, repoId, merge(curRepo, values)).then((response) => { message.success('仓库信息已更新'); - onReset(); dispatch({ type: SET_CUR_REPO, payload: response, @@ -49,64 +50,74 @@ const Overview = ({ curRepo, orgSid, teamName, repoId }: IProps) => { }); }; + useEffect(() => { + onReset(); + }, [curRepo]); + return ( -
onFinish(values.name)} - > - - {curRepo.scm_url} - - - {edit ? : {curRepo.name}} - - - <> - } - /> - - {creatorInfo.nickname} - - - + onFinish(values)} + > + + {curRepo.scm_url} + + + {edit ? : {curRepo.ssh_url}} + + + {edit ? : {curRepo.name}} + + + <> + } + /> + + {creatorInfo.nickname} + + + - - {formatDateTime(curRepo.created_time)} - + + {formatDateTime(curRepo.created_time)} + -
- {edit ? ( - <> - - - - ) : ( - - )} -
-
+
+ {edit ? ( + <> + + + + ) : ( + + )} + {deletable && } +
+ ); }; export default Overview; diff --git a/web/packages/tca-analysis/src/services/common.ts b/web/packages/tca-analysis/src/services/common.ts index 8009bd2c5..a3f8f0671 100644 --- a/web/packages/tca-analysis/src/services/common.ts +++ b/web/packages/tca-analysis/src/services/common.ts @@ -21,7 +21,6 @@ export const getMainBaseURL = (org_sid: string, team_name: string) => `${MAIN_SE export const getAnalysisBaseURL = (org_sid: string, team_name: string) => `${ANALYSIS_SERVER_API}${getBaseURL(org_sid, team_name)}`; - /** * 获取代码库列表 * @param query @@ -85,3 +84,11 @@ export const delProjectTeamMember = (orgSid: string, teamName: string, role: num * @param orgSid */ export const getOrgMembers = (orgSid: string) => get(`${MAIN_SERVER_API}/orgs/${orgSid}/memberconf/`); + +/** + * 禁用团队项目 + * @param orgSid + * @param teamName + * @param params + */ + export const disableProject = (orgSid: string, teamName: string, params: any) => put(`${getMainBaseURL(orgSid, teamName)}/status/`, params); diff --git a/web/packages/tca-analysis/src/services/projects.ts b/web/packages/tca-analysis/src/services/projects.ts index 1bc0205d5..29628fdc7 100644 --- a/web/packages/tca-analysis/src/services/projects.ts +++ b/web/packages/tca-analysis/src/services/projects.ts @@ -9,7 +9,7 @@ * author luochunlan@coding.net * create at 2020-10-23 */ -import { get, post, put, getFile, postFile } from './index'; +import { get, post, put, del, getFile, postFile } from './index'; import { MAIN_SERVER, MAIN_SERVER_API, ANALYSIS_SERVER_API, getMainBaseURL, getAnalysisBaseURL } from './common'; const getProjectBaseURL = (org_sid: string, team_name: string, repoId: string | number, projectId: number) => `${getMainBaseURL(org_sid, team_name)}/repos/${repoId}/projects/${projectId}`; @@ -45,6 +45,13 @@ export const createJob = (org_sid: string, team_name: string, repoId: string | n */ export const getProjectDetail = (org_sid: string, team_name: string, repoId: string | number, projectId: string | number) => get(`${getMainBaseURL(org_sid, team_name)}/repos/${repoId}/projects/${projectId}/`); +/** + * 获取分支项目信息 + * @param repoId - 代码库ID + * @param projectId - 项目ID + */ + export const delProject = (org_sid: string, team_name: string, repoId: string | number, projectId: string | number) => del(`${getMainBaseURL(org_sid, team_name)}/repos/${repoId}/projects/${projectId}/`); + /** * 获取指定代码库下与 CodeDog 关联的分支 * @param repoId - 代码库ID diff --git a/web/packages/tca-analysis/src/services/repos.ts b/web/packages/tca-analysis/src/services/repos.ts index c3453ad09..0794d83b7 100644 --- a/web/packages/tca-analysis/src/services/repos.ts +++ b/web/packages/tca-analysis/src/services/repos.ts @@ -9,7 +9,7 @@ * author luochunlan@coding.net * create at 2020-10-23 */ -import { get, post, put } from './index'; +import { get, post, put, del } from './index'; import { MAIN_SERVER_API, getMainBaseURL } from './common'; /** @@ -58,7 +58,6 @@ export const getRepo = (orgSid: string, teamName: string, repoId: any) => get(`$ */ export const putRepo = (orgSid: string, teamName: string, repoId: any, data: any) => put(`${getMainBaseURL(orgSid, teamName)}/repos/${repoId}/`, data); - /** * 添加代码库成员 * @param orgSid @@ -68,3 +67,9 @@ export const putRepo = (orgSid: string, teamName: string, repoId: any, data: any * @returns */ export const postRepoMembers = (orgSid: string, teamName: string, repoId: any, data: any) => post(`${getMainBaseURL(orgSid, teamName)}/repos/${repoId}/memberconf/`, data); + +/** + * 删除代码库 + * @param repoId: 代码库id + */ + export const delRepo = (orgSid: string, teamName: string, repoId: any) => del(`${getMainBaseURL(orgSid, teamName)}/repos/${repoId}/`); diff --git a/web/packages/tca-analysis/src/utils/getRoutePath.ts b/web/packages/tca-analysis/src/utils/getRoutePath.ts index e083deb62..c053b596a 100644 --- a/web/packages/tca-analysis/src/utils/getRoutePath.ts +++ b/web/packages/tca-analysis/src/utils/getRoutePath.ts @@ -6,11 +6,23 @@ /** - * 获取基础路由前缀 + * 获取项目列表路由地址 + * @param org_sid + */ + export const getProjectListRouter = (org_sid: string) => `/t/${org_sid}/projects`; + + /** + * 获取项目概览路由地址 * @param org_sid * @param name */ + export const getProjectOverviewRouter = (org_sid: string, name: string) => `/t/${org_sid}/p/${name}/profile`; +/** + * 获取基础路由前缀 + * @param org_sid + * @param name + */ export const getBaseRouter = (org_sid: string, name: string) => `/t/${org_sid}/p/${name}`; /** diff --git a/web/packages/tca-layout/src/components/delete-modal/index.tsx b/web/packages/tca-layout/src/components/delete-modal/index.tsx new file mode 100644 index 000000000..f2e3529a3 --- /dev/null +++ b/web/packages/tca-layout/src/components/delete-modal/index.tsx @@ -0,0 +1,106 @@ +// Copyright (c) 2021-2022 THL A29 Limited +// +// This source code file is made available under MIT License +// See LICENSE for details +// ============================================================================== + +/** + * 确认删除操作弹框 + */ + +import React, { useEffect, useState } from 'react'; +import { Modal, Form, Input, message, Button } from 'coding-oa-uikit'; +import { t } from '@src/i18n/i18next'; + +import s from './style.scss'; + +interface DeleteModalProps { + actionType: string; + objectType: string; + confirmName: string; + visible: boolean; + addtionInfo?: string; + onCancel: () => void; + onOk: () => void; +} + +const DeleteModal = ({ actionType, objectType, confirmName, addtionInfo='', visible, onCancel, onOk }: DeleteModalProps) => { + const [form] = Form.useForm(); + const [confirmed, setConfirmed] = useState(true); + + useEffect(() => { + visible && form.resetFields(); + visible && setConfirmed(false); + }, [visible]); + + /** + * 表单提交操作 + * @param formData 参数 + */ + const onSubmitHandle = () => { + form.validateFields().then((formData) => { + if (formData?.confirm === confirmName) { + onOk(); + } else { + message.error(t('验证失败,请重新输入')); + } + }); + }; + + const checkConfirm = (changedValues: any) => { + if (changedValues?.confirm === confirmName) { + setConfirmed(true); + } else { + setConfirmed(false); + } + }; + + return ( + + {t('确认')}{actionType} + , + , + ]} + > +

+ {t('您正在')}{actionType}{objectType} {confirmName}{' '}
+

+ {addtionInfo &&

{addtionInfo}

} +

{t(`为确认${actionType}操作,请输入您要${actionType}的`)}{objectType}

+
+ + + +
+
+ ); +}; + +export default DeleteModal; diff --git a/web/packages/tca-layout/src/components/delete-modal/style.scss b/web/packages/tca-layout/src/components/delete-modal/style.scss new file mode 100644 index 000000000..59d9b1e32 --- /dev/null +++ b/web/packages/tca-layout/src/components/delete-modal/style.scss @@ -0,0 +1,21 @@ +.delete-modal { + + .warning-message { + margin-bottom: 20px; + } + + .confirm-message { + margin-bottom: 8px; + font-weight: 600; + } + + .confirm-text { + background-color: #ebedf1; + font-weight: 500; + padding: 0 6px; + } + + .confirm-input { + margin-bottom: 0px; + } +} \ No newline at end of file diff --git a/web/packages/tca-layout/src/modules/layout/header/enterprise.tsx b/web/packages/tca-layout/src/modules/layout/header/enterprise.tsx index b62c51c20..bcb61c84d 100644 --- a/web/packages/tca-layout/src/modules/layout/header/enterprise.tsx +++ b/web/packages/tca-layout/src/modules/layout/header/enterprise.tsx @@ -9,6 +9,7 @@ import { Link, useHistory } from 'react-router-dom'; import classnames from 'classnames'; import { Breadcrumb, Select } from 'coding-oa-uikit'; import AngleRight from 'coding-oa-uikit/lib/icon/AngleRight'; +import { filter } from 'lodash'; // 项目内 import { getHomeRouter, getTeamRouter, getTeamsRouter, getProjectRouter } from '@src/utils/getRoutePath'; @@ -35,7 +36,9 @@ const Enterprise = ({ org, orgSid, teamName }: IProps) => { // 团队变更时重新获取项目列表 if (org.org_sid && teamName) { getProjects(org.org_sid, null).then((response) => { - setProjectOptions(response.map((item: any) => ({ + // 只显示未禁用的项目 + const activeProjects = filter(response, {status: 1}); + setProjectOptions(activeProjects.map((item: any) => ({ ...item, label: item.display_name, value: item.name, diff --git a/web/packages/tca-layout/src/modules/layout/manage/siderbar-manage/menus.tsx b/web/packages/tca-layout/src/modules/layout/manage/siderbar-manage/menus.tsx index 7f7e0df7c..dcc57d2d4 100644 --- a/web/packages/tca-layout/src/modules/layout/manage/siderbar-manage/menus.tsx +++ b/web/packages/tca-layout/src/modules/layout/manage/siderbar-manage/menus.tsx @@ -15,6 +15,8 @@ import Scan from 'coding-oa-uikit/lib/icon/Scan'; import Sitemap from 'coding-oa-uikit/lib/icon/Sitemap'; import Key from 'coding-oa-uikit/lib/icon/Key'; import Tiles from 'coding-oa-uikit/lib/icon/Tiles'; +import Panel from 'coding-oa-uikit/lib/icon/Panel'; +import Project from 'coding-oa-uikit/lib/icon/Project'; const MENUS: MenuItem[] = [ { @@ -23,6 +25,18 @@ const MENUS: MenuItem[] = [ link: '/manage/users', key: 'users', }, + { + icon: , + title: t('团队管理'), + link: '/manage/orgs', + key: 'orgs', + }, + { + icon: , + title: t('项目管理'), + link: '/manage/teams', + key: 'teams', + }, { icon: , title: t('分析记录管理'), diff --git a/web/packages/tca-layout/src/modules/team/components/profile/index.tsx b/web/packages/tca-layout/src/modules/team/components/profile/index.tsx index fb039d36a..5c271173e 100644 --- a/web/packages/tca-layout/src/modules/team/components/profile/index.tsx +++ b/web/packages/tca-layout/src/modules/team/components/profile/index.tsx @@ -5,15 +5,18 @@ // ============================================================================== import React, { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; - +import { useParams, useHistory } from 'react-router-dom'; +import { find } from 'lodash'; import { Input, Form, Button, Row, Col, Statistic, Card, message } from 'coding-oa-uikit'; import Group from 'coding-oa-uikit/lib/icon/Group'; import Project from 'coding-oa-uikit/lib/icon/Project'; import Package from 'coding-oa-uikit/lib/icon/Package'; + +import { useStateStore } from '@src/context/store'; import { t } from '@src/i18n/i18next'; -import { getTeamInfo, updateTeamInfo } from '@src/services/team'; +import { getTeamInfo, updateTeamInfo, disableTeam } from '@src/services/team'; import { formatDateTime } from '@src/utils/index'; +import DeleteModal from '@src/components/delete-modal'; import style from './style.scss'; @@ -24,8 +27,15 @@ const layout = { const Profile = () => { const [form] = Form.useForm(); const { orgSid }: any = useParams(); - const [data, setData] = useState({}) as any; - const [isEdit, setIsEdit] = useState(false); + const [data, setData] = useState({}); + const [isEdit, setIsEdit] = useState(false); + const history = useHistory(); + // 判断用户是否有权限删除团队,仅超级管理员和团队管理员可以删除 + const { userinfo } = useStateStore(); + const isAdmin = !!find(data?.admins, { username: userinfo.username }); // 当前用户是否是管理员 + const [deleteVisible, setDeleteVisible] = useState(false); + const isSuperuser = userinfo.is_superuser; // 是否为超级管理员 + const deletable = isAdmin || isSuperuser; // 删除权限 useEffect(() => { if (orgSid) { @@ -37,7 +47,7 @@ const Profile = () => { const onFinish = (formData: any) => { updateTeamInfo(orgSid, formData).then((res) => { - message.success('团队信息更新成功'); + message.success(t('团队信息更新成功')); reset(); setData(res); }); @@ -48,8 +58,32 @@ const Profile = () => { form.resetFields(); }; + const onDelete = () => { + setDeleteVisible(true); + } + + const handleDeleteTeam = () => { + disableTeam(orgSid, { + status: 99 + }).then(() => { + message.success(t('团队已禁用!')); + history.push('/teams'); + }).catch((e: any) => { + console.error(e); + }).finally(() => setDeleteVisible(false)); + }; + return (
+ setDeleteVisible(false)} + onOk={handleDeleteTeam} + />

{data.name}

{t('团队概览')}

@@ -160,6 +194,9 @@ const Profile = () => { {t('编辑')} )} + {deletable && }
diff --git a/web/packages/tca-layout/src/modules/team/components/projects/index.tsx b/web/packages/tca-layout/src/modules/team/components/projects/index.tsx index 702e53c73..ec9ff2028 100644 --- a/web/packages/tca-layout/src/modules/team/components/projects/index.tsx +++ b/web/packages/tca-layout/src/modules/team/components/projects/index.tsx @@ -7,6 +7,7 @@ import React, { useEffect, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { Tabs, Button, Input } from 'coding-oa-uikit'; +import { filter } from 'lodash'; import { getProjects } from '@src/services/team'; import List from './list'; @@ -18,12 +19,12 @@ const { TabPane } = Tabs; const Projects = () => { const { orgSid }: any = useParams(); const history: any = useHistory(); - const [loading, setLoading] = useState(false); - const [data, setData] = useState(); - const [manageData, setManageData] = useState(); - const [tab, setTab] = useState('all'); - const [projectName, setProjectName] = useState(''); - const [visible, setVisible] = useState(false); + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + const [manageData, setManageData] = useState([]); + const [tab, setTab] = useState('all'); + const [projectName, setProjectName] = useState(''); + const [visible, setVisible] = useState(false); useEffect(() => { getListData(); @@ -43,7 +44,9 @@ const Projects = () => { setLoading(true); getProjects(orgSid, { ...params }).then((res) => { - tab === 'manage' ? setManageData(res) : setData(res); + // 只显示未禁用的项目 + const activeProjects = filter(res, {status: 1}); + tab === 'manage' ? setManageData(activeProjects) : setData(activeProjects); }) .finally(() => { setLoading(false); diff --git a/web/packages/tca-layout/src/modules/team/components/workspace/index.tsx b/web/packages/tca-layout/src/modules/team/components/workspace/index.tsx index e4819708e..621b78814 100644 --- a/web/packages/tca-layout/src/modules/team/components/workspace/index.tsx +++ b/web/packages/tca-layout/src/modules/team/components/workspace/index.tsx @@ -10,6 +10,7 @@ import { useParams, Link } from 'react-router-dom'; import { Tabs, Table } from 'coding-oa-uikit'; import AlignLeft from 'coding-oa-uikit/lib/icon/AlignLeft'; import Clock from 'coding-oa-uikit/lib/icon/Clock'; +import { filter } from 'lodash'; import { DEFAULT_PAGER } from '@src/common/constants'; import { formatDateTime } from '@src/utils/index'; @@ -41,8 +42,8 @@ const Workspace = () => { pageStart: offset, count: response.count, }); - - setList(response.results || []); + const activeTeamRepos = filter(response?.results, ['project_team.status',1]); + setList(activeTeamRepos || []); }) .finally(() => { setLoading(false); @@ -97,6 +98,21 @@ const Workspace = () => { )} /> + ( + <> + + {project?.display_name} + + + )} + /> get(`${MAIN_SERVER_API}/orgs/`, */ export const createTeam = (params: any) => post(`${MAIN_SERVER_API}/orgs/`, params); +/** + * 禁用团队 + * @param orgSid 团队唯一标识 + * @param params 参数 + */ + export const disableTeam = (orgSid: string, params: any) => put(`${MAIN_SERVER_API}/orgs/${orgSid}/status/`, params); /** * 获取团队详情 diff --git a/web/packages/tca-manage/src/components/delete-modal/index.tsx b/web/packages/tca-manage/src/components/delete-modal/index.tsx new file mode 100644 index 000000000..8cfb8ea1c --- /dev/null +++ b/web/packages/tca-manage/src/components/delete-modal/index.tsx @@ -0,0 +1,107 @@ +// Copyright (c) 2021-2022 THL A29 Limited +// +// This source code file is made available under MIT License +// See LICENSE for details +// ============================================================================== + +/** + * 确认删除操作弹框 + */ + + import React, { useEffect, useState } from 'react'; + import { Modal, Form, Input, message, Button } from 'coding-oa-uikit'; + import { t } from '@src/i18n/i18next'; + + import s from './style.scss'; + + interface DeleteModalProps { + actionType: string; + objectType: string; + confirmName: string; + visible: boolean; + addtionInfo?: string; + onCancel: () => void; + onOk: () => void; + } + + const DeleteModal = ({ actionType, objectType, confirmName, addtionInfo='', visible, onCancel, onOk }: DeleteModalProps) => { + const [form] = Form.useForm(); + const [confirmed, setConfirmed] = useState(true); + + useEffect(() => { + visible && form.resetFields(); + visible && setConfirmed(false); + }, [visible]); + + /** + * 表单提交操作 + * @param formData 参数 + */ + const onSubmitHandle = () => { + form.validateFields().then((formData) => { + if (formData?.confirm === confirmName) { + onOk(); + } else { + message.error(t('验证失败,请重新输入')); + } + }); + }; + + const checkConfirm = (changedValues: any) => { + if (changedValues?.confirm === confirmName) { + setConfirmed(true); + } else { + setConfirmed(false); + } + }; + + return ( + + {t('确认')}{actionType} + , + , + ]} + > +

+ {t('您正在')}{actionType}{objectType} {confirmName}{' '}
+

+ {addtionInfo &&

{addtionInfo}

} +

{t(`为确认${actionType}操作,请输入您要${actionType}的`)}{objectType}

+
+ + + +
+
+ ); + }; + + export default DeleteModal; + \ No newline at end of file diff --git a/web/packages/tca-manage/src/components/delete-modal/style.scss b/web/packages/tca-manage/src/components/delete-modal/style.scss new file mode 100644 index 000000000..59d9b1e32 --- /dev/null +++ b/web/packages/tca-manage/src/components/delete-modal/style.scss @@ -0,0 +1,21 @@ +.delete-modal { + + .warning-message { + margin-bottom: 20px; + } + + .confirm-message { + margin-bottom: 8px; + font-weight: 600; + } + + .confirm-text { + background-color: #ebedf1; + font-weight: 500; + padding: 0 6px; + } + + .confirm-input { + margin-bottom: 0px; + } +} \ No newline at end of file diff --git a/web/packages/tca-manage/src/modules/orgs/constants.ts b/web/packages/tca-manage/src/modules/orgs/constants.ts new file mode 100644 index 000000000..7af20c199 --- /dev/null +++ b/web/packages/tca-manage/src/modules/orgs/constants.ts @@ -0,0 +1,22 @@ +import { t } from '@src/i18n/i18next'; + +export const ORG_STATUS_ENUM = { + ACTIVE: 1, + INACTIVE: 99, +}; + +export const ORG_STATUS_CHOICES = { + [ORG_STATUS_ENUM.ACTIVE]: t('活跃'), + [ORG_STATUS_ENUM.INACTIVE]: t('禁用'), +}; + +export const ORG_STATUS_OPTIONS = [ + { + label: ORG_STATUS_CHOICES[ORG_STATUS_ENUM.ACTIVE], + value: ORG_STATUS_ENUM.ACTIVE, + }, + { + label: ORG_STATUS_CHOICES[ORG_STATUS_ENUM.INACTIVE], + value: ORG_STATUS_ENUM.INACTIVE, + }, +]; diff --git a/web/packages/tca-manage/src/modules/orgs/index.tsx b/web/packages/tca-manage/src/modules/orgs/index.tsx new file mode 100644 index 000000000..ae4c299e6 --- /dev/null +++ b/web/packages/tca-manage/src/modules/orgs/index.tsx @@ -0,0 +1,140 @@ +import React, { useEffect, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Row, Col, Tabs, Modal, message } from 'coding-oa-uikit'; + +// 项目内 +import { t } from '@src/i18n/i18next'; +import { getPaginationParams, getFilterURLPath } from '@src/utils'; +import { DEFAULT_PAGER } from '@src/common/constants'; +import { useURLParams, useDeepEffect } from '@src/utils/hooks'; +import { getOrgs, putOrgStatus } from '@src/services/orgs'; +import DeleteModal from '@src/components/delete-modal'; + +// 模块内 +import s from './style.scss'; +import Search from './search'; +import OrgTable from './org-table'; +import { ORG_STATUS_ENUM } from './constants'; + +const { TabPane } = Tabs; +const { confirm } = Modal; + +const FILTER_FIELDS = ['name', 'status']; + +const customFilterURLPath = (params = {}) => getFilterURLPath(FILTER_FIELDS, params); + +const Orgs = () => { + const history = useHistory(); + const [listData, setListData] = useState>([]); + const [count, setCount] = useState(DEFAULT_PAGER.count); + const [loading, setLoading] = useState(false); + const [reload, setReload] = useState(false); + const { filter, currentPage, searchParams } = useURLParams(FILTER_FIELDS); + const [deleteVisible, setDeleteVisible] = useState(false); + const [curOrg, setCurOrg] = useState(null); + + /** + * 根据路由参数获取团队列表 + */ + const getListData = () => { + setLoading(true); + getOrgs(filter).then((response) => { + setCount(response.count); + setListData(response.results || []); + setLoading(false); + }); + }; + + // 当路由参数变化时触发 + useDeepEffect(() => { + getListData(); + }, [filter]); + + // 手动触发 + useEffect(() => { + getListData(); + }, [reload]); + + // 筛选 + const onSearch = (params: any) => { + history.push(customFilterURLPath({ + limit: DEFAULT_PAGER.pageSize, + offset: DEFAULT_PAGER.pageStart, + ...params, + })); + }; + + // 翻页 + const onChangePageSize = (page: number, pageSize: number) => { + const params = getPaginationParams(page, pageSize); + history.push(customFilterURLPath(params)); + }; + + // 禁用团队 + const onDeleteOrg = (org: any) => { + setDeleteVisible(true); + setCurOrg(org); + }; + + const handleDeleteOrg = () => { + putOrgStatus(curOrg.org_sid, {status: ORG_STATUS_ENUM.INACTIVE}).then(() => { + message.success(t('已禁用团队')); + setReload(!reload); + setDeleteVisible(false); + setCurOrg(null); + }); + }; + + // 恢复团队 + const onRecoverOrg = (org: any) => { + confirm({ + title: t('恢复团队'), + content: t('确定要恢复已禁用的团队吗?'), + onOk() { + putOrgStatus(org.org_sid, {status: ORG_STATUS_ENUM.ACTIVE}).then(() => { + message.success(t('已恢复团队')); + setReload(!reload); + }); + }, + onCancel() {}, + }); + }; + + return ( + <> + + + + + + + +
+ +
+
+ `${range[0]} - ${range[1]} 条数据,共 ${total} 条`, + onChange: onChangePageSize, + }} + onDelete={onDeleteOrg} + onRecover={onRecoverOrg} + /> +
+ setDeleteVisible(false)} + onOk={handleDeleteOrg} + /> + + ); +}; + +export default Orgs; diff --git a/web/packages/tca-manage/src/modules/orgs/org-table.tsx b/web/packages/tca-manage/src/modules/orgs/org-table.tsx new file mode 100644 index 000000000..4de17bd1e --- /dev/null +++ b/web/packages/tca-manage/src/modules/orgs/org-table.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { Link, useHistory } from 'react-router-dom'; +import { Table, Tag, Tooltip, Button, Statistic, Row, Col, Card } from 'coding-oa-uikit'; +import Project from 'coding-oa-uikit/lib/icon/Project'; +import Repos from 'coding-oa-uikit/lib/icon/Repos'; +import Group from 'coding-oa-uikit/lib/icon/Group'; +import Undo from 'coding-oa-uikit/lib/icon/Undo'; +import Stop from 'coding-oa-uikit/lib/icon/Stop'; + +// 项目内 +import { t } from '@src/i18n/i18next'; +import EllipsisTemplate from '@src/components/ellipsis'; +import { formatDateTime } from '@src/utils'; +import { getOrgRouter } from '@src//utils/getRoutePath'; + +// 模块内 +import { + ORG_STATUS_ENUM, + ORG_STATUS_CHOICES, +} from './constants'; + +const FormatStatus = (org: any) => { + const status = org?.status; + const info = status === ORG_STATUS_ENUM.ACTIVE ? { + text: ORG_STATUS_CHOICES[ORG_STATUS_ENUM.ACTIVE], + color: 'success', + } : { + text: ORG_STATUS_CHOICES[ORG_STATUS_ENUM.INACTIVE], + color : 'red', + }; + return ( + {info.text} + ); +}; + +const FormatName = (org: any) => ( + <> + {org?.status === ORG_STATUS_ENUM.ACTIVE + ? + {org.name} + + : {org.name} + } +
+ {org.owner} / {org.tel_number} / {formatDateTime(org.created_time)} +
+ +); + +const FormatOverview = (org: any) => ( + + + + } + title={{t('成员数')}} + value={org.user_count} + valueStyle={{ color: '#f0850a', fontSize: '14px' }} + /> + + + + + } + title={{t('项目数')}} + value={org.team_count} + valueStyle={{ color: '#3d98ff', fontSize: '14px' }} + /> + + + + + } + title={{t('代码库')}} + value={org.repo_count} + valueStyle={{ color: '#3f8600', fontSize: '14px' }} + /> + + + +); + +const FormatOp = (org: any, onDelete: (org: any) => void, onRecover: (org: any) => void) => { + const history = useHistory(); + const status = org?.status; + const orgSid = org?.org_sid; + return ( + <> + +