From 4aca0f12e0166aa40885286ae4a180d34ff49c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=89=BA=E6=B3=BD?= Date: Fri, 6 Dec 2024 15:07:57 +0800 Subject: [PATCH] =?UTF-8?q?PullRequest:=20588=20feat:=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge branch 'feat/deleteProject of git@code.alipay.com:oceanbase/oceanbase-developer-center.git into dev-4.3.3 https://code.alipay.com/oceanbase/oceanbase-developer-center/pull_requests/588 Signed-off-by: 晓康 * feat: 删除项目 * feat: 增加验证值传参 * feat:添加未完成工单的列表 * feat: 对接接口 --- src/common/network/project.ts | 7 + src/common/network/task.ts | 9 ++ src/component/CommonTable/Toolbar.tsx | 1 + src/component/CommonTable/interface.ts | 2 + src/component/DisplayTable/index.tsx | 3 +- src/component/Empty/ProjectEmpty/index.tsx | 22 +++- src/component/Table/MiniTable/index.tsx | 13 +- .../Task/component/TaskTable/index.tsx | 122 ++++++++++-------- src/d.ts/index.ts | 29 ++++- src/d.ts/project.ts | 6 + src/d.ts/projectNotification.ts | 3 + src/page/Project/Database/index.tsx | 107 ++++++++------- .../Notification/components/Channel.tsx | 13 +- .../Notification/components/Policy.tsx | 10 +- .../Notification/components/columns.tsx | 14 +- src/page/Project/Project/ListItem/index.less | 6 + src/page/Project/Project/ListItem/index.tsx | 33 +++-- src/page/Project/Project/MoreBtn/index.less | 11 ++ src/page/Project/Project/MoreBtn/index.tsx | 66 ++++++++++ src/page/Project/Project/index.tsx | 86 ++++++++---- .../components/SensitiveColumn/index.tsx | 16 ++- src/page/Project/Setting/Info/TaskList.tsx | 72 +++++++++++ src/page/Project/Setting/Info/index.less | 4 + src/page/Project/Setting/Info/index.tsx | 106 +++++++++++---- src/page/Project/User/index.tsx | 50 +++---- .../DeleteProjectModal.tsx/index.tsx | 103 +++++++++++++++ src/page/Project/helper.ts | 16 +++ 27 files changed, 725 insertions(+), 205 deletions(-) create mode 100644 src/page/Project/Project/MoreBtn/index.less create mode 100644 src/page/Project/Project/MoreBtn/index.tsx create mode 100644 src/page/Project/Setting/Info/TaskList.tsx create mode 100644 src/page/Project/Setting/Info/index.less create mode 100644 src/page/Project/components/DeleteProjectModal.tsx/index.tsx create mode 100644 src/page/Project/helper.ts diff --git a/src/common/network/project.ts b/src/common/network/project.ts index 986a1db30..afe14f2a7 100644 --- a/src/common/network/project.ts +++ b/src/common/network/project.ts @@ -272,3 +272,10 @@ export async function addTablePermissions(params: { ); return !!res?.data; } + +export async function batchDeleteProject(projectId: number[]): Promise { + const res = await request.post('/api/v2/collaboration/projects/batchDelete', { + data: projectId, + }); + return !!res?.data; +} diff --git a/src/common/network/task.ts b/src/common/network/task.ts index 954d589eb..4e54fa5d2 100644 --- a/src/common/network/task.ts +++ b/src/common/network/task.ts @@ -33,6 +33,7 @@ import { IPartitionPlanTable, IPartitionTablePreviewConfig, IResponseData, + UnfinishedTickets, ISubTaskRecords, ITaskResult, Operation, @@ -155,6 +156,14 @@ export async function getTaskList(params: { return res?.data; } +/** + * 查询未完成的任务列表 + */ +export async function getUnfinishedTickets(projectId: number): Promise { + const res = await request.get(`/api/v2/collaboration/projects/${projectId}/unfinishedTickets`); + return res.data; +} + /** * 查询周期任务列表 */ diff --git a/src/component/CommonTable/Toolbar.tsx b/src/component/CommonTable/Toolbar.tsx index d61014305..02cb7cfa6 100644 --- a/src/component/CommonTable/Toolbar.tsx +++ b/src/component/CommonTable/Toolbar.tsx @@ -62,6 +62,7 @@ export const Toolbar: React.FC = (props) => { } = props; return ( + {operationContent?.isNeedOccupyElement &&
} {operationContent && } {titleContent && } diff --git a/src/component/CommonTable/interface.ts b/src/component/CommonTable/interface.ts index c7fd5f44e..b94920b94 100644 --- a/src/component/CommonTable/interface.ts +++ b/src/component/CommonTable/interface.ts @@ -87,6 +87,8 @@ export interface IOperationOption { } export interface IOperationContent { options: IOperationOption[]; + /** 是否需要占位 */ + isNeedOccupyElement?: boolean; } export interface IRowSelecter extends TableRowSelection { options: { diff --git a/src/component/DisplayTable/index.tsx b/src/component/DisplayTable/index.tsx index 044a48ad8..88c5dfaf1 100644 --- a/src/component/DisplayTable/index.tsx +++ b/src/component/DisplayTable/index.tsx @@ -141,6 +141,7 @@ export default class DisplayTable extends React.Component< showSizeChanger = true, showQuickJumper = true, enableResize, + scroll, ...rest } = this.props; const { defaultPageSize, columnWidthMap } = this.state; @@ -175,7 +176,7 @@ export default class DisplayTable extends React.Component< } } components={enableResize ? this.components : null} - scroll={{ x: 'max-content' }} + scroll={scroll ? scroll : { x: 'max-content' }} /> ); diff --git a/src/component/Empty/ProjectEmpty/index.tsx b/src/component/Empty/ProjectEmpty/index.tsx index 85e0cfa56..d43807637 100644 --- a/src/component/Empty/ProjectEmpty/index.tsx +++ b/src/component/Empty/ProjectEmpty/index.tsx @@ -1,11 +1,17 @@ import { formatMessage } from '@/util/intl'; import { Result } from 'antd'; import styles from './index.less'; +import { ProjectTabType } from '@/d.ts/project'; -export default function ProjectEmpty({ type, renderActionButton }) { +interface ProjectEmptyProps { + type: ProjectTabType; + renderActionButton: () => JSX.Element; +} + +const ProjectEmpty: React.FC = ({ type, renderActionButton }) => { const renderTitle = (type) => { switch (type) { - case 'all': + case ProjectTabType.ALL: return (
{formatMessage({ @@ -14,7 +20,7 @@ export default function ProjectEmpty({ type, renderActionButton }) { })}
); - case 'deleted': + case ProjectTabType.ARCHIVED: return (
{formatMessage({ @@ -30,7 +36,7 @@ export default function ProjectEmpty({ type, renderActionButton }) { const renderSubTitle = (type) => { switch (type) { - case 'all': + case ProjectTabType.ALL: return (
@@ -48,7 +54,7 @@ export default function ProjectEmpty({ type, renderActionButton }) {
); - case 'deleted': + case ProjectTabType.ARCHIVED: return (
@@ -79,7 +85,9 @@ export default function ProjectEmpty({ type, renderActionButton }) { } /> - {type === 'all' && renderActionButton()} + {type === ProjectTabType.ALL && renderActionButton()} ); -} +}; + +export default ProjectEmpty; diff --git a/src/component/Table/MiniTable/index.tsx b/src/component/Table/MiniTable/index.tsx index d6c4cb439..9c8daf35b 100644 --- a/src/component/Table/MiniTable/index.tsx +++ b/src/component/Table/MiniTable/index.tsx @@ -23,24 +23,33 @@ import styles from './index.less'; import classNames from 'classnames'; import { ResizeTitle } from '@/component/CommonTable/component/ResizeTitle'; import { DEFAULT_COLUMN_WIDTH } from '@/component/CommonTable/const'; +import type { ColumnGroupType, ColumnType } from 'antd/es/table'; + +type IColumnsType = (( + | ColumnGroupType + | ColumnType +) & { hide?: boolean })[]; interface IProps extends TableProps { isExpandedRowRender?: boolean; loadData: (page: TablePaginationConfig, filters: Record) => void; // 是否启用 列宽可拖拽 enableResize?: boolean; + columns: IColumnsType; } export default function MiniTable({ loadData, isExpandedRowRender = false, enableResize = false, + columns: PropColumns = [], ...restProps }: IProps) { const [pageSize, setPageSize] = useState(0); const [columnWidthMap, setColumnWidthMap] = useState(null); const domRef = useRef(); + const columns = PropColumns.filter((item) => !item.hide); useLayoutEffect(() => { if (domRef.current) { @@ -114,7 +123,7 @@ export default function MiniTable({ } columns={ enableResize - ? restProps?.columns?.map((oriColumn) => { + ? columns?.map((oriColumn) => { return { ...oriColumn, width: @@ -127,7 +136,7 @@ export default function MiniTable({ } as React.HTMLAttributes), }; }) - : restProps?.columns || [] + : columns || [] } />
diff --git a/src/component/Task/component/TaskTable/index.tsx b/src/component/Task/component/TaskTable/index.tsx index fa9ea13df..e5a8b0bda 100644 --- a/src/component/Task/component/TaskTable/index.tsx +++ b/src/component/Task/component/TaskTable/index.tsx @@ -50,11 +50,13 @@ import { inject, observer } from 'mobx-react'; import type { Moment } from 'moment'; import moment from 'moment'; import type { FixedType } from 'rc-table/lib/interface'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useContext } from 'react'; import { getTaskGroupLabels, getTaskLabelByType, isCycleTaskPage } from '../../helper'; import styles from '../../index.less'; import TaskTools from '../ActionBar'; import { listProjects } from '@/common/network/project'; +import ProjectContext from '@/page/Project/ProjectContext'; +import { isProjectArchived } from '@/page/Project/helper'; import { useRequest } from 'ahooks'; const { RangePicker } = DatePicker; const { Text, Link } = Typography; @@ -226,7 +228,8 @@ const TaskTable: React.FC = inject( const [hoverInNewTaskMenuBtn, setHoverInNewTaskMenuBtn] = useState(false); const [hoverInNewTaskMenu, setHoverInNewTaskMenu] = useState(false); const [listParams, setListParams] = useState(null); - + const { project } = useContext(ProjectContext) || {}; + const projectArchived = isProjectArchived(project); const loadParams = useRef(null); const { activePageKey } = pageStore; const columns = initColumns(listParams); @@ -567,6 +570,66 @@ const TaskTable: React.FC = inject( ); }; + const getOperationContentOption = () => { + if (projectArchived) return []; + if (isAll) { + return [ + { + type: IOperationOptionType.custom, + render: () => ( + + + + ), + }, + ]; + } + return [ + { + type: IOperationOptionType.button, + content: [ + TaskPageType.APPLY_PROJECT_PERMISSION, + TaskPageType.APPLY_DATABASE_PERMISSION, + TaskPageType.APPLY_TABLE_PERMISSION, + ].includes(taskTabType) + ? activeTaskLabel + : formatMessage( + { + id: 'odc.src.component.Task.component.TaskTable.NewActiveTasklabel', + defaultMessage: '新建{activeTaskLabel}', + }, + { activeTaskLabel }, + ), + //`新建${activeTaskLabel}` + isPrimary: true, + onClick: () => { + props.onMenuClick(taskTabType); + }, + }, + ]; + }; + return ( = inject( titleContent={null} enableResize operationContent={{ - options: [ - isAll - ? { - type: IOperationOptionType.custom, - render: () => ( - - - - ), - } - : { - type: IOperationOptionType.button, - content: [ - TaskPageType.APPLY_PROJECT_PERMISSION, - TaskPageType.APPLY_DATABASE_PERMISSION, - TaskPageType.APPLY_TABLE_PERMISSION, - ].includes(taskTabType) - ? activeTaskLabel - : formatMessage( - { - id: 'odc.src.component.Task.component.TaskTable.NewActiveTasklabel', - defaultMessage: '新建{activeTaskLabel}', - }, - { activeTaskLabel }, - ), - //`新建${activeTaskLabel}` - isPrimary: true, - onClick: () => { - props.onMenuClick(taskTabType); - }, - }, - ], + options: getOperationContentOption(), + isNeedOccupyElement: projectArchived, }} filterContent={{ enabledSearch: false, diff --git a/src/d.ts/index.ts b/src/d.ts/index.ts index 070cf19da..19d1a2725 100644 --- a/src/d.ts/index.ts +++ b/src/d.ts/index.ts @@ -3941,7 +3941,7 @@ export interface AgainTaskRecord { id: string | number; } -// 无锁结构变更任务进度状态 +/** 无锁结构变更任务进度状态 */ export enum ProgressOfLocklessStructureChangeTaskStatusMap { CREATE_GHOST_TABLES = 'CREATE_GHOST_TABLES', //'创建影子表' CREATE_DATA_TASK = 'CREATE_DATA_TASK', //'创建数据迁移任务' @@ -3951,3 +3951,30 @@ export enum ProgressOfLocklessStructureChangeTaskStatusMap { SWAP_TABLE = 'SWAP_TABLE', // '切换中' CLEAR_RESOURCE = 'CLEAR_RESOURCE', // '释放迁移任务资源' } + +interface UnfinishedTaskType { + approvable: boolean; + approveInstanceId: number; + candidateApprovers: { + id: number; + name: string; + accountName: string; + }[]; + createTime: number; + creator: { + id: number; + name: string; + accountName: string; + roleNames: string[]; + }; + description: string; + id: number; + project: IProject; + status: TaskStatus; + type: TaskType; +} + +export type UnfinishedTickets = { + unfinishedFlowInstances: UnfinishedTaskType[]; + unfinishedSchedules: UnfinishedTaskType[]; +}; diff --git a/src/d.ts/project.ts b/src/d.ts/project.ts index 9cd5ecf5e..b6323b0bb 100644 --- a/src/d.ts/project.ts +++ b/src/d.ts/project.ts @@ -17,6 +17,12 @@ import { TablePermissionType } from '@/d.ts/table'; import { DatabasePermissionType } from './database'; +export enum ProjectTabType { + /** 全部项目 */ + ALL = 'all', + /** 归档项目 */ + ARCHIVED = 'archived', +} export enum ProjectRole { DEVELOPER = 'DEVELOPER', DBA = 'DBA', diff --git a/src/d.ts/projectNotification.ts b/src/d.ts/projectNotification.ts index e79b11981..56a838725 100644 --- a/src/d.ts/projectNotification.ts +++ b/src/d.ts/projectNotification.ts @@ -153,6 +153,7 @@ export interface IChannel { /** @description 通道 描述 */ description?: string; } +export type IChannelColumnsKeys = keyof IChannel | 'action'; export interface ITestChannelResult { active: boolean; @@ -174,6 +175,8 @@ export interface IPolicy { channels: IChannel[]; eventName: string; } +export type IPolicyColumnsKeys = keyof IPolicy | 'action'; + export type TBatchUpdatePolicy = { id?: number; policyMetadataId?: number; diff --git a/src/page/Project/Database/index.tsx b/src/page/Project/Database/index.tsx index 2535b7ce8..d909f12d0 100644 --- a/src/page/Project/Database/index.tsx +++ b/src/page/Project/Database/index.tsx @@ -55,6 +55,7 @@ import Header from './Header'; import styles from './index.less'; import ParamContext, { IFilterParams } from './ParamContext'; import StatusName from './StatusName'; +import { isProjectArchived } from '@/page/Project/helper'; interface IProps { id: string; modalStore?: ModalStore; @@ -63,7 +64,7 @@ interface IProps { const Database: React.FC = ({ id, modalStore }) => { const statusMap = datasourceStatus.statusMap; const { project } = useContext(ProjectContext); - + const projectArchived = isProjectArchived(project); const [total, setTotal] = useState(0); const [searchValue, setSearchValue] = useState(''); const [filterParams, setFilterParams] = useState({ @@ -150,14 +151,18 @@ const Database: React.FC = ({ id, modalStore }) => { default: } }; - const renderNoPermissionDBWithTip = (name: React.ReactNode) => { + const renderNoPermissionDBWithTip = (name: React.ReactNode, showTip = true) => { return ( {name} @@ -201,21 +206,48 @@ const Database: React.FC = ({ id, modalStore }) => { ); }, [selectedRowKeys, data]); + const rowSelection = { + selectedRowKeys: selectedRowKeys, + preserveSelectedRowKeys: true, + onChange: (selectedRowKeys: React.Key[], selectedRows: IDatabase[]) => { + setSelectedRowKeys(selectedRowKeys); + }, + getCheckboxProps: (record: IDatabase) => { + const hasChangeAuth = record.authorizedPermissionTypes?.includes( + DatabasePermissionType.CHANGE, + ); + const hasQueryAuth = record.authorizedPermissionTypes?.includes(DatabasePermissionType.QUERY); + const disabled = + !hasChangeAuth && !hasQueryAuth && !record?.authorizedPermissionTypes?.length; + const status = statusMap.get(record?.dataSource?.id) || record?.dataSource?.status; + const config = getDataSourceModeConfig(record?.dataSource?.type); + + return { + disabled: + disabled || + !record.existed || + ![IConnectionStatus.ACTIVE, IConnectionStatus.TESTING]?.includes(status?.status) || + !config?.features?.task?.includes(TaskType.MULTIPLE_ASYNC), + name: record.name, + }; + }, + }; + + const tablrCardTitle = ( + reload()} + projectId={parseInt(id)} + onOpenLogicialDatabase={() => setOpenLogicialDatabase(true)} + /> + ); + return ( reload()} - projectId={parseInt(id)} - onOpenLogicialDatabase={() => setOpenLogicialDatabase(true)} - /> - } + title={projectArchived ? null : tablrCardTitle} extra={ = ({ id, modalStore }) => { > rowKey={'id'} - rowSelection={{ - selectedRowKeys: selectedRowKeys, - preserveSelectedRowKeys: true, - onChange: (selectedRowKeys: React.Key[], selectedRows: IDatabase[]) => { - setSelectedRowKeys(selectedRowKeys); - }, - getCheckboxProps: (record: IDatabase) => { - const hasChangeAuth = record.authorizedPermissionTypes?.includes( - DatabasePermissionType.CHANGE, - ); - const hasQueryAuth = record.authorizedPermissionTypes?.includes( - DatabasePermissionType.QUERY, - ); - const disabled = - !hasChangeAuth && !hasQueryAuth && !record?.authorizedPermissionTypes?.length; - const status = statusMap.get(record?.dataSource?.id) || record?.dataSource?.status; - const config = getDataSourceModeConfig(record?.dataSource?.type); - - return { - disabled: - disabled || - !record.existed || - ![IConnectionStatus.ACTIVE, IConnectionStatus.TESTING]?.includes(status?.status) || - !config?.features?.task?.includes(TaskType.MULTIPLE_ASYNC), - name: record.name, - }; - }, - }} + rowSelection={projectArchived ? null : rowSelection} scroll={{ x: 1150, }} @@ -289,7 +294,8 @@ const Database: React.FC = ({ id, modalStore }) => { DatabasePermissionType.QUERY, ); const disabled = - !hasChangeAuth && !hasQueryAuth && !record?.authorizedPermissionTypes?.length; + (!hasChangeAuth && !hasQueryAuth && !record?.authorizedPermissionTypes?.length) || + projectArchived; const style = getDataSourceStyleByConnectType(record?.dataSource?.type); if (!record.existed) { return disabled ? ( @@ -301,7 +307,7 @@ const Database: React.FC = ({ id, modalStore }) => { defaultMessage: '当前数据库不存在', })} /*当前数据库不存在*/ > - {renderNoPermissionDBWithTip(name)} + {renderNoPermissionDBWithTip(name, !projectArchived)} ) : ( = ({ id, modalStore }) => { ); } return disabled ? ( - renderNoPermissionDBWithTip(name) + renderNoPermissionDBWithTip(name, !projectArchived) ) : (
{record?.type === 'LOGICAL' && } @@ -477,6 +483,7 @@ const Database: React.FC = ({ id, modalStore }) => { dataIndex: 'actions', key: 'actions', width: 210, + hide: projectArchived, render(_, record) { const config = getDataSourceModeConfig(record?.dataSource?.type); const notSupportToResourceTree = !config?.features?.resourceTree; diff --git a/src/page/Project/Notification/components/Channel.tsx b/src/page/Project/Notification/components/Channel.tsx index 4f916caea..7c0ad1c6c 100644 --- a/src/page/Project/Notification/components/Channel.tsx +++ b/src/page/Project/Notification/components/Channel.tsx @@ -59,8 +59,10 @@ import { } from 'antd'; import { useForm, useWatch } from 'antd/lib/form/Form'; import classNames from 'classnames'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useContext } from 'react'; import { getChannelColumns } from './columns'; +import ProjectContext from '@/page/Project/ProjectContext'; +import { isProjectArchived } from '@/page/Project/helper'; import styles from './index.less'; import { EChannelTypeMap, @@ -80,6 +82,9 @@ const Channel: React.FC<{ const [formDrawerOpen, setFormDrawerOpen] = useState(false); const [channelsList, setChannelsList] = useState, 'channelConfig'>>>(); + const { project } = useContext(ProjectContext); + const projectArchived = isProjectArchived(project); + const loadChannels = async (args: ITableLoadOptions) => { const { filters, sorter, pagination, pageSize } = args ?? {}; const { name, type } = filters ?? {}; @@ -168,6 +173,7 @@ const Channel: React.FC<{ handleDelete, handleChannelEdit, hanleOpenChannelDetailDrawer, + hideColumns: projectArchived ? ['action'] : [], }); return (
@@ -194,7 +200,10 @@ const Channel: React.FC<{ showToolbar={true} onLoad={loadChannels} onChange={loadChannels} - operationContent={{ options: operationOptions }} + operationContent={{ + options: projectArchived ? [] : operationOptions, + isNeedOccupyElement: projectArchived, + }} tableProps={{ columns, dataSource: channelsList?.contents || [], diff --git a/src/page/Project/Notification/components/Policy.tsx b/src/page/Project/Notification/components/Policy.tsx index 96b21f2bd..540bbfb30 100644 --- a/src/page/Project/Notification/components/Policy.tsx +++ b/src/page/Project/Notification/components/Policy.tsx @@ -32,11 +32,13 @@ import { formatMessage } from '@/util/intl'; import { useSetState } from 'ahooks'; import { Button, Divider, Form, message, Modal, Select } from 'antd'; import { useForm } from 'antd/lib/form/Form'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useContext } from 'react'; import { DetailChannelDrawer, FromChannelDrawer } from './Channel'; import { getPolicyColumns } from './columns'; import styles from './index.less'; import { EPolicyFormMode, TPolicyForm } from './interface'; +import ProjectContext from '@/page/Project/ProjectContext'; +import { isProjectArchived } from '@/page/Project/helper'; const Policy: React.FC<{ projectId: number; @@ -53,7 +55,8 @@ const Policy: React.FC<{ mode: EPolicyFormMode.SINGLE, policies: [], }); - + const { project } = useContext(ProjectContext); + const projectArchived = isProjectArchived(project); const loadPolicies = async (args: ITableLoadOptions) => { const { eventName, channels } = argsRef.current?.filters ?? {}; const results = await getPoliciesList(projectId, {}); @@ -195,6 +198,7 @@ const Policy: React.FC<{ handleUpdatePolicies, handleSwitchPoliciesStatus, hanleOpenChannelDetailDrawer, + hideColumns: projectArchived ? ['enabled', 'action'] : [], }); const rowSelector: IRowSelecter = { @@ -283,7 +287,7 @@ const Policy: React.FC<{ rowKey: 'policyMetadataId', pagination: pagination || false, }} - rowSelecter={rowSelector} + rowSelecter={projectArchived ? null : rowSelector} />
); diff --git a/src/page/Project/Notification/components/columns.tsx b/src/page/Project/Notification/components/columns.tsx index 4c6a38c9d..87f37b533 100644 --- a/src/page/Project/Notification/components/columns.tsx +++ b/src/page/Project/Notification/components/columns.tsx @@ -21,6 +21,8 @@ import { IChannel, IMessage, IPolicy, + IPolicyColumnsKeys, + IChannelColumnsKeys, } from '@/d.ts/projectNotification'; import { formatMessage } from '@/util/intl'; import { getLocalFormatDateTime } from '@/util/utils'; @@ -172,19 +174,22 @@ type GetPolicyColumns = ({ handleUpdatePolicies, handleSwitchPoliciesStatus, hanleOpenChannelDetailDrawer, + hideColumns, }: { projectId: number; handleUpdatePolicies: (formType: TPolicyForm) => void; handleSwitchPoliciesStatus: (formType: TPolicyForm, enabled?: boolean) => Promise; hanleOpenChannelDetailDrawer; + hideColumns?: IPolicyColumnsKeys[]; }) => ColumnType[]; export const getPolicyColumns: GetPolicyColumns = function ({ projectId, handleUpdatePolicies, handleSwitchPoliciesStatus, hanleOpenChannelDetailDrawer, + hideColumns = [], }) { - return [ + const columns = [ { title: formatMessage({ id: 'src.page.Project.Notification.components.3F3F37F5', @@ -334,6 +339,7 @@ export const getPolicyColumns: GetPolicyColumns = function ({ }, }, ]; + return columns.filter((item) => !hideColumns.includes(item.key as IPolicyColumnsKeys)); }; // #endregion @@ -342,17 +348,20 @@ type GetChannelColumn = ({ handleDelete, handleChannelEdit, hanleOpenChannelDetailDrawer, + hideColumns, }: { handleDelete: (channelId: number) => void; handleChannelEdit: (channelId: number) => void; hanleOpenChannelDetailDrawer: (channel: Omit, 'channelConfig'>) => void; + hideColumns?: IChannelColumnsKeys[]; }) => ColumnType>[]; export const getChannelColumns: GetChannelColumn = function ({ handleDelete, handleChannelEdit, hanleOpenChannelDetailDrawer, + hideColumns = [], }) { - return [ + const columns: ColumnType>[] = [ { title: formatMessage({ id: 'src.page.Project.Notification.components.76BA6F01', @@ -468,5 +477,6 @@ export const getChannelColumns: GetChannelColumn = function ({ }, }, ]; + return columns.filter((item) => !hideColumns.includes(item.key as IChannelColumnsKeys)); }; // #endregion diff --git a/src/page/Project/Project/ListItem/index.less b/src/page/Project/Project/ListItem/index.less index 131b89eea..71a1a0e70 100644 --- a/src/page/Project/Project/ListItem/index.less +++ b/src/page/Project/Project/ListItem/index.less @@ -37,4 +37,10 @@ overflow: hidden; text-overflow: ellipsis; } + .action { + flex-basis: 50px; + flex-grow: 0; + flex-shrink: 0; + justify-content: right; + } } diff --git a/src/page/Project/Project/ListItem/index.tsx b/src/page/Project/Project/ListItem/index.tsx index 207ef1ac3..5c61e260f 100644 --- a/src/page/Project/Project/ListItem/index.tsx +++ b/src/page/Project/Project/ListItem/index.tsx @@ -16,29 +16,45 @@ import { IProject, ProjectRole } from '@/d.ts/project'; import Icon from '@ant-design/icons'; -import { Space } from 'antd'; +import { Checkbox } from 'antd'; import classNames from 'classnames'; import React, { forwardRef } from 'react'; import styles from './index.less'; - +import type { SelectProject } from '@/page/Project/components/DeleteProjectModal.tsx'; import { ReactComponent as ProjectSvg } from '@/svgr/project_space.svg'; import { ReactComponent as UserSvg } from '@/svgr/user.svg'; interface IProps { data: IProject; onClick: (p: IProject) => void; + action: React.ReactElement; + onSelectChange?: (isSelected: boolean, params: any) => void; + selectProjectList: SelectProject[]; } export default forwardRef(function ListItem( - { data, onClick }: IProps, + { data, onClick, action, onSelectChange, selectProjectList }: IProps, ref: React.Ref, ) { + const onChange = (e) => { + onSelectChange(e.target.checked, { + id: data.id, + name: data.name, + }); + }; + return ( -
+
+ {action && ( +
+ e.stopPropagation()} + checked={selectProjectList.some((item) => item.id === data.id)} + > +
+ )} +
@@ -51,6 +67,7 @@ export default forwardRef(function ListItem( ?.map((a) => a.name) ?.join(', ') || '-'}
+ {action &&
{action}
}
); }); diff --git a/src/page/Project/Project/MoreBtn/index.less b/src/page/Project/Project/MoreBtn/index.less new file mode 100644 index 000000000..415b3d9bf --- /dev/null +++ b/src/page/Project/Project/MoreBtn/index.less @@ -0,0 +1,11 @@ +.menu { + :global { + .ant-dropdown-menu-title-content { + padding-left: 6px; + padding-right: 40px; + } + .ant-dropdown-menu-item-icon { + color: var(--icon-color-normal); + } + } +} diff --git a/src/page/Project/Project/MoreBtn/index.tsx b/src/page/Project/Project/MoreBtn/index.tsx new file mode 100644 index 000000000..f9a168f40 --- /dev/null +++ b/src/page/Project/Project/MoreBtn/index.tsx @@ -0,0 +1,66 @@ +import { Dropdown } from 'antd'; +import { inject, observer } from 'mobx-react'; +import { EllipsisOutlined } from '@ant-design/icons'; +import { ItemType } from 'antd/es/menu/hooks/useItems'; +import styles from './index.less'; +import { useState } from 'react'; +import { IProject } from '@/d.ts/project'; +import DeleteProjectModal from '@/page/Project/components/DeleteProjectModal.tsx'; + +enum Actions { + REMOVE = 'remove', +} + +interface MoreBtnProps { + project: IProject; + reload: () => void; +} + +const MoreBtn: React.FC = function (props) { + const { project, reload } = props; + const [openDeleteProjectModal, setOpenDeleteProjectModal] = useState(false); + + const items: ItemType[] = [ + { + label: '删除项目', + key: Actions.REMOVE, + }, + ]; + + return ( + <> + + + + + + ); +}; + +export default inject('modalStore')(observer(MoreBtn)); diff --git a/src/page/Project/Project/index.tsx b/src/page/Project/Project/index.tsx index 3dd4972e2..e2c014a36 100644 --- a/src/page/Project/Project/index.tsx +++ b/src/page/Project/Project/index.tsx @@ -23,40 +23,39 @@ import Search from '@/component/Input/Search'; import PageContainer, { TitleType } from '@/component/PageContainer'; import ApplyPermissionButton from '@/component/Task/ApplyPermission/CreateButton'; import { actionTypes, IManagerResourceType } from '@/d.ts'; -import { IProject } from '@/d.ts/project'; +import { IProject, ProjectTabType } from '@/d.ts/project'; import { IPageType } from '@/d.ts/_index'; import { setDefaultProject } from '@/service/projectHistory'; import { formatMessage } from '@/util/intl'; import { useNavigate } from '@umijs/max'; -import { List, Space, Spin, Typography } from 'antd'; +import { List, Space, Spin, Typography, Button, message } from 'antd'; import VirtualList from 'rc-virtual-list'; -import { useContext, useEffect, useRef, useState } from 'react'; -import ProjectContext from '../ProjectContext'; +import { useEffect, useRef, useState } from 'react'; import CreateProjectDrawer from './CreateProject/Drawer'; import styles from './index.less'; import ListItem from './ListItem'; import userStore from '@/store/login'; +import MoreBtn from './MoreBtn'; +import DeleteProjectModal from '@/page/Project/components/DeleteProjectModal.tsx'; +import type { SelectProject } from '@/page/Project/components/DeleteProjectModal.tsx'; -const { Title, Text } = Typography; const titleOptions: { label: string; - value: 'all' | 'deleted'; + value: ProjectTabType; }[] = [ { label: formatMessage({ id: 'odc.Project.Project.AllProjects', defaultMessage: '全部项目', }), - //全部项目 - value: 'all', + value: ProjectTabType.ALL, }, { label: formatMessage({ id: 'odc.Project.Project.ArchiveProject', defaultMessage: '归档项目', }), - //归档项目 - value: 'deleted', + value: ProjectTabType.ARCHIVED, }, ]; @@ -68,20 +67,20 @@ const Project = () => { ); const [dataSource, setDataSource] = useState([]); const [projectSearchName, setProjectSearchName] = useState(null); - const [projectType, setProjectType] = useState<'all' | 'deleted'>('all'); + const [projectType, setProjectType] = useState(ProjectTabType.ALL); const [loading, setLoading] = useState(false); const navigate = useNavigate(); - const context = useContext(ProjectContext); - const { project } = context; - const isProjectDeleted = projectType === 'deleted'; + const projectTypeIsArchived = projectType === ProjectTabType.ARCHIVED; + const [openDeleteProjectModal, setOpenDeleteProjectModal] = useState(false); + const [selectProjectList, setSelectProjectList] = useState([]); const sessionStorageKey = `projectSearch-${userStore?.organizationId}-${userStore?.user?.id}`; const appendData = async (currentPage, dataSource, projectType, projectSearchName) => { setLoading(true); try { - const isProjectDeleted = projectType === 'deleted'; - const res = await listProjects(projectSearchName, currentPage + 1, 40, isProjectDeleted); + const projectTypeIsArchived = projectType === ProjectTabType.ARCHIVED; + const res = await listProjects(projectSearchName, currentPage + 1, 40, projectTypeIsArchived); if (res) { setCurrentPage(currentPage + 1); /** @@ -123,7 +122,7 @@ const Project = () => { showDivider: true, defaultValue: projectType, }} - onTabChange={(v: 'all' | 'deleted') => { + onTabChange={(v: ProjectTabType) => { setProjectType(v); reload(v); }} @@ -137,11 +136,11 @@ const Project = () => { fallback={} {...createPermission(IManagerResourceType.project, actionTypes.create)} > - reload()} /> + reload()} /> {!!dataSource?.length && ( { } /> )} + {projectTypeIsArchived && ( + + )} { {(item) => ( { - if (isProjectDeleted) { - return; - } setDefaultProject(p.id); navigate(`/project/${p.id}/${IPageType.Project_Database}`); }} + selectProjectList={selectProjectList} + onSelectChange={(isSelected, values) => { + if (isSelected) { + setSelectProjectList([...selectProjectList, values]); + } else { + setSelectProjectList( + selectProjectList.filter((item) => item.id !== values.id), + ); + } + }} data={item} + action={ + projectTypeIsArchived ? ( + { + e.stopPropagation(); + }} + > + + + ) : null + } /> )} @@ -222,7 +253,7 @@ const Project = () => { { {...createPermission(IManagerResourceType.project, actionTypes.create)} > reload()} /> @@ -249,6 +280,13 @@ const Project = () => { )}
+ ); }; diff --git a/src/page/Project/Sensitive/components/SensitiveColumn/index.tsx b/src/page/Project/Sensitive/components/SensitiveColumn/index.tsx index d89a7458c..2e716be8d 100644 --- a/src/page/Project/Sensitive/components/SensitiveColumn/index.tsx +++ b/src/page/Project/Sensitive/components/SensitiveColumn/index.tsx @@ -46,6 +46,8 @@ import SensitiveContext from '../../SensitiveContext'; import EditSensitiveColumnModal from './components/EditSensitiveColumnModal'; import FormSensitiveColumnDrawer from './components/FormSensitiveColumnDrawer'; import ManualForm from './components/ManualForm'; +import ProjectContext from '@/page/Project/ProjectContext'; +import { isProjectArchived } from '@/page/Project/helper'; import styles from './index.less'; export const PopoverContainer: React.FC<{ @@ -91,6 +93,7 @@ const getColumns: ({ dataSourceIdMap, hasRowSelected, maskingAlgorithmIdMap, + hideColumns, }) => ColumnsType = ({ handleStatusSwitch, handleEdit, @@ -100,8 +103,9 @@ const getColumns: ({ dataSourceIdMap, hasRowSelected, maskingAlgorithmIdMap, + hideColumns = [], }) => { - return [ + const columns: ColumnsType = [ { title: formatMessage({ id: 'odc.components.SensitiveColumn.DataSource', @@ -307,6 +311,7 @@ const getColumns: ({ ), }, ]; + return columns.filter((item) => !hideColumns.includes(item.key)); }; const SensitiveColumn = ({ projectId, @@ -316,6 +321,8 @@ const SensitiveColumn = ({ }) => { const tableRef = useRef(); const sensitiveContext = useContext(SensitiveContext); + const { project } = useContext(ProjectContext); + const projectArchived = isProjectArchived(project); const { dataSourceIdMap, maskingAlgorithms, maskingAlgorithmIdMap, maskingAlgorithmOptions } = sensitiveContext; const [sensitiveColumnIds, setSensitiveColumnIds] = useState([]); @@ -539,6 +546,7 @@ const SensitiveColumn = ({ maskingAlgorithms, dataSourceIdMap: dataSourceIdMap, maskingAlgorithmIdMap: maskingAlgorithmIdMap, + hideColumns: projectArchived ? ['action'] : [], }); const operationOptions: IOperationOption[] = []; operationOptions.push({ @@ -592,6 +600,7 @@ const SensitiveColumn = ({ }, onClick: () => {}, }); + return ( <> diff --git a/src/page/Project/Setting/Info/TaskList.tsx b/src/page/Project/Setting/Info/TaskList.tsx new file mode 100644 index 000000000..a4ab5b19d --- /dev/null +++ b/src/page/Project/Setting/Info/TaskList.tsx @@ -0,0 +1,72 @@ +import VirtualList from 'rc-virtual-list'; +import { List } from 'antd'; +import React, { useEffect, useRef, useState } from 'react'; +import styles from './index.less'; +import classNames from 'classnames'; +import DisplayTable from '@/component/DisplayTable'; +import { formatMessage } from '@/util/intl'; +import { TaskTypeMap } from '@/component/Task/component/TaskTable'; +import StatusLabel from '@/component/Task/component/Status'; + +interface TaskListProps { + dataSource: any[]; +} + +const TaskList: React.FC = (props) => { + const { dataSource } = props; + + const columns = [ + { + dataIndex: 'id', + key: 'id', + title: formatMessage({ + id: 'odc.component.TaskTable.No', + defaultMessage: '编号', + }), + width: 100, + }, + { + dataIndex: 'type', + key: 'type', + title: formatMessage({ + id: 'odc.component.TaskTable.Type', + defaultMessage: '类型', + }), + //类型 + ellipsis: true, + width: 140, + render: (type, record) => { + return TaskTypeMap[type]; + }, + }, + { + dataIndex: 'status', + key: 'status', + title: formatMessage({ + id: 'odc.component.TaskTable.Status', + defaultMessage: '状态', + }), + width: 120, + render: (status, record) => ( + + ), + }, + ]; + + return ( + + ); +}; + +export default TaskList; diff --git a/src/page/Project/Setting/Info/index.less b/src/page/Project/Setting/Info/index.less new file mode 100644 index 000000000..e5b369bea --- /dev/null +++ b/src/page/Project/Setting/Info/index.less @@ -0,0 +1,4 @@ +.TaskList { + border: 1px solid var(--odc-border-color); + width: 100%; +} diff --git a/src/page/Project/Setting/Info/index.tsx b/src/page/Project/Setting/Info/index.tsx index e8f05834f..c078daeb7 100644 --- a/src/page/Project/Setting/Info/index.tsx +++ b/src/page/Project/Setting/Info/index.tsx @@ -18,14 +18,20 @@ import { setProjectAchived, updateProject } from '@/common/network/project'; import { IProject } from '@/d.ts/project'; import { formatMessage } from '@/util/intl'; import { history } from '@umijs/max'; -import { Button, Form, Input, message, Popconfirm, Space } from 'antd'; +import { Button, Form, Input, message, Popconfirm, Space, Modal } from 'antd'; import { useContext, useEffect, useState } from 'react'; import ProjectContext from '../../ProjectContext'; +import { isProjectArchived } from '@/page/Project/helper'; +import { getUnfinishedTickets } from '@/common/network/task'; +import TaskList from './TaskList'; +import DeleteProjectModal from '@/page/Project/components/DeleteProjectModal.tsx'; export default function Info() { const [form] = Form.useForm>(); const context = useContext(ProjectContext); const [isModify, setIsModify] = useState(false); + const projectArchived = isProjectArchived(context.project); + const [openDeleteProjectModal, setOpenDeleteProjectModal] = useState(false); useEffect(() => { if (context.project) { @@ -49,19 +55,63 @@ export default function Info() { } } - async function deleteProject() { - const isSuccess = await setProjectAchived({ - projectId: context?.projectId, - archived: true, - }); - if (!isSuccess) { - return; + const handleProjectAchived = async () => { + const res = await getUnfinishedTickets(context.projectId); + const tatolUnfinishedTicketsCount = + res?.unfinishedFlowInstances?.length + res?.unfinishedSchedules?.length; + if (tatolUnfinishedTicketsCount > 0) { + Modal.error({ + title: '项目存在未完成的工单,暂不支持归档', + width: 500, + content: ( + <> +
{`以下 ${tatolUnfinishedTicketsCount} 个工单未完成:`}
+ {res?.unfinishedFlowInstances?.length > 0 && ( + + + + )} + {res?.unfinishedSchedules?.length > 0 && ( + + + + )} + + ), + }); + } else { + Modal.confirm({ + title: '确定要归档这个项目吗?', + content: '项目归档后将不可恢复,但仍保留相关数据,可前往归档项目中查看项目。', + okText: formatMessage({ + id: 'app.button.ok', + defaultMessage: '确定', + }), + cancelText: formatMessage({ + id: 'app.button.cancel', + defaultMessage: '取消', + }), + onOk: async () => { + const isSuccess = await setProjectAchived({ + projectId: context?.projectId, + archived: true, + }); + if (!isSuccess) { + return; + } + message.success( + formatMessage({ + id: 'odc.Setting.Info.OperationSucceeded', + defaultMessage: '操作成功', + }), //操作成功 + ); + history.push('/project'); + }, + }); } - message.success( - formatMessage({ id: 'odc.Setting.Info.OperationSucceeded', defaultMessage: '操作成功' }), //操作成功 - ); - history.push('/project'); - } + }; return (
@@ -87,6 +137,7 @@ export default function Info() { id: 'odc.Setting.Info.EnterAName', defaultMessage: '请输入名称', })} + disabled={projectArchived} /*请输入名称*/ style={{ width: 400 }} /> @@ -103,6 +154,7 @@ export default function Info() { defaultMessage: '请输入描述', })} /*请输入描述*/ style={{ width: 480 }} + disabled={projectArchived} autoSize={{ minRows: 4, maxRows: 8 }} /> @@ -116,16 +168,13 @@ export default function Info() { }) /*确认修改*/ } - - + ) : ( + - + )} + { + history.push('/project'); + }} + />
); } diff --git a/src/page/Project/User/index.tsx b/src/page/Project/User/index.tsx index 2d6a52b46..5681cb63a 100644 --- a/src/page/Project/User/index.tsx +++ b/src/page/Project/User/index.tsx @@ -32,6 +32,7 @@ import ProjectContext from '../ProjectContext'; import AddUserModal from './AddUserModal'; import ManageModal from './ManageModal'; import UpdateUserModal from './UpdateUserModal'; +import { isProjectArchived } from '@/page/Project/helper'; export const projectRoleTextMap = { [ProjectRole.OWNER]: formatMessage({ id: 'odc.User.AddUserModal.Administrator', @@ -58,6 +59,7 @@ interface IProps { const User: React.FC = ({ id, userStore }) => { const context = useContext(ProjectContext); const { project } = context; + const projectArchived = isProjectArchived(project); const isOwner = project?.currentUserResourceRoles?.some((item) => item === ProjectRole.OWNER); const isDBA = project?.currentUserResourceRoles?.some((item) => item === ProjectRole.DBA); const [addUserModalVisiable, setAddUserModalVisiable] = useState(false); @@ -121,30 +123,31 @@ const User: React.FC = ({ id, userStore }) => { setManageModalVisiable(false); } - return ( - - - + isOwner + ? '' + : formatMessage({ + id: 'src.page.Project.User.0C0586E8', + defaultMessage: '暂无权限', + }) } + > + + + ); + + return ( + @@ -209,8 +212,9 @@ const User: React.FC = ({ id, userStore }) => { //操作 dataIndex: 'name', width: 135, + hide: projectArchived, render(_, record) { - const disabled = !isOwner + const disabled = !isOwner; const isMe = userStore?.user?.id === record.id; return ( diff --git a/src/page/Project/components/DeleteProjectModal.tsx/index.tsx b/src/page/Project/components/DeleteProjectModal.tsx/index.tsx new file mode 100644 index 000000000..5fe8ea406 --- /dev/null +++ b/src/page/Project/components/DeleteProjectModal.tsx/index.tsx @@ -0,0 +1,103 @@ +import { Modal, Input, Alert, Form, Descriptions, message } from 'antd'; +import { batchDeleteProject } from '@/common/network/project'; +import { formatMessage } from '@/util/intl'; +import { useEffect, useState } from 'react'; + +export type SelectProject = { + id: number; + name: string; +}; + +interface DeleteProjectModalIProps { + open: boolean; + setOpen: React.Dispatch>; + projectList: SelectProject[]; + verifyValue: string; + beforeDelete?: () => void; +} +const DeleteProjectModal: React.FC = (props) => { + const { open, setOpen, projectList, verifyValue, beforeDelete } = props; + const [form] = Form.useForm<{ + verifyFields: string; + }>(); + + useEffect(() => { + if (!open) { + form.resetFields(); + } + }, [open]); + + const handleOK = async () => { + const value = await form.validateFields(); + const { verifyFields } = value; + if (verifyFields === verifyValue) { + const ids = projectList.map((i) => i.id); + const isSuccess = await batchDeleteProject(ids); + if (!isSuccess) { + return; + } + message.success( + formatMessage({ + id: 'odc.Setting.Info.OperationSucceeded', + defaultMessage: '操作成功', + }), //操作成功 + ); + beforeDelete?.(); + setOpen(false); + } else { + form.setFields([ + { + name: 'verifyFields', + value: verifyFields, + errors: [`请输入 ${verifyValue} `], + }, + ]); + } + }; + + return ( + { + setOpen(false); + }} + onOk={handleOK} + okText={'删除'} + okType={'danger'} + > + + + + {projectList.map((item) => item.name).join('; ')} + + +
+ + 请输入 {verifyValue} 以确认操作 + + } + style={{ marginBottom: '0px' }} + rules={[ + { + required: true, + message: '请输入', + }, + ]} + > + + +
+
+ ); +}; + +export default DeleteProjectModal; diff --git a/src/page/Project/helper.ts b/src/page/Project/helper.ts new file mode 100644 index 000000000..55dcec64b --- /dev/null +++ b/src/page/Project/helper.ts @@ -0,0 +1,16 @@ +import { IProject } from '@/d.ts/project'; + +/** + * 已归档项目 + * 数据库页:隐藏顶部按钮、列表checkbox,操作列以及列数据库名称不可点击 + * 工单页: 隐藏新建按钮 + * 成员页:隐藏新建按钮和操作列 + * 敏感表页:隐藏添加、checkbox、操作列 + * 消息页: + * 隐藏tab-推送规则下的列表checkbox,启用状态、操作列 + * 隐藏tab-推送通道下的新建按钮,操作列 + * 设置页:禁用项目名称修改、描述、展示操作按钮删除项目 + */ +export const isProjectArchived = (project: IProject) => { + return !!project?.archived; +};