From 14308f39b61d078e39eeee0878fe286ca3b6ba45 Mon Sep 17 00:00:00 2001 From: zzyangh <799463087@qq.com> Date: Thu, 17 Apr 2025 16:57:34 +0800 Subject: [PATCH 1/8] [chore]: Update api --- packages/shared/lib/api/base/index.ts | 1 + .../base/service/ResourceOverview/index.d.ts | 50 ++++++++ .../base/service/ResourceOverview/index.ts | 62 +++++++++ .../shared/lib/api/base/service/common.d.ts | 120 +++++++++++++----- 4 files changed, 204 insertions(+), 29 deletions(-) create mode 100644 packages/shared/lib/api/base/service/ResourceOverview/index.d.ts create mode 100644 packages/shared/lib/api/base/service/ResourceOverview/index.ts diff --git a/packages/shared/lib/api/base/index.ts b/packages/shared/lib/api/base/index.ts index 3f29c48ddc..e81b5bf974 100644 --- a/packages/shared/lib/api/base/index.ts +++ b/packages/shared/lib/api/base/index.ts @@ -22,3 +22,4 @@ export { default as SMSService } from './service/SMS'; export { default as UserService } from './service/User'; export { default as UserGroupService } from './service/UserGroup'; export { default as WebhookService } from './service/Webhook'; +export { default as ResourceOverviewService } from './service/ResourceOverview'; diff --git a/packages/shared/lib/api/base/service/ResourceOverview/index.d.ts b/packages/shared/lib/api/base/service/ResourceOverview/index.d.ts new file mode 100644 index 0000000000..b3d379b6b6 --- /dev/null +++ b/packages/shared/lib/api/base/service/ResourceOverview/index.d.ts @@ -0,0 +1,50 @@ +import { + IResourceOverviewResourceListResV1, + IResourceOverviewResourceTypeDistributionResV1, + IResourceOverviewStatisticsResV1, + IResourceOverviewTopologyResV1 +} from '../common.d'; + +export interface IGetResourceOverviewResourceListV1Params { + filter_by_db_type?: string; + + filter_by_business_tag_uid?: string; + + filter_by_environment_tag_uid?: string; + + filter_by_project_uid?: string; + + fuzzy_search_resource_name?: string; + + sort_by_field?: string; + + sort_asc?: boolean; + + page_index?: number; + + page_size?: number; +} + +export interface IGetResourceOverviewResourceListV1Return + extends IResourceOverviewResourceListResV1 {} + +export interface IGetResourceOverviewResourceTypeDistributionV1Return + extends IResourceOverviewResourceTypeDistributionResV1 {} + +export interface IGetResourceOverviewStatisticsV1Return + extends IResourceOverviewStatisticsResV1 {} + +export interface IGetResourceOverviewTopologyV1Params { + filter_by_db_type?: string; + + filter_by_business_tag_uid?: string; + + filter_by_environment_tag_uid?: string; + + filter_by_project_uid?: string; + + fuzzy_search_resource_name?: string; +} + +export interface IGetResourceOverviewTopologyV1Return + extends IResourceOverviewTopologyResV1 {} diff --git a/packages/shared/lib/api/base/service/ResourceOverview/index.ts b/packages/shared/lib/api/base/service/ResourceOverview/index.ts new file mode 100644 index 0000000000..2f59f6218e --- /dev/null +++ b/packages/shared/lib/api/base/service/ResourceOverview/index.ts @@ -0,0 +1,62 @@ +/* tslint:disable no-identical-functions */ +/* tslint:disable no-useless-cast */ +/* tslint:disable no-unnecessary-type-assertion */ +/* tslint:disable no-big-function */ +/* tslint:disable no-duplicate-string */ +import ServiceBase from '../Service.base'; +import { AxiosRequestConfig } from 'axios'; + +import { + IGetResourceOverviewResourceListV1Params, + IGetResourceOverviewResourceListV1Return, + IGetResourceOverviewResourceTypeDistributionV1Return, + IGetResourceOverviewStatisticsV1Return, + IGetResourceOverviewTopologyV1Params, + IGetResourceOverviewTopologyV1Return +} from './index.d'; + +class ResourceOverviewService extends ServiceBase { + public GetResourceOverviewResourceListV1( + params: IGetResourceOverviewResourceListV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + return this.get( + '/v1/dms/resource_overview/resource_list', + paramsData, + options + ); + } + + public GetResourceOverviewResourceTypeDistributionV1( + options?: AxiosRequestConfig + ) { + return this.get( + '/v1/dms/resource_overview/resource_type_distribution', + undefined, + options + ); + } + + public GetResourceOverviewStatisticsV1(options?: AxiosRequestConfig) { + return this.get( + '/v1/dms/resource_overview/statistics', + undefined, + options + ); + } + + public GetResourceOverviewTopologyV1( + params: IGetResourceOverviewTopologyV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + return this.get( + '/v1/dms/resource_overview/topology', + paramsData, + options + ); + } +} + +export default new ResourceOverviewService(); diff --git a/packages/shared/lib/api/base/service/common.d.ts b/packages/shared/lib/api/base/service/common.d.ts index 72131b5a65..4b5832456d 100644 --- a/packages/shared/lib/api/base/service/common.d.ts +++ b/packages/shared/lib/api/base/service/common.d.ts @@ -1073,34 +1073,6 @@ export interface IImportDBService { user?: string; } -export interface IImportDBServiceCommon { - additional_params?: IAdditionalParam[]; - - db_type?: string; - - desc?: string; - - host?: string; - - is_enable_masking?: boolean; - - maintenance_times?: IMaintenanceTime[]; - - name?: string; - - password?: string; - - port?: string; - - project_uid?: string; - - source?: string; - - sqle_config?: ISQLEConfig; - - user?: string; -} - export interface IImportDBServiceV2 { additional_params?: IAdditionalParam[]; @@ -1108,7 +1080,7 @@ export interface IImportDBServiceV2 { desc?: string; - environment_tag_uid: string; + environment_tag_name: string; host?: string; @@ -2131,6 +2103,96 @@ export interface IRejectDataExportWorkflowReq { payload?: IRejectDataExportWorkflowPayload; } +export interface IResourceBusiness { + business_tag?: IBusinessTag; + + project?: IResourceProject[]; +} + +export interface IResourceDBService { + db_service_name?: string; + + db_service_uid?: string; +} + +export interface IResourceListData { + audit_score?: number; + + business_tag?: IBusinessTag; + + environment_tag?: IEnvironmentTag; + + high_priority_sql_count?: number; + + pending_workflow_count?: number; + + project?: IResourceProject; + + resource_name?: string; + + resource_type?: string; + + resource_uid?: string; +} + +export interface IResourceOverviewResourceListResV1 { + code?: number; + + data?: IResourceListData[]; + + message?: string; + + total_nums?: number; +} + +export interface IResourceOverviewResourceTypeDistributionResV1 { + code?: number; + + data?: IResourceTypeDistributionData[]; + + message?: string; + + total_nums?: number; +} + +export interface IResourceOverviewStatisticsResV1 { + code?: number; + + data?: { + business_total_number?: number; + + db_service_total_number?: number; + + project_total_number?: number; + }; + + message?: string; +} + +export interface IResourceOverviewTopologyResV1 { + code?: number; + + data?: IResourceBusiness[]; + + message?: string; + + total_nums?: number; +} + +export interface IResourceProject { + db_service?: IResourceDBService[]; + + project_name?: string; + + project_uid?: string; +} + +export interface IResourceTypeDistributionData { + count?: number; + + resource_type?: string; +} + export interface IRole { desc?: string; From 904f4c2c5439ffca2f9f5d8441b7f784eb13eee8 Mon Sep 17 00:00:00 2001 From: zzyangh <799463087@qq.com> Date: Thu, 17 Apr 2025 16:59:38 +0800 Subject: [PATCH 2/8] [feature](EditableSelect): Adjust delete event --- .../Form/EnvironmentField/index.tsx | 76 ++++------- .../Modal/ProjectForm/BusinessField/index.tsx | 64 ++++----- .../EditableSelect/EditableSelect.tsx | 128 ++++++++++-------- .../components/EditableSelect/index.type.ts | 2 +- 4 files changed, 123 insertions(+), 147 deletions(-) diff --git a/packages/base/src/page/DataSource/components/Form/EnvironmentField/index.tsx b/packages/base/src/page/DataSource/components/Form/EnvironmentField/index.tsx index 90491c05a8..a53d8796bf 100644 --- a/packages/base/src/page/DataSource/components/Form/EnvironmentField/index.tsx +++ b/packages/base/src/page/DataSource/components/Form/EnvironmentField/index.tsx @@ -74,11 +74,8 @@ const EnvironmentField: React.FC = ({ }); }; - const deleteEnvironmentTag = ( - environmentTagUid: string, - resolve: (value: boolean) => void, - reject: () => void - ) => { + const deleteEnvironmentTag = (environmentTagUid: string) => { + startOperationLoading(); DmsApi.ProjectService.DeleteEnvironmentTag({ environment_tag_uid: environmentTagUid, project_uid: projectID ?? '' @@ -88,45 +85,32 @@ const EnvironmentField: React.FC = ({ messageApi.success( t('dmsDataSource.dataSourceForm.deleteEnvironmentAttributeSuccess') ); - resolve(true); refresh(); - } else { - reject(); } }) - .catch(() => { - reject(); + .finally(() => { + stopOperationLoading(); }); }; - //todo 目前这种写法不太友好 待和产品沟通 优化删除交互 修改此处代码 const onDelete = (item: EditableSelectOption) => { - if (boundServices.length > 0) { - return Promise.resolve(false); - } const environmentTagUid = item.value.toString(); - return new Promise((resolve, reject) => { - DmsApi.DBServiceService.ListDBServicesV2({ - filter_by_environment_tag_uid: environmentTagUid, - project_uid: projectID ?? '', - page_size: 9999 - }) - .then((res) => { - if (res.data.code === ResponseCode.SUCCESS) { - setBoundServices(res.data.data ?? []); - if (res.data.data?.length === 0) { - deleteEnvironmentTag(environmentTagUid, resolve, reject); - } else { - reject(); - } - } else { - reject(); + DmsApi.DBServiceService.ListDBServicesV2({ + filter_by_environment_tag_uid: environmentTagUid, + project_uid: projectID ?? '', + page_size: 9999 + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + setBoundServices(res.data.data ?? []); + if (res.data.data?.length === 0) { + deleteEnvironmentTag(environmentTagUid); } - }) - .catch(() => { - reject(); - }); - }); + } + }) + .finally(() => { + stopOperationLoading(); + }); }; const onUpdate = (item: EditableSelectOption) => { @@ -158,7 +142,14 @@ const EnvironmentField: React.FC = ({ addButtonText={t( 'dmsDataSource.dataSourceForm.addEnvironmentAttribute' )} - deletionConfirmTitle={ + deletionConfirmTitle={t( + 'dmsDataSource.dataSourceForm.deleteEnvironmentAttributeConfirm' + )} + onDelete={onDelete} + onUpdate={onUpdate} + onAdd={onAdd} + loading={operationLoading || loading} + errorMessage={ boundServices.length > 0 ? t( 'dmsDataSource.dataSourceForm.deleteEnvironmentAttributeError', @@ -166,19 +157,8 @@ const EnvironmentField: React.FC = ({ name: boundServices.map((item) => item.name).join(',') } ) - : t( - 'dmsDataSource.dataSourceForm.deleteEnvironmentAttributeConfirm' - ) + : undefined } - onDelete={onDelete} - onUpdate={onUpdate} - onAdd={onAdd} - loading={operationLoading || loading} - onConfirmOpenChange={(open) => { - if (!open) { - setBoundServices([]); - } - }} /> ); diff --git a/packages/base/src/page/Project/Modal/ProjectForm/BusinessField/index.tsx b/packages/base/src/page/Project/Modal/ProjectForm/BusinessField/index.tsx index 71275277b2..bb4a78e70d 100644 --- a/packages/base/src/page/Project/Modal/ProjectForm/BusinessField/index.tsx +++ b/packages/base/src/page/Project/Modal/ProjectForm/BusinessField/index.tsx @@ -67,53 +67,39 @@ const BusinessField = (props: BusinessSelectorProps) => { }); }; - const deleteBusinessTag = ( - id: string, - resolve: (value: boolean) => void, - reject: () => void - ) => { + const deleteBusinessTag = (id: string) => { + startOperationLoading(); DmsApi.ProjectService.DeleteBusinessTag({ business_tag_uid: id }) .then((res) => { if (res.data.code === ResponseCode.SUCCESS) { messageApi.success(t('dmsProject.projectForm.deleteBusinessSuccess')); - resolve(true); refresh(); - } else { - reject(); } }) - .catch(() => { - reject(); + .finally(() => { + stopOperationLoading(); }); }; const onDelete = (item: EditableSelectOption) => { - if (boundProjectList.length > 0) { - return Promise.resolve(false); - } - return new Promise((resolve, reject) => { - DmsApi.ProjectService.ListProjectsV2({ - filter_by_business_tag: item.label, - page_size: 9999 - }) - .then((res) => { - if (res.data.code === ResponseCode.SUCCESS) { - setBoundProjectList(res.data.data ?? []); - if (res.data.data?.length === 0) { - deleteBusinessTag(item.value.toString(), resolve, reject); - } else { - reject(); - } - } else { - reject(); + startOperationLoading(); + DmsApi.ProjectService.ListProjectsV2({ + filter_by_business_tag: item.label, + page_size: 9999 + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + setBoundProjectList(res.data.data ?? []); + if (res.data.data?.length === 0) { + deleteBusinessTag(item.value.toString()); } - }) - .catch(() => { - reject(); - }); - }); + } + }) + .finally(() => { + stopOperationLoading(); + }); }; const onUpdate = (item: EditableSelectOption) => { @@ -145,19 +131,17 @@ const BusinessField = (props: BusinessSelectorProps) => { onDelete={onDelete} onUpdate={onUpdate} addButtonText={t('dmsProject.projectForm.addBusiness')} - deletionConfirmTitle={ + deletionConfirmTitle={t( + 'dmsProject.projectForm.deleteBusinessConfirmTitle' + )} + errorMessage={ boundProjectList.length > 0 ? t('dmsProject.projectForm.deleteBusinessError', { name: boundProjectList.map((item) => item.name).join(',') }) - : t('dmsProject.projectForm.deleteBusinessConfirmTitle') + : undefined } loading={operationLoading || loading} - onConfirmOpenChange={(open) => { - if (!open) { - setBoundProjectList([]); - } - }} /> ); diff --git a/packages/shared/lib/components/EditableSelect/EditableSelect.tsx b/packages/shared/lib/components/EditableSelect/EditableSelect.tsx index 724b4c9d3e..1fe1275b08 100644 --- a/packages/shared/lib/components/EditableSelect/EditableSelect.tsx +++ b/packages/shared/lib/components/EditableSelect/EditableSelect.tsx @@ -18,6 +18,7 @@ import { DownOutlined } from '@actiontech/icons'; import type { MenuProps } from 'antd'; +import { ReminderInformation } from '../ReminderInformation'; const EditableSelect: React.FC = ({ value, @@ -34,7 +35,7 @@ const EditableSelect: React.FC = ({ addable = true, updatable = true, deletable = true, - onConfirmOpenChange + errorMessage }) => { const { t } = useTranslation(); const [newItemName, setNewItemName] = useState(''); @@ -44,6 +45,9 @@ const EditableSelect: React.FC = ({ ); const [editName, setEditName] = useState(''); const [dropdownOpen, setDropdownOpen] = useState(false); + const [deletingItem, setDeletingItem] = useState( + null + ); const handleAdd = () => { onAdd?.(newItemName); @@ -63,6 +67,7 @@ const EditableSelect: React.FC = ({ const startEditing = (item: EditableSelectOption) => { setEditingItem(item); setEditName(item.label); + setDeletingItem(null); }; const cancelEditing = () => { @@ -72,6 +77,7 @@ const EditableSelect: React.FC = ({ const startAdding = () => { setIsAdding(true); + setDeletingItem(null); }; const resetState = () => { @@ -79,6 +85,7 @@ const EditableSelect: React.FC = ({ setIsAdding(false); setEditingItem(null); setEditName(''); + setDeletingItem(null); }; const onOpenChange = (open: boolean) => { @@ -100,73 +107,78 @@ const EditableSelect: React.FC = ({ resetState(); }, label: ( -
- {editingItem?.value === option.value ? ( -
e.stopPropagation()}> - setEditName(e.target.value)} - /> -
- - {t('common.cancel')} - - - {t('common.save')} - -
-
- ) : ( - <> - {option.label} - - +
+
+ {editingItem?.value === option.value ? ( +
e.stopPropagation()}> + setEditName(e.target.value)} + /> +
+ + {t('common.cancel')} + } - onClick={(e) => { - e.stopPropagation(); - startEditing(option); - }} - className="edit-button" - /> - - - { - e?.stopPropagation(); - return onDelete?.(option); - }} - onCancel={(e) => { - e?.stopPropagation(); - }} - onOpenChange={onConfirmOpenChange} + onClick={handleUpdate} + disabled={!editName.trim()} > + {t('common.save')} + +
+
+ ) : ( + <> + {option.label} + + } + icon={} onClick={(e) => { e.stopPropagation(); + startEditing(option); }} - className="delete-button" + className="edit-button" /> - - - - - )} + + + { + e?.stopPropagation(); + setDeletingItem(option); + onDelete?.(option); + }} + onCancel={(e) => { + e?.stopPropagation(); + }} + > + } + onClick={(e) => { + e.stopPropagation(); + }} + className="delete-button" + /> + + + + + )} +
+ + +
) })); diff --git a/packages/shared/lib/components/EditableSelect/index.type.ts b/packages/shared/lib/components/EditableSelect/index.type.ts index f56acf0df3..6777ef18de 100644 --- a/packages/shared/lib/components/EditableSelect/index.type.ts +++ b/packages/shared/lib/components/EditableSelect/index.type.ts @@ -22,5 +22,5 @@ export interface EditableSelectProps { addButtonText?: string; placeholder?: string; disabled?: boolean; - onConfirmOpenChange?: (open: boolean) => void; + errorMessage?: string; } From ecb1e06278b9db44e1a53897c21fcef890023f57 Mon Sep 17 00:00:00 2001 From: zzyangh <799463087@qq.com> Date: Thu, 17 Apr 2025 17:01:04 +0800 Subject: [PATCH 3/8] [test]: Update unit testing --- .../__snapshots__/index.test.tsx.snap | 384 +++++++++--------- .../EnvironmentField/__tests__/index.test.tsx | 3 - .../__snapshots__/index.test.tsx.snap | 200 ++++----- .../BusinessField/__tests__/index.test.tsx | 4 - .../src/testUtils/mockApi/project/data.ts | 4 +- .../__tests__/EditableSelect.test.tsx | 22 + .../EditableSelect.test.tsx.snap | 192 ++++----- 7 files changed, 420 insertions(+), 389 deletions(-) diff --git a/packages/base/src/page/DataSource/components/AddDataSource/__snapshots__/index.test.tsx.snap b/packages/base/src/page/DataSource/components/AddDataSource/__snapshots__/index.test.tsx.snap index f6aa26893d..a654126ea4 100644 --- a/packages/base/src/page/DataSource/components/AddDataSource/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/DataSource/components/AddDataSource/__snapshots__/index.test.tsx.snap @@ -2612,63 +2612,65 @@ exports[`page/DataSource/AddDataSource render submit when add database api succe - @@ -2683,63 +2685,65 @@ exports[`page/DataSource/AddDataSource render submit when add database api succe - @@ -6731,63 +6735,65 @@ exports[`page/DataSource/AddDataSource render submit when projectID is undefined - @@ -6802,63 +6808,65 @@ exports[`page/DataSource/AddDataSource render submit when projectID is undefined - diff --git a/packages/base/src/page/DataSource/components/Form/EnvironmentField/__tests__/index.test.tsx b/packages/base/src/page/DataSource/components/Form/EnvironmentField/__tests__/index.test.tsx index 3c57346c45..ada18e271e 100644 --- a/packages/base/src/page/DataSource/components/Form/EnvironmentField/__tests__/index.test.tsx +++ b/packages/base/src/page/DataSource/components/Form/EnvironmentField/__tests__/index.test.tsx @@ -90,8 +90,6 @@ describe('Project/EnvironmentField', () => { }); it('render delete environment tag when environment tag is bound', async () => { - const mockPromiseRejected = jest.spyOn(Promise, 'reject'); - mockPromiseRejected.mockImplementation(() => Promise.resolve(false)); customRender(); await act(async () => jest.advanceTimersByTime(3000)); expect(listEnvironmentTagsSpy).toHaveBeenCalledTimes(1); @@ -111,7 +109,6 @@ describe('Project/EnvironmentField', () => { project_uid: mockProjectInfo.projectID }); await act(async () => jest.advanceTimersByTime(3000)); - expect(mockPromiseRejected).toHaveBeenCalledTimes(1); expect( screen.getByText( `当前环境已绑定:${DBServicesList.map((item) => item.name).join( diff --git a/packages/base/src/page/DataSource/components/UpdateDataSource/__snapshots__/index.test.tsx.snap b/packages/base/src/page/DataSource/components/UpdateDataSource/__snapshots__/index.test.tsx.snap index 41d9e42e5a..c04fd9c9e5 100644 --- a/packages/base/src/page/DataSource/components/UpdateDataSource/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/DataSource/components/UpdateDataSource/__snapshots__/index.test.tsx.snap @@ -4623,63 +4623,65 @@ exports[`page/DataSource/UpdateDataSource render edit data when has all value 3` - @@ -4694,63 +4696,65 @@ exports[`page/DataSource/UpdateDataSource render edit data when has all value 3` - diff --git a/packages/base/src/page/Project/Modal/ProjectForm/BusinessField/__tests__/index.test.tsx b/packages/base/src/page/Project/Modal/ProjectForm/BusinessField/__tests__/index.test.tsx index 792419d3f3..4f98c11da9 100644 --- a/packages/base/src/page/Project/Modal/ProjectForm/BusinessField/__tests__/index.test.tsx +++ b/packages/base/src/page/Project/Modal/ProjectForm/BusinessField/__tests__/index.test.tsx @@ -83,10 +83,7 @@ describe('Project/BusinessField', () => { expect(screen.getByText('更新业务成功')).toBeInTheDocument(); }); - // todo 删除时有多种情况会rejected 暂时只覆盖一种 后续补充其他case it('render delete business tag when business tag is bound', async () => { - const mockPromiseRejected = jest.spyOn(Promise, 'reject'); - mockPromiseRejected.mockImplementation(() => Promise.resolve(false)); superRender(); await act(async () => jest.advanceTimersByTime(3000)); expect(listBusinessTagsSpy).toHaveBeenCalledTimes(1); @@ -105,7 +102,6 @@ describe('Project/BusinessField', () => { page_size: 9999 }); await act(async () => jest.advanceTimersByTime(3000)); - expect(mockPromiseRejected).toHaveBeenCalledTimes(1); expect( screen.getByText( `当前业务已绑定:${mockProjectList diff --git a/packages/base/src/testUtils/mockApi/project/data.ts b/packages/base/src/testUtils/mockApi/project/data.ts index 517ccc784e..f6399f0f04 100644 --- a/packages/base/src/testUtils/mockApi/project/data.ts +++ b/packages/base/src/testUtils/mockApi/project/data.ts @@ -98,7 +98,7 @@ export const mockBatchImportDBCheckData: IImportDBServiceV2[] = [ port: '3306', user: 'root', password: '123456', - environment_tag_uid: '1', + environment_tag_name: 'environment-1', maintenance_times: [ { maintenance_start_time: { @@ -130,7 +130,7 @@ export const mockBatchImportDBCheckData: IImportDBServiceV2[] = [ port: '1521', user: 'system', password: '123456', - environment_tag_uid: '2', + environment_tag_name: 'environment-2', maintenance_times: [], desc: 'oracle_1', sqle_config: { diff --git a/packages/shared/lib/components/EditableSelect/__tests__/EditableSelect.test.tsx b/packages/shared/lib/components/EditableSelect/__tests__/EditableSelect.test.tsx index e28d319888..3a62acb156 100644 --- a/packages/shared/lib/components/EditableSelect/__tests__/EditableSelect.test.tsx +++ b/packages/shared/lib/components/EditableSelect/__tests__/EditableSelect.test.tsx @@ -220,6 +220,28 @@ describe('EditableSelect', () => { expect(mockOnDelete).toHaveBeenCalledWith(mockOptions[0]); }); + it('render error message when confirm delete failed', async () => { + const errorMessage = '当前数据不可删除'; + customRender({ + errorMessage + }); + fireEvent.click(getBySelector('.editable-select-trigger')); + await act(async () => jest.advanceTimersByTime(0)); + + const deleteButton = getAllBySelector('.delete-button')[0]; + fireEvent.click(deleteButton); + await act(async () => jest.advanceTimersByTime(0)); + expect(screen.getByText('确认要删除么?')).toBeInTheDocument(); + + const confirmButton = getBySelector('.ant-btn-primary.ant-btn-sm'); + fireEvent.click(confirmButton); + await act(async () => jest.advanceTimersByTime(0)); + + expect(mockOnDelete).toHaveBeenCalledWith(mockOptions[0]); + + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + }); + it('should not show actions when disabled', async () => { customRender({ disabled: true diff --git a/packages/shared/lib/components/EditableSelect/__tests__/__snapshots__/EditableSelect.test.tsx.snap b/packages/shared/lib/components/EditableSelect/__tests__/__snapshots__/EditableSelect.test.tsx.snap index 308f9f297a..48d6bc1667 100644 --- a/packages/shared/lib/components/EditableSelect/__tests__/__snapshots__/EditableSelect.test.tsx.snap +++ b/packages/shared/lib/components/EditableSelect/__tests__/__snapshots__/EditableSelect.test.tsx.snap @@ -90,63 +90,65 @@ exports[`EditableSelect should render select with options 2`] = ` - @@ -161,63 +163,65 @@ exports[`EditableSelect should render select with options 2`] = ` - From e3edaca60811136efa64f9d04a71dfde1a5a22aa Mon Sep 17 00:00:00 2001 From: zzyangh <799463087@qq.com> Date: Thu, 17 Apr 2025 17:08:49 +0800 Subject: [PATCH 4/8] [feature](ResourceOverview): Add resource overview page --- packages/base/src/data/EmitterKey.ts | 3 +- .../base/src/hooks/useBusinessTag/index.tsx | 48 +++ .../base/src/hooks/useProjectTips/index.ts | 35 ++- packages/base/src/locale/zh-CN/dmsProject.ts | 3 +- packages/base/src/locale/zh-CN/index.ts | 5 +- .../base/src/locale/zh-CN/resourceOverview.ts | 36 +++ packages/base/src/page/Project/action.tsx | 21 +- packages/base/src/page/Project/index.tsx | 1 + .../ResourceDetail/ResourceTopology/index.tsx | 120 ++++++++ .../components/ResourceDetail/actions.tsx | 35 +++ .../components/ResourceDetail/columns.tsx | 75 +++++ .../ResourceDetail/hooks/useFilterParams.ts | 113 +++++++ .../components/ResourceDetail/index.tsx | 275 ++++++++++++++++++ .../Statistic/BaseInfo/StatisticCard.tsx | 19 ++ .../components/Statistic/BaseInfo/index.tsx | 86 ++++++ .../Statistic/DistributionChart/index.tsx | 219 ++++++++++++++ .../components/Statistic/index.tsx | 26 ++ .../base/src/page/ResourceOverview/index.tsx | 36 +++ .../src/page/ResourceOverview/index.type.ts | 5 + .../base/src/page/ResourceOverview/style.ts | 80 +++++ packages/base/src/router/router.base.tsx | 8 + packages/base/src/theme/dark/index.ts | 4 +- .../base/src/theme/dark/resourceOverview.ts | 14 + packages/base/src/theme/light/index.ts | 4 +- .../base/src/theme/light/resourceOverview.ts | 14 + packages/base/src/theme/type.ts | 13 + packages/base/src/types/theme.type.ts | 4 +- packages/shared/lib/data/routePaths.ts | 3 +- .../usePermission/permissionManifest.ts | 20 ++ .../lib/features/usePermission/permissions.ts | 7 +- 30 files changed, 1307 insertions(+), 25 deletions(-) create mode 100644 packages/base/src/hooks/useBusinessTag/index.tsx create mode 100644 packages/base/src/locale/zh-CN/resourceOverview.ts create mode 100644 packages/base/src/page/ResourceOverview/components/ResourceDetail/ResourceTopology/index.tsx create mode 100644 packages/base/src/page/ResourceOverview/components/ResourceDetail/actions.tsx create mode 100644 packages/base/src/page/ResourceOverview/components/ResourceDetail/columns.tsx create mode 100644 packages/base/src/page/ResourceOverview/components/ResourceDetail/hooks/useFilterParams.ts create mode 100644 packages/base/src/page/ResourceOverview/components/ResourceDetail/index.tsx create mode 100644 packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/StatisticCard.tsx create mode 100644 packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/index.tsx create mode 100644 packages/base/src/page/ResourceOverview/components/Statistic/DistributionChart/index.tsx create mode 100644 packages/base/src/page/ResourceOverview/components/Statistic/index.tsx create mode 100644 packages/base/src/page/ResourceOverview/index.tsx create mode 100644 packages/base/src/page/ResourceOverview/index.type.ts create mode 100644 packages/base/src/page/ResourceOverview/style.ts create mode 100644 packages/base/src/theme/dark/resourceOverview.ts create mode 100644 packages/base/src/theme/light/resourceOverview.ts diff --git a/packages/base/src/data/EmitterKey.ts b/packages/base/src/data/EmitterKey.ts index 492004229c..f5a08fb494 100644 --- a/packages/base/src/data/EmitterKey.ts +++ b/packages/base/src/data/EmitterKey.ts @@ -13,7 +13,8 @@ enum EmitterKey { DMS_Refresh_Export_Data_Workflow = 'DMS_Refresh_Export_Data_Workflow', DMS_Refresh_Sync_Data_Source = 'DMS_Refresh_Sync_Data_Source', DMS_Refresh_Global_Data_Source = 'DMS_Refresh_Global_Data_Source', - DMS_Batch_Test_Data_Source_Connection = 'DMS_Batch_Test_Data_Source_Connection' + DMS_Batch_Test_Data_Source_Connection = 'DMS_Batch_Test_Data_Source_Connection', + Refresh_Resource_Overview_Page = 'Refresh_Resource_Overview_Page' } export default EmitterKey; diff --git a/packages/base/src/hooks/useBusinessTag/index.tsx b/packages/base/src/hooks/useBusinessTag/index.tsx new file mode 100644 index 0000000000..3707690161 --- /dev/null +++ b/packages/base/src/hooks/useBusinessTag/index.tsx @@ -0,0 +1,48 @@ +import { useRequest } from 'ahooks'; +import { DmsApi } from '@actiontech/shared/lib/api'; +import { ResponseCode } from '@actiontech/shared/lib/enum'; +import { useMemo } from 'react'; + +const useBusinessTag = () => { + const { + data, + loading, + run: updateBusinessTagList + } = useRequest( + () => { + return DmsApi.ProjectService.ListBusinessTags({ + page_size: 1000 + }) + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + return res.data.data; + } + return []; + }) + .catch(() => { + return []; + }); + }, + { + manual: true + } + ); + + const businessTagOptions = useMemo(() => { + return ( + data?.map((businessTag) => ({ + label: businessTag.name, + value: businessTag.uid + })) ?? [] + ); + }, [data]); + + return { + businessTagList: data ?? [], + businessTagOptions, + loading, + updateBusinessTagList + }; +}; + +export default useBusinessTag; diff --git a/packages/base/src/hooks/useProjectTips/index.ts b/packages/base/src/hooks/useProjectTips/index.ts index 5c2705f28b..c4369ca224 100644 --- a/packages/base/src/hooks/useProjectTips/index.ts +++ b/packages/base/src/hooks/useProjectTips/index.ts @@ -3,26 +3,33 @@ import { DmsApi } from '@actiontech/shared/lib/api'; import { ResponseCode } from '@actiontech/shared/lib/enum'; import { useBoolean } from 'ahooks'; import { IListProjectV2 } from '@actiontech/shared/lib/api/base/service/common'; +import { IListProjectsV2Params } from '@actiontech/shared/lib/api/base/service/Project/index.d'; const useProjectTips = () => { const [projectList, setProjectList] = useState([]); const [loading, { setTrue, setFalse }] = useBoolean(); - const updateProjects = useCallback(() => { - setTrue(); - DmsApi.ProjectService.ListProjectsV2({ page_size: 9999 }) - .then((res) => { - if (res.data.code === ResponseCode.SUCCESS) { - setProjectList(res.data.data ?? []); - } + const updateProjects = useCallback( + (params?: Omit) => { + setTrue(); + DmsApi.ProjectService.ListProjectsV2({ + ...(params ?? {}), + page_size: 9999 }) - .catch(() => { - setProjectList([]); - }) - .finally(() => { - setFalse(); - }); - }, [setFalse, setTrue]); + .then((res) => { + if (res.data.code === ResponseCode.SUCCESS) { + setProjectList(res.data.data ?? []); + } + }) + .catch(() => { + setProjectList([]); + }) + .finally(() => { + setFalse(); + }); + }, + [setFalse, setTrue] + ); const projectIDOptions = useMemo(() => { return projectList.map((project) => { diff --git a/packages/base/src/locale/zh-CN/dmsProject.ts b/packages/base/src/locale/zh-CN/dmsProject.ts index 889bbaa970..ad37ed5c1d 100644 --- a/packages/base/src/locale/zh-CN/dmsProject.ts +++ b/packages/base/src/locale/zh-CN/dmsProject.ts @@ -89,5 +89,6 @@ export default { '业务:在项目的基础上,业务作为子项目或特定业务领域的标签,帮助您进一步组织和细化数据资源。业务层使您能够针对特定需求进行数据的集中管理和操作。', dataSource: '数据源:数据源是构建在业务基础上的实际操作对象。您可以在数据源层面执行具体的数据操作,如查询、更新和管理数据。' - } + }, + resourceOverview: '资源全景视图' }; diff --git a/packages/base/src/locale/zh-CN/index.ts b/packages/base/src/locale/zh-CN/index.ts index f715c66cd9..be15b1e9f9 100644 --- a/packages/base/src/locale/zh-CN/index.ts +++ b/packages/base/src/locale/zh-CN/index.ts @@ -13,7 +13,7 @@ import dmsMenu from './dmsMenu'; import dmsAccount from './dmsAccount'; import dmsDataExport from './dmsDataExport'; import dmsGlobalDataSource from './dmsGlobalDataSource'; - +import resourceOverview from './resourceOverview'; // eslint-disable-next-line import/no-anonymous-default-export export default { translation: { @@ -30,6 +30,7 @@ export default { dmsMenu, dmsAccount, dmsDataExport, - dmsGlobalDataSource + dmsGlobalDataSource, + resourceOverview } }; diff --git a/packages/base/src/locale/zh-CN/resourceOverview.ts b/packages/base/src/locale/zh-CN/resourceOverview.ts new file mode 100644 index 0000000000..95039c5b9d --- /dev/null +++ b/packages/base/src/locale/zh-CN/resourceOverview.ts @@ -0,0 +1,36 @@ +// eslint-disable-next-line import/no-anonymous-default-export +export default { + title: '资源全景视图', + baseInfo: { + businessCount: '业务总数', + projectCount: '项目总数', + sourceCount: '数据源总数' + }, + + expandAll: '展开全部', + collapseAll: '折叠全部', + + resourceTopology: '资源拓扑', + resourceDetail: '资源详情', + selectProjectFirst: '请先选择所属项目', + unboundProject: '暂未关联项目', + resourceList: { + resourceName: '资源名称', + type: '资源类型', + business: '所属业务', + project: '所属项目', + environment: '所属环境', + auditScore: '审核质量评分', + highPrioritySqlCount: '高优先级SQL数', + pendingWorkflowCount: '待处理工单数量', + viewDetail: '查看详情' + }, + distributionChart: { + title: '资源类型分布', + emptyText: '暂无数据,购买数据源后回到本页查看', + sourceTotal: '数据源总计', + sourceNumItem: '数据源数量', + sourceProportionItem: '数据源占比' + }, + export: '导出' +}; diff --git a/packages/base/src/page/Project/action.tsx b/packages/base/src/page/Project/action.tsx index 0d70b7b1b0..264dd428f7 100644 --- a/packages/base/src/page/Project/action.tsx +++ b/packages/base/src/page/Project/action.tsx @@ -12,7 +12,11 @@ export const ProjectManagementPageHeaderActions = ( exportPending: boolean, onCreate: () => void ): Record< - 'batch_import_data_source' | 'import' | 'export' | 'create', + | 'batch_import_data_source' + | 'import' + | 'export' + | 'create' + | 'global_resource_overview', ReactNode > => { return { @@ -65,6 +69,21 @@ export const ProjectManagementPageHeaderActions = ( onClick={onCreate} /> + ), + global_resource_overview: ( + + + ) }; }; diff --git a/packages/base/src/page/Project/index.tsx b/packages/base/src/page/Project/index.tsx index 21a0c0fcc9..e57064c4f3 100644 --- a/packages/base/src/page/Project/index.tsx +++ b/packages/base/src/page/Project/index.tsx @@ -68,6 +68,7 @@ const Project: React.FC = () => { } extra={ + {headerActions.global_resource_overview} {headerActions.batch_import_data_source} {headerActions.export} {headerActions.import} diff --git a/packages/base/src/page/ResourceOverview/components/ResourceDetail/ResourceTopology/index.tsx b/packages/base/src/page/ResourceOverview/components/ResourceDetail/ResourceTopology/index.tsx new file mode 100644 index 0000000000..c9572812c5 --- /dev/null +++ b/packages/base/src/page/ResourceOverview/components/ResourceDetail/ResourceTopology/index.tsx @@ -0,0 +1,120 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { BasicButton } from '@actiontech/shared'; +import { Card, Tree, Space, type TreeDataNode } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { DownOutlined, RightOutlined } from '@actiontech/icons'; +import { AntTreeNodeProps } from 'antd/es/tree'; +import classNames from 'classnames'; + +interface TreeNodeData extends TreeDataNode { + key: string; + children?: TreeNodeData[]; +} + +interface ResourceTopologyProps { + topology: TreeNodeData[]; + selectedKey?: string; + setSelectedKey: (keys: string) => void; + expandedKeys: string[]; + setExpandedKeys: (keys: string[]) => void; +} + +const ResourceTopology: React.FC = ({ + topology, + selectedKey, + setSelectedKey, + expandedKeys, + setExpandedKeys +}) => { + const { t } = useTranslation(); + const [autoExpandParent, setAutoExpandParent] = useState(true); + const [allKeys, setAllKeys] = useState([]); + + const onExpand = (keys: React.Key[]) => { + setExpandedKeys(keys as string[]); + setAutoExpandParent(false); + }; + + const onSelect = (keys: React.Key[]) => { + setSelectedKey(keys[0] as string); + }; + + const getAllKeys = useCallback(() => { + const keys: string[] = []; + const traverse = (nodes: TreeNodeData[]) => { + nodes.forEach((node) => { + keys.push(node.key); + if (node.children) { + traverse(node.children); + } + }); + }; + traverse(topology ?? []); + return keys; + }, [topology]); + + const toggleExpandAll = useCallback( + (expand: boolean) => { + if (expand) { + setExpandedKeys(allKeys); + } else { + setExpandedKeys([]); + } + setAutoExpandParent(true); + }, + [setExpandedKeys, allKeys] + ); + + useEffect(() => { + if (topology.length > 0) { + const keys = getAllKeys(); + setAllKeys(keys); + setExpandedKeys(keys); + } + }, [topology, getAllKeys, setExpandedKeys]); + + return ( + + toggleExpandAll(true)} + > + {t('resourceOverview.expandAll')} + + toggleExpandAll(false)} + > + {t('resourceOverview.collapseAll')} + + + } + > + { + if (expanded) { + return ; + } + return ; + }} + /> + + ); +}; + +export default ResourceTopology; diff --git a/packages/base/src/page/ResourceOverview/components/ResourceDetail/actions.tsx b/packages/base/src/page/ResourceOverview/components/ResourceDetail/actions.tsx new file mode 100644 index 0000000000..9fc044fc3b --- /dev/null +++ b/packages/base/src/page/ResourceOverview/components/ResourceDetail/actions.tsx @@ -0,0 +1,35 @@ +import { t } from '../../../../locale'; +import { ActiontechTableToolbarActionMeta } from '@actiontech/shared/lib/components/ActiontechTable'; +import { IResourceListData } from '@actiontech/shared/lib/api/base/service/common'; +import { ActiontechTableActionMeta } from '@actiontech/shared/lib/components/ActiontechTable/index.type'; +export const ResourceDetailToolbarActions = ( + onExport: () => void, + exporting: boolean +): ActiontechTableToolbarActionMeta[] => { + return [ + { + key: 'export', + text: t('dmsCloudBeaver.operationList.exportButton'), + buttonProps: { + onClick: onExport, + loading: exporting + } + } + ]; +}; + +export const ResourceDetailTableActions = ( + gotoDataSourceDetail: (record?: IResourceListData) => void +): ActiontechTableActionMeta[] => { + return [ + { + key: 'view-detail', + text: t('resourceOverview.resourceList.viewDetail'), + buttonProps: (record) => ({ + onClick: () => { + gotoDataSourceDetail(record); + } + }) + } + ]; +}; diff --git a/packages/base/src/page/ResourceOverview/components/ResourceDetail/columns.tsx b/packages/base/src/page/ResourceOverview/components/ResourceDetail/columns.tsx new file mode 100644 index 0000000000..a52ebe9536 --- /dev/null +++ b/packages/base/src/page/ResourceOverview/components/ResourceDetail/columns.tsx @@ -0,0 +1,75 @@ +import { t } from '../../../../locale'; +import { IResourceListData } from '@actiontech/shared/lib/api/base/service/common'; +import { ActiontechTableColumn } from '@actiontech/shared/lib/components/ActiontechTable'; +import { IGetResourceOverviewResourceListV1Params } from '@actiontech/shared/lib/api/base/service/ResourceOverview/index.d'; +import { DatabaseTypeLogo } from '@actiontech/shared'; + +export const ResourceDetailListColumns = ( + getLogoUrlByDbType: (dbType: string) => string +): ActiontechTableColumn< + IResourceListData, + IGetResourceOverviewResourceListV1Params, + 'address' +> => { + return [ + { + dataIndex: 'resource_name', + title: t('resourceOverview.resourceList.resourceName') + }, + { + dataIndex: 'resource_type', + title: t('resourceOverview.resourceList.type'), + render: (dbType) => { + if (!dbType) return '-'; + + return ( + + ); + }, + filterCustomType: 'select', + filterKey: 'filter_by_db_type' + }, + { + dataIndex: 'business_tag', + title: t('resourceOverview.resourceList.business'), + render: (businessTag) => { + return businessTag?.name ?? '-'; + }, + filterCustomType: 'select', + filterKey: 'filter_by_business_tag_uid' + }, + { + dataIndex: 'project', + title: t('resourceOverview.resourceList.project'), + render: (project) => { + return project?.project_name ?? '-'; + }, + filterCustomType: 'select', + filterKey: 'filter_by_project_uid' + }, + { + dataIndex: 'environment_tag', + title: t('resourceOverview.resourceList.environment'), + render: (environmentTag) => { + return environmentTag?.name ?? '-'; + }, + filterCustomType: 'select', + filterKey: 'filter_by_environment_tag_uid' + }, + { + dataIndex: 'audit_score', + title: t('resourceOverview.resourceList.auditScore') + }, + { + dataIndex: 'high_priority_sql_count', + title: t('resourceOverview.resourceList.highPrioritySqlCount') + }, + { + dataIndex: 'pending_workflow_count', + title: t('resourceOverview.resourceList.pendingWorkflowCount') + } + ]; +}; diff --git a/packages/base/src/page/ResourceOverview/components/ResourceDetail/hooks/useFilterParams.ts b/packages/base/src/page/ResourceOverview/components/ResourceDetail/hooks/useFilterParams.ts new file mode 100644 index 0000000000..65dcbabf35 --- /dev/null +++ b/packages/base/src/page/ResourceOverview/components/ResourceDetail/hooks/useFilterParams.ts @@ -0,0 +1,113 @@ +import { useMemo, useEffect } from 'react'; +import { useDbServiceDriver } from '@actiontech/shared/lib/features'; +import { FilterCustomProps } from '@actiontech/shared/lib/components/ActiontechTable'; +import useGlobalDataSourceType from '../../../../GlobalDataSource/hooks/useGlobalDataSourceType'; +import useProjectTips from '../../../../../hooks/useProjectTips'; +import useBusinessTag from '../../../../../hooks/useBusinessTag'; +import useServiceEnvironment from 'sqle/src/hooks/useServiceEnvironment'; +import { IResourceListData } from '@actiontech/shared/lib/api/base/service/common'; +import { useTranslation } from 'react-i18next'; + +const useFilterParams = (projectId?: string) => { + const { t } = useTranslation(); + const { getLogoUrlByDbType, updateDriverList } = useDbServiceDriver(); + + const { + updateDbTypeList, + dbTypeOptions, + loading: getDbTypeListLoading + } = useGlobalDataSourceType(); + + const { + projectIDOptions, + updateProjects, + loading: getProjectsLoading + } = useProjectTips(); + + const { + businessTagOptions, + businessTagList, + updateBusinessTagList, + loading: getBusinessTagLoading + } = useBusinessTag(); + + const { + environmentOptions, + updateEnvironmentList, + loading: getEnvironmentTagLoading + } = useServiceEnvironment(); + + const filterCustomProps = useMemo(() => { + return new Map([ + [ + 'resource_type', + { options: dbTypeOptions, loading: getDbTypeListLoading } + ], + [ + 'business_tag', + { + options: businessTagOptions, + loading: getBusinessTagLoading, + onChange: (v: string) => { + const businessTag = businessTagList.find((item) => item.uid === v); + if (businessTag) { + updateProjects({ filter_by_business_tag: businessTag.name }); + } + } + } + ], + [ + 'project', + { + options: projectIDOptions, + loading: getProjectsLoading, + onChange: updateEnvironmentList + } + ], + [ + 'environment_tag', + { + options: environmentOptions, + loading: getEnvironmentTagLoading, + disabled: !projectId, + placeholder: !projectId + ? t('resourceOverview.selectProjectFirst') + : undefined + } + ] + ]); + }, [ + dbTypeOptions, + getDbTypeListLoading, + projectIDOptions, + getProjectsLoading, + businessTagOptions, + getBusinessTagLoading, + environmentOptions, + getEnvironmentTagLoading, + updateEnvironmentList, + projectId, + t, + updateProjects, + businessTagList + ]); + + useEffect(() => { + updateDriverList(); + updateProjects(); + updateDbTypeList(); + updateBusinessTagList(); + }, [ + updateBusinessTagList, + updateDbTypeList, + updateDriverList, + updateProjects + ]); + + return { + getLogoUrlByDbType, + filterCustomProps + }; +}; + +export default useFilterParams; diff --git a/packages/base/src/page/ResourceOverview/components/ResourceDetail/index.tsx b/packages/base/src/page/ResourceOverview/components/ResourceDetail/index.tsx new file mode 100644 index 0000000000..1d78a439bf --- /dev/null +++ b/packages/base/src/page/ResourceOverview/components/ResourceDetail/index.tsx @@ -0,0 +1,275 @@ +import { useMemo, useEffect, useCallback, useState } from 'react'; +import { useTypedNavigate } from '@actiontech/shared'; +import { Card, Row, Col, Typography } from 'antd'; +import { + FlagFilled, + DatabaseFilled, + DatabaseSchemaFilled +} from '@actiontech/icons'; +import useThemeStyleData from '../../../../hooks/useThemeStyleData'; +import { + ActiontechTable, + useTableRequestError, + useTableRequestParams, + useTableFilterContainer, + TableToolbar, + TableFilterContainer, + ActiontechTableWrapper, + ColumnsSettingProps +} from '@actiontech/shared/lib/components/ActiontechTable'; +import { ResourceDetailListColumns } from './columns'; +import { DmsApi } from '@actiontech/shared/lib/api'; +import { useRequest } from 'ahooks'; +import { IResourceListData } from '@actiontech/shared/lib/api/base/service/common'; +import { ResourceDetailWrapper } from '../../style'; +import { useTranslation } from 'react-i18next'; +import { IGetResourceOverviewResourceListV1Params } from '@actiontech/shared/lib/api/base/service/ResourceOverview/index.d'; +import { ResourceDetailTableActions } from './actions'; +import { ROUTE_PATHS } from '@actiontech/shared/lib/data/routePaths'; +import useCurrentUser from '@actiontech/shared/lib/features/useCurrentUser'; +import ResourceTopology from './ResourceTopology'; +import useFilterParams from './hooks/useFilterParams'; +import EmitterKey from '../../../../data/EmitterKey'; +import EventEmitter from '../../../../utils/EventEmitter'; + +const ResourceManagementPage: React.FC = () => { + const { t } = useTranslation(); + const { baseTheme } = useThemeStyleData(); + const { username } = useCurrentUser(); + + const navigate = useTypedNavigate(); + + const [selectedKey, setSelectedKey] = useState(); + const [expandedKeys, setExpandedKeys] = useState([]); + + const { + tableFilterInfo, + updateTableFilterInfo, + tableChange, + pagination, + refreshBySearchKeyword, + searchKeyword, + setSearchKeyword + } = useTableRequestParams< + IResourceListData, + IGetResourceOverviewResourceListV1Params + >(); + + const { getLogoUrlByDbType, filterCustomProps } = useFilterParams( + tableFilterInfo.filter_by_project_uid + ); + + const { requestErrorMessage, handleTableRequestError } = + useTableRequestError(); + + const { + data: resourceList, + loading: getResourceListLoading, + refresh: refreshResourceList + } = useRequest( + () => { + return handleTableRequestError( + DmsApi.ResourceOverviewService.GetResourceOverviewResourceListV1({ + ...tableFilterInfo, + page_index: pagination.page_index, + page_size: pagination.page_size, + fuzzy_search_resource_name: searchKeyword + }) + ); + }, + { + refreshDeps: [pagination, tableFilterInfo] + } + ); + + const { + data: topology, + loading: topologyLoading, + refresh: refreshTopology + } = useRequest( + () => { + return DmsApi.ResourceOverviewService.GetResourceOverviewTopologyV1({ + ...tableFilterInfo + }).then((res) => { + return ( + res.data.data?.map((item, index) => { + return { + key: item.business_tag?.uid ?? '', + title: item.business_tag?.name, + icon: ( + + ), + selectable: false, + className: 'business-tag-item', + children: item.project?.length + ? item.project?.map((project) => { + return { + key: project.project_uid ?? '', + title: project.project_name, + icon: , + selectable: false, + children: project.db_service?.map((service) => { + return { + key: service.db_service_uid ?? '', + title: service.db_service_name, + icon: ( + + ) + }; + }) + }; + }) + : [ + { + key: `no-project-${index}`, + className: 'no-project-tips', + title: ( + + {t('resourceOverview.unboundProject')} + + ), + selectable: false, + children: [] + } + ] + }; + }) ?? [] + ); + }); + }, + { + refreshDeps: [tableFilterInfo] + } + ); + + const columns = useMemo( + () => ResourceDetailListColumns(getLogoUrlByDbType), + [getLogoUrlByDbType] + ); + + const actions = useMemo(() => { + const gotoDataSourceDetail = (record?: IResourceListData) => { + navigate(ROUTE_PATHS.BASE.DATA_SOURCE.index, { + params: { projectID: record?.project?.project_uid ?? '' } + }); + }; + return ResourceDetailTableActions(gotoDataSourceDetail); + }, [navigate]); + + const { filterButtonMeta, filterContainerMeta, updateAllSelectedFilterItem } = + useTableFilterContainer(columns, updateTableFilterInfo); + + const tableSetting = useMemo( + () => ({ + tableName: 'resource_overview_resource_list', + username: username + }), + [username] + ); + + const refreshPage = useCallback(() => { + refreshTopology(); + refreshResourceList(); + }, [refreshTopology, refreshResourceList]); + + useEffect(() => { + const { unsubscribe } = EventEmitter.subscribe( + EmitterKey.Refresh_Resource_Overview_Page, + () => { + refreshPage(); + } + ); + + return unsubscribe; + }, [refreshPage]); + + return ( + + + { + refreshBySearchKeyword(); + } + }} + /> + + + + + + + + { + return `${record?.resource_uid}`; + }} + pagination={{ + total: resourceList?.total ?? 0, + current: pagination.page_index + }} + columns={columns} + errorMessage={requestErrorMessage} + onChange={tableChange} + actions={actions} + onRow={(record) => { + return { + onClick: () => { + setSelectedKey(record?.resource_uid); + if ( + record?.resource_uid && + !expandedKeys.some((i) => i === record?.resource_uid) + ) { + setExpandedKeys([ + ...expandedKeys, + record?.resource_uid + ]); + } + }, + className: + selectedKey === record?.resource_uid + ? 'ant-table-row-selected' + : '' + }; + }} + /> + + + + + + ); +}; + +export default ResourceManagementPage; diff --git a/packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/StatisticCard.tsx b/packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/StatisticCard.tsx new file mode 100644 index 0000000000..21eda6eb0f --- /dev/null +++ b/packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/StatisticCard.tsx @@ -0,0 +1,19 @@ +import { Card, Statistic, Space } from 'antd'; +import { StatisticCardProps } from '../../../index.type'; + +const StatisticCard: React.FC = ({ + title, + count, + icon +}) => { + return ( + +
+
{icon}
+ {title}} value={count} /> +
+
+ ); +}; + +export default StatisticCard; diff --git a/packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/index.tsx b/packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/index.tsx new file mode 100644 index 0000000000..e560f6c9fb --- /dev/null +++ b/packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/index.tsx @@ -0,0 +1,86 @@ +import { useTranslation } from 'react-i18next'; +import { Space, Spin } from 'antd'; +import StatisticCard from './StatisticCard'; +import { + FlagFilled, + DatabaseFilled, + DatabaseSchemaFilled +} from '@actiontech/icons'; +import useThemeStyleData from '../../../../../hooks/useThemeStyleData'; +import { DmsApi } from '@actiontech/shared/lib/api'; +import { useRequest } from 'ahooks'; +import { ResponseCode } from '@actiontech/shared/lib/enum'; +import EmitterKey from '../../../../../data/EmitterKey'; +import EventEmitter from '../../../../../utils/EventEmitter'; +import { useEffect } from 'react'; + +const ResourceOverviewBaseInfo: React.FC = () => { + const { t } = useTranslation(); + + const { baseTheme } = useThemeStyleData(); + + const { + data: overviewData, + loading, + refresh + } = useRequest(() => { + return DmsApi.ResourceOverviewService.GetResourceOverviewStatisticsV1().then( + (res) => { + if (res.data.code === ResponseCode.SUCCESS) { + return res.data.data; + } + } + ); + }); + + useEffect(() => { + const { unsubscribe } = EventEmitter.subscribe( + EmitterKey.Refresh_Resource_Overview_Page, + () => { + refresh(); + } + ); + + return unsubscribe; + }, [refresh]); + + return ( + + + + } + /> + } + /> + + } + /> + + + ); +}; + +export default ResourceOverviewBaseInfo; diff --git a/packages/base/src/page/ResourceOverview/components/Statistic/DistributionChart/index.tsx b/packages/base/src/page/ResourceOverview/components/Statistic/DistributionChart/index.tsx new file mode 100644 index 0000000000..87a65f878e --- /dev/null +++ b/packages/base/src/page/ResourceOverview/components/Statistic/DistributionChart/index.tsx @@ -0,0 +1,219 @@ +import { useRef, useState, useEffect } from 'react'; +import { Pie, PieConfig, Tooltip } from '@ant-design/plots'; +import i18n from 'i18next'; +import { useTranslation } from 'react-i18next'; +import ChartWrapper from 'sqle/src/components/ChartCom/ChartWrapper'; +import useThemeStyleData from '../../../../../hooks/useThemeStyleData'; +import ChartTooltip from 'sqle/src/components/ChartCom/ChartTooltip'; +import { formatParamsBySeparator } from '@actiontech/shared/lib/utils/Tool'; +import { SharedTheme } from '@actiontech/shared/lib/types/theme.type'; +import { floatToNumberPercent } from '@actiontech/shared/lib/utils/Math'; +import { useChangeTheme } from '@actiontech/shared/lib/features'; +import { DmsApi } from '@actiontech/shared/lib/api'; +import { useRequest } from 'ahooks'; +import { ResponseCode } from '@actiontech/shared/lib/enum'; +import EmitterKey from '../../../../../data/EmitterKey'; +import EventEmitter from '../../../../../utils/EventEmitter'; + +const renderTooltipFormatter: Tooltip['formatter'] = (item) => { + return { + name: item.name, + value: item.value + }; +}; + +const renderTooltipCustomContent = ( + dataSource: any[], + sharedTheme: SharedTheme, + totalNumber: number +) => { + const data = dataSource[0]; + if (!data) return null; + const currentColor = data?.color; + return ( + + ); +}; + +const DistributionChart = () => { + const { t } = useTranslation(); + const { currentTheme } = useChangeTheme(); + const { sqleTheme, sharedTheme, baseTheme } = useThemeStyleData(); + const [errorMessage, setErrorMessage] = useState(''); + const sourceTotalNumberRef = useRef(0); + + const { + data: dataSource, + loading, + refresh: getApiData + } = useRequest(() => + DmsApi.ResourceOverviewService.GetResourceOverviewResourceTypeDistributionV1().then( + (res) => { + if (res.data.code === ResponseCode.SUCCESS) { + return res.data.data?.map((item) => { + return { + name: item.resource_type, + value: item.count + }; + }); + } else { + setErrorMessage(res.data.message ?? t('common.unknownError')); + } + } + ) + ); + + const config: PieConfig = { + data: dataSource ?? [], + appendPadding: 20, + angleField: 'value', // 数据值字段 + colorField: 'name', + color: sqleTheme.statistics.rectColor, + radius: 0.9, + innerRadius: 0.85, + pieStyle: { + stroke: baseTheme.resourceOverview.chart.pieStroke, + strokeOpacity: 1, + lineWidth: 4, + lineCap: 'round' + }, + label: false, + legend: { + layout: 'vertical', + position: 'left', + flipPage: false, + reversed: false, + animate: false, + itemSpacing: 16, + offsetY: -10, + textAlign: 'center', + itemName: { + style: { + fill: sqleTheme.reportStatistics.DatabaseSourceOrder.default + .colorTextTertiary, + fontSize: 12, + fontWeight: 500, + fontFamily: 'PlusJakartaSans Medium' + } + }, + marker: { + symbol: 'circle', + style: { + lineWidth: 5, + shadowBlur: 5, + stroke: baseTheme.resourceOverview.chart.markStroke, + shadowColor: baseTheme.resourceOverview.chart.markShowdowColor, + textAlign: 'center' + } + } + }, + statistic: { + title: { + style: { + fontSize: '24px', + lineHeight: '32px', + height: '32px', + color: + sqleTheme.reportStatistics.DatabaseSourceOrder.default.colorText, + fontWeight: 700, + fontFamily: 'PlusJakartaSans Medium', + marginTop: '10px' + }, + customHtml: (container, view, datum, filteredData) => { + const totalNumber = + filteredData?.reduce((prev, next) => prev + next.value, 0) ?? 0; + sourceTotalNumberRef.current = totalNumber; + return formatParamsBySeparator(totalNumber); + } + }, + content: { + style: { + fontSize: '12px', + lineHeight: '20px', + height: '20px', + color: + sqleTheme.reportStatistics.DatabaseSourceOrder.default + .colorTextTertiary, + fontWeight: 400, + marginTop: '14px' + }, + content: t('resourceOverview.distributionChart.sourceTotal') + } + }, + tooltip: { + fields: ['name', 'value'], + formatter: renderTooltipFormatter, + customContent: (title: string, data: any[]) => + renderTooltipCustomContent( + data, + sharedTheme, + sourceTotalNumberRef.current + ) + }, + interactions: [ + { + type: 'element-selected' + }, + { + type: 'element-active' + } + ], + state: { + active: { + style: { + lineWidth: 0, + stroke: baseTheme.resourceOverview.chart.activeStroke, + offsetY: 4 + } + } + } + }; + + useEffect(() => { + const { unsubscribe } = EventEmitter.subscribe( + EmitterKey.Refresh_Resource_Overview_Page, + () => { + getApiData(); + } + ); + + return unsubscribe; + }, [getApiData]); + + return ( + { + getApiData(); + }} + > + + + ); +}; + +export default DistributionChart; +export { renderTooltipFormatter, renderTooltipCustomContent }; diff --git a/packages/base/src/page/ResourceOverview/components/Statistic/index.tsx b/packages/base/src/page/ResourceOverview/components/Statistic/index.tsx new file mode 100644 index 0000000000..348b8a07a4 --- /dev/null +++ b/packages/base/src/page/ResourceOverview/components/Statistic/index.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from 'react-i18next'; +import { Col, Row, Card } from 'antd'; +import DistributionChart from './DistributionChart'; +import ResourceOverviewBaseInfo from './BaseInfo'; + +const ResourceOverviewStatistic: React.FC = () => { + const { t } = useTranslation(); + + return ( + + + + + + + + + + + ); +}; + +export default ResourceOverviewStatistic; diff --git a/packages/base/src/page/ResourceOverview/index.tsx b/packages/base/src/page/ResourceOverview/index.tsx new file mode 100644 index 0000000000..6cbc5881f5 --- /dev/null +++ b/packages/base/src/page/ResourceOverview/index.tsx @@ -0,0 +1,36 @@ +import { PageHeader } from '@actiontech/shared/'; +import { useTranslation } from 'react-i18next'; +import { ResourceOverviewStyleWrapper } from './style'; +import ResourceDetail from './components/ResourceDetail'; +import ResourceOverviewStatistic from './components/Statistic'; +import { TableRefreshButton } from '@actiontech/shared/lib/components/ActiontechTable'; +import { Space } from 'antd'; +import EmitterKey from '../../data/EmitterKey'; +import EventEmitter from '../../utils/EventEmitter'; + +const ResourceOverview = () => { + const { t } = useTranslation(); + + const onRefresh = () => { + EventEmitter.emit(EmitterKey.Refresh_Resource_Overview_Page); + }; + + return ( +
+ + {t('resourceOverview.title')} + + + } + /> + + + + +
+ ); +}; + +export default ResourceOverview; diff --git a/packages/base/src/page/ResourceOverview/index.type.ts b/packages/base/src/page/ResourceOverview/index.type.ts new file mode 100644 index 0000000000..57e485106a --- /dev/null +++ b/packages/base/src/page/ResourceOverview/index.type.ts @@ -0,0 +1,5 @@ +export interface StatisticCardProps { + title: string; + count: number; + icon?: React.ReactNode; +} diff --git a/packages/base/src/page/ResourceOverview/style.ts b/packages/base/src/page/ResourceOverview/style.ts new file mode 100644 index 0000000000..6f94d16bb9 --- /dev/null +++ b/packages/base/src/page/ResourceOverview/style.ts @@ -0,0 +1,80 @@ +import { styled } from '@mui/material/styles'; + +export const ResourceOverviewStyleWrapper = styled('div')` + padding: 24px 0; + + .resource-overview-base-info-wrapper { + padding: 0 40px; + + .resource-overview-base-info-chart-card { + .ant-card-body { + height: 330px; + } + } + } + + .statistic-card-wrapper { + display: flex; + margin-bottom: 24px; + + .ant-space-item { + flex: 1; + } + } + + .ant-card.statistic-card { + .statistic-card-content { + display: flex; + align-items: center; + + .statistic-card-icon { + display: flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 50%; + margin-right: 24px; + background-color: ${({ theme }) => + theme.sharedTheme.basic.colorPrimaryBgHover}; + } + } + + .ant-statistic-content { + font-size: 24px; + font-weight: 600; + } + } +`; + +export const ResourceDetailWrapper = styled('div')` + padding-bottom: 60px; + + .actiontech-table-toolbar-namespace { + border-top: 1px solid + ${({ theme }) => theme.sharedTheme.uiToken.colorBorderSecondary} !important; + } + + .resource-detail-wrapper { + padding: 0 40px; + margin-top: 24px; + } + + .ant-card.resource-detail-table-card { + .ant-card-body { + padding: 0; + margin-top: 1px; + } + } + + .no-project-tips.ant-tree-treenode { + .ant-tree-switcher.ant-tree-switcher-noop { + visibility: hidden; + } + } + + & .ant-btn-default.btn-active { + color: ${({ theme }) => theme.sharedTheme.uiToken.colorPrimary}; + background: ${({ theme }) => theme.sharedTheme.basic.colorPrimaryBgActive}; + } +`; diff --git a/packages/base/src/router/router.base.tsx b/packages/base/src/router/router.base.tsx index 2e3f7c14f0..99ef18a7b8 100644 --- a/packages/base/src/router/router.base.tsx +++ b/packages/base/src/router/router.base.tsx @@ -34,6 +34,8 @@ const GlobalBatchImportDataSource = React.lazy( const GlobalAddDataSource = React.lazy( () => import('../page/DataSource/components/AddDataSource') ); + +const ResourceOverview = React.lazy(() => import('../page/ResourceOverview')); // #endif const Home = React.lazy(() => import('../page/Home')); @@ -153,6 +155,12 @@ export const BaseGlobalRouterConfig: RouterConfigItem[] = [ element: } ] + }, + { + path: ROUTE_PATHS.BASE.RESOURCE_OVERVIEW, + key: 'resourceOverview', + element: , + permission: PERMISSIONS.PAGES.BASE.RESOURCE_OVERVIEW } // #endif ]; diff --git a/packages/base/src/theme/dark/index.ts b/packages/base/src/theme/dark/index.ts index 33984233d5..7376f47f00 100644 --- a/packages/base/src/theme/dark/index.ts +++ b/packages/base/src/theme/dark/index.ts @@ -5,6 +5,7 @@ import { systemTheme } from './system'; import { dataExportTheme } from './dataExport'; import { sqleDarkTheme } from 'sqle/src/theme/dark'; import { iconTheme } from './icon'; +import { resourceOverviewTheme } from './resourceOverview'; const darkTheme = createTheme({ baseTheme: { @@ -12,7 +13,8 @@ const darkTheme = createTheme({ guidance: guidanceTheme, system: systemTheme, dataExport: dataExportTheme, - icon: iconTheme + icon: iconTheme, + resourceOverview: resourceOverviewTheme }, sqleTheme: sqleDarkTheme }); diff --git a/packages/base/src/theme/dark/resourceOverview.ts b/packages/base/src/theme/dark/resourceOverview.ts new file mode 100644 index 0000000000..d2c628c11a --- /dev/null +++ b/packages/base/src/theme/dark/resourceOverview.ts @@ -0,0 +1,14 @@ +import { ResourceOverviewTheme } from '../type'; + +export const resourceOverviewTheme: ResourceOverviewTheme = { + icon: { + databaseFilled: '#4583FF', + schemaFilled: '#15C7D4' + }, + chart: { + pieStroke: '#fff', + markStroke: 'rgba(26, 206, 219, 0.2)', + markShowdowColor: 'rgba(26, 206, 219, 0.2)', + activeStroke: '#fff' + } +}; diff --git a/packages/base/src/theme/light/index.ts b/packages/base/src/theme/light/index.ts index 66959630ed..8d94bad216 100644 --- a/packages/base/src/theme/light/index.ts +++ b/packages/base/src/theme/light/index.ts @@ -5,6 +5,7 @@ import { sqleLightTheme } from 'sqle/src/theme/light'; import { systemTheme } from './system'; import { dataExportTheme } from './dataExport'; import { iconTheme } from './icon'; +import { resourceOverviewTheme } from './resourceOverview'; const lightTheme = createTheme({ baseTheme: { @@ -12,7 +13,8 @@ const lightTheme = createTheme({ guidance: guidanceTheme, system: systemTheme, dataExport: dataExportTheme, - icon: iconTheme + icon: iconTheme, + resourceOverview: resourceOverviewTheme }, sqleTheme: sqleLightTheme }); diff --git a/packages/base/src/theme/light/resourceOverview.ts b/packages/base/src/theme/light/resourceOverview.ts new file mode 100644 index 0000000000..3ca5f534e7 --- /dev/null +++ b/packages/base/src/theme/light/resourceOverview.ts @@ -0,0 +1,14 @@ +import { ResourceOverviewTheme } from '../type'; + +export const resourceOverviewTheme: ResourceOverviewTheme = { + icon: { + databaseFilled: '#4583FF', + schemaFilled: '#15C7D4' + }, + chart: { + pieStroke: '#ffffff', + markStroke: 'rgba(26, 206, 219, 0.2)', + markShowdowColor: 'rgba(26, 206, 219, 0.2)', + activeStroke: '#ffffff' + } +}; diff --git a/packages/base/src/theme/type.ts b/packages/base/src/theme/type.ts index cf427abcbc..eb485f1157 100644 --- a/packages/base/src/theme/type.ts +++ b/packages/base/src/theme/type.ts @@ -100,3 +100,16 @@ export type IconTheme = { modify: string; }; }; + +export type ResourceOverviewTheme = { + icon: { + databaseFilled: string; + schemaFilled: string; + }; + chart: { + pieStroke: string; + markStroke: string; + markShowdowColor: string; + activeStroke: string; + }; +}; diff --git a/packages/base/src/types/theme.type.ts b/packages/base/src/types/theme.type.ts index 9cf0c5617b..70a7198502 100644 --- a/packages/base/src/types/theme.type.ts +++ b/packages/base/src/types/theme.type.ts @@ -3,7 +3,8 @@ import { GuidanceTheme, SideMenuTheme, SystemTheme, - IconTheme + IconTheme, + ResourceOverviewTheme } from '../theme/type'; export interface BaseTheme { @@ -12,6 +13,7 @@ export interface BaseTheme { system: SystemTheme; dataExport: DataExportTheme; icon: IconTheme; + resourceOverview: ResourceOverviewTheme; } interface ThemeCustom { diff --git a/packages/shared/lib/data/routePaths.ts b/packages/shared/lib/data/routePaths.ts index b682648b20..9892833ab7 100644 --- a/packages/shared/lib/data/routePaths.ts +++ b/packages/shared/lib/data/routePaths.ts @@ -116,7 +116,8 @@ export const ROUTE_PATHS = { path: ':projectID/cloud-beaver', query: OPEN_CLOUD_BEAVER_URL_PARAM_NAME } - } + }, + RESOURCE_OVERVIEW: '/resource-overview' }, SQLE: { PREFIX: '/sqle', diff --git a/packages/shared/lib/features/usePermission/permissionManifest.ts b/packages/shared/lib/features/usePermission/permissionManifest.ts index e1483a7252..adeb88239f 100644 --- a/packages/shared/lib/features/usePermission/permissionManifest.ts +++ b/packages/shared/lib/features/usePermission/permissionManifest.ts @@ -80,6 +80,16 @@ export const PERMISSION_MANIFEST: Record< type: 'page', moduleSupport: [SystemModuleSupported.knowledge] }, + [PERMISSIONS.PAGES.BASE.RESOURCE_OVERVIEW]: { + id: PERMISSIONS.PAGES.BASE.RESOURCE_OVERVIEW, + type: 'page', + role: [ + SystemRole.admin, + SystemRole.globalManager, + SystemRole.globalViewing, + SystemRole.certainProjectManager + ] + }, // action //cloud beaver @@ -866,6 +876,16 @@ export const PERMISSION_MANIFEST: Record< projectManager: true, role: [SystemRole.admin, SystemRole.globalManager] }, + [PERMISSIONS.ACTIONS.BASE.PROJECT_MANAGER.GLOBAL_RESOURCE_OVERVIEW]: { + id: PERMISSIONS.ACTIONS.BASE.PROJECT_MANAGER.GLOBAL_RESOURCE_OVERVIEW, + type: 'action', + role: [ + SystemRole.admin, + SystemRole.globalManager, + SystemRole.globalViewing, + SystemRole.certainProjectManager + ] + }, // 版本管理 [PERMISSIONS.ACTIONS.SQLE.VERSION_MANAGEMENT.ADD]: { diff --git a/packages/shared/lib/features/usePermission/permissions.ts b/packages/shared/lib/features/usePermission/permissions.ts index 337109c72c..cc8730db1b 100644 --- a/packages/shared/lib/features/usePermission/permissions.ts +++ b/packages/shared/lib/features/usePermission/permissions.ts @@ -5,7 +5,8 @@ export const PERMISSIONS = { DATA_SOURCE_MANAGEMENT: 'page:data_source_management', GLOBAL_DATA_SOURCE: 'page:global_data_source', SYNC_DATA_SOURCE: 'page:sync_data_source', - SYSTEM_SETTING: 'page:system_setting' + SYSTEM_SETTING: 'page:system_setting', + RESOURCE_OVERVIEW: 'page:resource_overview' }, SQLE: { OPERATION_RECORD: 'page:operation_record', @@ -116,7 +117,9 @@ export const PERMISSIONS = { EDIT: 'action:project_manager_edit', DELETE: 'action:project_manager_delete', ARCHIVE: 'action:project_manager_archive', - UNARCHIVE: 'action:project_manager_unarchive' + UNARCHIVE: 'action:project_manager_unarchive', + GLOBAL_RESOURCE_OVERVIEW: + 'action:project_manager_global_resource_overview' }, HOME: { ALL_OPERATIONS: 'action:home_all_operations' From f79c9087e8272288ea938dcd1c1d8fc043c5193a Mon Sep 17 00:00:00 2001 From: zzyangh <799463087@qq.com> Date: Thu, 17 Apr 2025 17:09:09 +0800 Subject: [PATCH 5/8] [test]: Update unit testing --- .../Project/__snapshots__/index.test.tsx.snap | 34 +++++++++++++++++++ packages/base/src/page/Project/index.test.tsx | 16 +++++++++ .../__snapshots__/router.base.test.tsx.snap | 17 ++++++++++ .../__snapshots__/router.sqle.test.tsx.snap | 7 ++++ .../base/src/router/test/router.base.test.tsx | 9 +++++ .../__snapshots__/index.test.ts.snap | 22 ++++++++++++ 6 files changed, 105 insertions(+) diff --git a/packages/base/src/page/Project/__snapshots__/index.test.tsx.snap b/packages/base/src/page/Project/__snapshots__/index.test.tsx.snap index 17bf9503e4..206a4489c6 100644 --- a/packages/base/src/page/Project/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/Project/__snapshots__/index.test.tsx.snap @@ -51,6 +51,23 @@ exports[`test base/page/project should match snapshot 1`] = `
+
+
{ expect(screen.getByText('创建项目')).toBeInTheDocument(); expect(screen.getByText('导 入')).toBeInTheDocument(); expect(screen.getByText('导 出')).toBeInTheDocument(); + expect(screen.getByText('资源全景视图')).toBeInTheDocument(); fireEvent.click(screen.getByText('创建项目')); expect(dispatchSpy).toHaveBeenCalledTimes(2); @@ -130,6 +131,21 @@ describe('test base/page/project', () => { expect(screen.queryByText('创建项目')).not.toBeInTheDocument(); expect(screen.queryByText('导 入')).not.toBeInTheDocument(); expect(screen.queryByText('导 出')).not.toBeInTheDocument(); + + cleanup(); + jest.clearAllMocks(); + jest.clearAllTimers(); + mockUseCurrentUser({ + userRoles: { + ...mockCurrentUserReturn.userRoles, + [SystemRole.admin]: false, + [SystemRole.globalManager]: false, + [SystemRole.certainProjectManager]: false, + [SystemRole.globalViewing]: false + } + }); + superRender(); + expect(screen.queryByText('资源全景视图')).not.toBeInTheDocument(); }); it('should export project info', async () => { diff --git a/packages/base/src/router/test/__snapshots__/router.base.test.tsx.snap b/packages/base/src/router/test/__snapshots__/router.base.test.tsx.snap index 06e8fb0768..6d51c3aff3 100644 --- a/packages/base/src/router/test/__snapshots__/router.base.test.tsx.snap +++ b/packages/base/src/router/test/__snapshots__/router.base.test.tsx.snap @@ -113,6 +113,13 @@ exports[`base/router-base-ee render base route data snap 1`] = ` "key": "project", "path": "/project", }, + { + "element":
+ resourceOverview +
, + "key": "resourceOverview", + "path": "/resource-overview", + }, { "children": [ { @@ -436,6 +443,16 @@ exports[`base/router-base-ee render base router when version is ee render route `; +exports[`base/router-base-ee render base router when version is ee render route resourceOverview render router resourceOverview 1`] = ` + +
+
+ resourceOverview +
+
+ +`; + exports[`base/router-base-ee render base router when version is ee render route syncDataSource render route syncDataSourceList 1`] = `
diff --git a/packages/base/src/router/test/__snapshots__/router.sqle.test.tsx.snap b/packages/base/src/router/test/__snapshots__/router.sqle.test.tsx.snap index ce879c4c75..4183a208e6 100644 --- a/packages/base/src/router/test/__snapshots__/router.sqle.test.tsx.snap +++ b/packages/base/src/router/test/__snapshots__/router.sqle.test.tsx.snap @@ -695,6 +695,13 @@ exports[`base/router-sqle-ee render sqle route data snap 1`] = ` "key": "project", "path": "/project", }, + { + "element":
+ resourceOverview +
, + "key": "resourceOverview", + "path": "/resource-overview", + }, { "children": [ { diff --git a/packages/base/src/router/test/router.base.test.tsx b/packages/base/src/router/test/router.base.test.tsx index 77a7a7bab4..792168cf47 100644 --- a/packages/base/src/router/test/router.base.test.tsx +++ b/packages/base/src/router/test/router.base.test.tsx @@ -183,6 +183,15 @@ describe('base/router-base-ee', () => { }); }); + describe('render route resourceOverview', () => { + it('render router resourceOverview', async () => { + const { baseElement } = customRender(['/resource-overview']); + + expect(screen.getByText('resourceOverview')).toBeInTheDocument(); + expect(baseElement).toMatchSnapshot(); + }); + }); + describe('render route syncDataSource', () => { it('render route syncDataSourceList', async () => { const { baseElement } = customRender([ diff --git a/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap b/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap index 2c0c340cf1..a6dccc09cf 100644 --- a/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap @@ -56,6 +56,7 @@ exports[`usePermission should match snapshot 1`] = ` "DELETE": "action:project_manager_delete", "EDIT": "action:project_manager_edit", "EXPORT": "action:project_manager_export", + "GLOBAL_RESOURCE_OVERVIEW": "action:project_manager_global_resource_overview", "IMPORT": "action:project_manager_import", "UNARCHIVE": "action:project_manager_unarchive", }, @@ -220,6 +221,7 @@ exports[`usePermission should match snapshot 1`] = ` "BASE": { "DATA_SOURCE_MANAGEMENT": "page:data_source_management", "GLOBAL_DATA_SOURCE": "page:global_data_source", + "RESOURCE_OVERVIEW": "page:resource_overview", "SYNC_DATA_SOURCE": "page:sync_data_source", "SYSTEM_SETTING": "page:system_setting", "USER_CENTER": "page:user_center", @@ -1052,6 +1054,16 @@ exports[`usePermission should match snapshot 2`] = ` ], "type": "action", }, + "action:project_manager_global_resource_overview": { + "id": "action:project_manager_global_resource_overview", + "role": [ + "admin", + "globalManager", + "globalViewing", + "certainProjectManager", + ], + "type": "action", + }, "action:project_manager_import": { "id": "action:project_manager_import", "role": [ @@ -1467,6 +1479,16 @@ exports[`usePermission should match snapshot 2`] = ` ], "type": "page", }, + "page:resource_overview": { + "id": "page:resource_overview", + "role": [ + "admin", + "globalManager", + "globalViewing", + "certainProjectManager", + ], + "type": "page", + }, "page:rule_management": { "id": "page:rule_management", "role": [ From 2f68d55cdcfa57a0d290772d427c5820d45e3466 Mon Sep 17 00:00:00 2001 From: zzyangh <799463087@qq.com> Date: Thu, 17 Apr 2025 17:42:52 +0800 Subject: [PATCH 6/8] [fix]: Filter project by business name --- .../__snapshots__/index.test.tsx.snap | 12 ++++++++++++ .../ResourceDetail/hooks/useFilterParams.ts | 4 +--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/base/src/hooks/useThemeStyleData/__snapshots__/index.test.tsx.snap b/packages/base/src/hooks/useThemeStyleData/__snapshots__/index.test.tsx.snap index 54d97c15dc..a2783cf0ce 100644 --- a/packages/base/src/hooks/useThemeStyleData/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/hooks/useThemeStyleData/__snapshots__/index.test.tsx.snap @@ -84,6 +84,18 @@ exports[`test base/hooks/useThemeStyleData should match snapshot 1`] = ` "modify": "#292C33", }, }, + "resourceOverview": { + "chart": { + "activeStroke": "#ffffff", + "markShowdowColor": "rgba(26, 206, 219, 0.2)", + "markStroke": "rgba(26, 206, 219, 0.2)", + "pieStroke": "#ffffff", + }, + "icon": { + "databaseFilled": "#4583FF", + "schemaFilled": "#15C7D4", + }, + }, "sideMenu": { "projectSelector": { "dropdown": { diff --git a/packages/base/src/page/ResourceOverview/components/ResourceDetail/hooks/useFilterParams.ts b/packages/base/src/page/ResourceOverview/components/ResourceDetail/hooks/useFilterParams.ts index 65dcbabf35..26eca47ca1 100644 --- a/packages/base/src/page/ResourceOverview/components/ResourceDetail/hooks/useFilterParams.ts +++ b/packages/base/src/page/ResourceOverview/components/ResourceDetail/hooks/useFilterParams.ts @@ -50,9 +50,7 @@ const useFilterParams = (projectId?: string) => { loading: getBusinessTagLoading, onChange: (v: string) => { const businessTag = businessTagList.find((item) => item.uid === v); - if (businessTag) { - updateProjects({ filter_by_business_tag: businessTag.name }); - } + updateProjects({ filter_by_business_tag: businessTag?.name }); } } ], From 8bcbf6fceb6baefb27ffa5415b7fbcb156505027 Mon Sep 17 00:00:00 2001 From: zzyangh <799463087@qq.com> Date: Fri, 18 Apr 2025 10:33:32 +0800 Subject: [PATCH 7/8] [style]: Modify Card and table pagination style --- .../ResourceDetail/ResourceTopology/index.tsx | 1 + .../components/ResourceDetail/index.tsx | 2 ++ .../components/Statistic/BaseInfo/StatisticCard.tsx | 2 +- .../ResourceOverview/components/Statistic/index.tsx | 1 + packages/base/src/page/ResourceOverview/index.tsx | 13 ++++++++----- packages/base/src/page/ResourceOverview/style.ts | 11 ++++++++++- 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/base/src/page/ResourceOverview/components/ResourceDetail/ResourceTopology/index.tsx b/packages/base/src/page/ResourceOverview/components/ResourceDetail/ResourceTopology/index.tsx index c9572812c5..78b5cb8467 100644 --- a/packages/base/src/page/ResourceOverview/components/ResourceDetail/ResourceTopology/index.tsx +++ b/packages/base/src/page/ResourceOverview/components/ResourceDetail/ResourceTopology/index.tsx @@ -96,6 +96,7 @@ const ResourceTopology: React.FC = ({ } + hoverable > { { total: resourceList?.total ?? 0, current: pagination.page_index }} + isPaginationFixed={false} columns={columns} errorMessage={requestErrorMessage} onChange={tableChange} diff --git a/packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/StatisticCard.tsx b/packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/StatisticCard.tsx index 21eda6eb0f..12ee6de429 100644 --- a/packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/StatisticCard.tsx +++ b/packages/base/src/page/ResourceOverview/components/Statistic/BaseInfo/StatisticCard.tsx @@ -7,7 +7,7 @@ const StatisticCard: React.FC = ({ icon }) => { return ( - +
{icon}
{title}} value={count} /> diff --git a/packages/base/src/page/ResourceOverview/components/Statistic/index.tsx b/packages/base/src/page/ResourceOverview/components/Statistic/index.tsx index 348b8a07a4..08ea0fc7eb 100644 --- a/packages/base/src/page/ResourceOverview/components/Statistic/index.tsx +++ b/packages/base/src/page/ResourceOverview/components/Statistic/index.tsx @@ -15,6 +15,7 @@ const ResourceOverviewStatistic: React.FC = () => { diff --git a/packages/base/src/page/ResourceOverview/index.tsx b/packages/base/src/page/ResourceOverview/index.tsx index 6cbc5881f5..8ba6f331aa 100644 --- a/packages/base/src/page/ResourceOverview/index.tsx +++ b/packages/base/src/page/ResourceOverview/index.tsx @@ -1,6 +1,9 @@ import { PageHeader } from '@actiontech/shared/'; import { useTranslation } from 'react-i18next'; -import { ResourceOverviewStyleWrapper } from './style'; +import { + ResourceOverviewDetailStyleWrapper, + ResourceOverviewStyleWrapper +} from './style'; import ResourceDetail from './components/ResourceDetail'; import ResourceOverviewStatistic from './components/Statistic'; import { TableRefreshButton } from '@actiontech/shared/lib/components/ActiontechTable'; @@ -16,7 +19,7 @@ const ResourceOverview = () => { }; return ( -
+ @@ -25,11 +28,11 @@ const ResourceOverview = () => { } /> - + - -
+ + ); }; diff --git a/packages/base/src/page/ResourceOverview/style.ts b/packages/base/src/page/ResourceOverview/style.ts index 6f94d16bb9..45913e5e87 100644 --- a/packages/base/src/page/ResourceOverview/style.ts +++ b/packages/base/src/page/ResourceOverview/style.ts @@ -1,6 +1,11 @@ import { styled } from '@mui/material/styles'; -export const ResourceOverviewStyleWrapper = styled('div')` +export const ResourceOverviewStyleWrapper = styled('section')` + background-color: ${({ theme }) => + theme.sharedTheme.uiToken.colorFillTertiary}; +`; + +export const ResourceOverviewDetailStyleWrapper = styled('div')` padding: 24px 0; .resource-overview-base-info-wrapper { @@ -77,4 +82,8 @@ export const ResourceDetailWrapper = styled('div')` color: ${({ theme }) => theme.sharedTheme.uiToken.colorPrimary}; background: ${({ theme }) => theme.sharedTheme.basic.colorPrimaryBgActive}; } + + & .ant-table-wrapper.actiontech-table-namespace.cursor-pointer { + padding-bottom: 0; + } `; From 5fffe1f7109ce1126dd7e14b502029c38e3a66ea Mon Sep 17 00:00:00 2001 From: zzyangh <799463087@qq.com> Date: Fri, 18 Apr 2025 13:17:22 +0800 Subject: [PATCH 8/8] [style](ResourceOverview): Modify filter and table column style --- .../base/src/locale/zh-CN/resourceOverview.ts | 1 + .../components/ResourceDetail/columns.tsx | 31 ++++++++++--- .../components/ResourceDetail/index.tsx | 45 ++++++++++--------- .../base/src/page/ResourceOverview/style.ts | 20 ++++++--- 4 files changed, 66 insertions(+), 31 deletions(-) diff --git a/packages/base/src/locale/zh-CN/resourceOverview.ts b/packages/base/src/locale/zh-CN/resourceOverview.ts index 95039c5b9d..6543b9a673 100644 --- a/packages/base/src/locale/zh-CN/resourceOverview.ts +++ b/packages/base/src/locale/zh-CN/resourceOverview.ts @@ -21,6 +21,7 @@ export default { project: '所属项目', environment: '所属环境', auditScore: '审核质量评分', + auditScoreTips: '分数区间是(0-100)分', highPrioritySqlCount: '高优先级SQL数', pendingWorkflowCount: '待处理工单数量', viewDetail: '查看详情' diff --git a/packages/base/src/page/ResourceOverview/components/ResourceDetail/columns.tsx b/packages/base/src/page/ResourceOverview/components/ResourceDetail/columns.tsx index a52ebe9536..8847dee86a 100644 --- a/packages/base/src/page/ResourceOverview/components/ResourceDetail/columns.tsx +++ b/packages/base/src/page/ResourceOverview/components/ResourceDetail/columns.tsx @@ -2,10 +2,14 @@ import { t } from '../../../../locale'; import { IResourceListData } from '@actiontech/shared/lib/api/base/service/common'; import { ActiontechTableColumn } from '@actiontech/shared/lib/components/ActiontechTable'; import { IGetResourceOverviewResourceListV1Params } from '@actiontech/shared/lib/api/base/service/ResourceOverview/index.d'; -import { DatabaseTypeLogo } from '@actiontech/shared'; +import { DatabaseTypeLogo, BasicToolTip } from '@actiontech/shared'; +import { TableColumnWithIconStyleWrapper } from '@actiontech/shared/lib/styleWrapper/element'; +import { FlagFilled, DatabaseSchemaFilled } from '@actiontech/icons'; +import { ResourceOverviewTheme } from '../../../../theme/type'; export const ResourceDetailListColumns = ( - getLogoUrlByDbType: (dbType: string) => string + getLogoUrlByDbType: (dbType: string) => string, + theme: ResourceOverviewTheme ): ActiontechTableColumn< IResourceListData, IGetResourceOverviewResourceListV1Params, @@ -36,7 +40,12 @@ export const ResourceDetailListColumns = ( dataIndex: 'business_tag', title: t('resourceOverview.resourceList.business'), render: (businessTag) => { - return businessTag?.name ?? '-'; + return ( + + + {businessTag?.name} + + ); }, filterCustomType: 'select', filterKey: 'filter_by_business_tag_uid' @@ -45,7 +54,12 @@ export const ResourceDetailListColumns = ( dataIndex: 'project', title: t('resourceOverview.resourceList.project'), render: (project) => { - return project?.project_name ?? '-'; + return ( + + + {project?.project_name} + + ); }, filterCustomType: 'select', filterKey: 'filter_by_project_uid' @@ -61,7 +75,14 @@ export const ResourceDetailListColumns = ( }, { dataIndex: 'audit_score', - title: t('resourceOverview.resourceList.auditScore') + title: ( + + {t('resourceOverview.resourceList.auditScore')} + + ) }, { dataIndex: 'high_priority_sql_count', diff --git a/packages/base/src/page/ResourceOverview/components/ResourceDetail/index.tsx b/packages/base/src/page/ResourceOverview/components/ResourceDetail/index.tsx index b4e02cf425..61228484cd 100644 --- a/packages/base/src/page/ResourceOverview/components/ResourceDetail/index.tsx +++ b/packages/base/src/page/ResourceOverview/components/ResourceDetail/index.tsx @@ -149,8 +149,9 @@ const ResourceManagementPage: React.FC = () => { ); const columns = useMemo( - () => ResourceDetailListColumns(getLogoUrlByDbType), - [getLogoUrlByDbType] + () => + ResourceDetailListColumns(getLogoUrlByDbType, baseTheme.resourceOverview), + [getLogoUrlByDbType, baseTheme.resourceOverview] ); const actions = useMemo(() => { @@ -195,25 +196,27 @@ const ResourceManagementPage: React.FC = () => { loading={topologyLoading || getResourceListLoading} setting={tableSetting} > - { - refreshBySearchKeyword(); - } - }} - /> - + + { + refreshBySearchKeyword(); + } + }} + /> + + theme.sharedTheme.uiToken.colorBorderSecondary} !important; + .resource-detail-table-toolbar-card.ant-card { + .ant-card-body { + padding: 0; + } + } + + .actiontech-table-toolbar-namespace, + .actiontech-table-filter-container-namespace { + border: 0; + border-radius: 8px; } .resource-detail-wrapper { - padding: 0 40px; margin-top: 24px; } @@ -85,5 +91,9 @@ export const ResourceDetailWrapper = styled('div')` & .ant-table-wrapper.actiontech-table-namespace.cursor-pointer { padding-bottom: 0; + + .actiontech-table-pagination { + border-radius: 8px; + } } `;